大部分程序员认为这就是他们的全部工作。他们的工作是且仅是:按照需求文档编写代码,并且修复任何 Bug。这真是大错特错。
从系统相关方(Stakeholder)的角度来看,他们所提出的一系列的变更需求的范畴都是类似的,因此成本也应该是固定的。但是从研发者角度来看,系统用户持续不断的变更需求就像是要求他们不停地用一堆不同形状的拼图块,拼成一个新的形状。整个拼图的过程越来越困难,因为现有系统的形状永远和需求的形状不一致.
问题的实际根源当然就是系统的架构设计。如果系统的架构设计偏向某种特定的“形状”,那么新的变更就会越来越难以实施。所以,好的系统架构设计应该尽可能做到与“形状”无关。
业务部门与研发人员经常犯的共同错误就是将第三优先级的事情提到第一优先级去做。换句话说,他们没有把真正紧急并且重要的功能和紧急但是不重要的功能分开。这个错误导致了重要的事被忽略了,重要的系统架构问题让位给了不重要的系统行为功能。
但研发人员还忘了一点,那就是业务部门原本就是没有能力评估系统架构的重要程度的,这本来就应该是研发人员自己的工作职责!所以,平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。
有成效的软件研发团队会迎难而上,毫不掩饰地与所有其他的系统相关方进行平等的争吵。请记住,作为—名软件开发人员,你也是相关者之一。软件系统的可维护性需要由你来保护,这是你角色的一部分,也是你职责中不可缺少的一部分。公司雇你的很大一部分原因就是需要有人来做这件事。
Dijkstra 曾经说过“测试只能展示 Bug 的存在,并不能证明不存在 Bug”,换句话说,一段程序可以由一个测试来证明其错误性,但是却不能被证明是正确的。测试的作用是让我们得出某段程序已经足够实现当前目标这一结论。
结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。这就是为什么现代编程语言一般不支持无限制的 goto 语句。更重要的是,这也是为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一。
面向对象编程到底是什么?业界在这个问题上存在着很多不同的说法和意见。然而对一个软件架构师来说,其含义应该是非常明确的:面向对象编程就是以对象为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以编译成插件,实现独立于高层组件的开发和部署。
这里的要点是:一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。
软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。
单一职责原则主要讨论的是函数和类之间的关系——但是它在两个讨论层面上会以不同的形式出现。在组件层面,我们可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它则是用于奠定架构边界的变更轴心(Axis of Change)。我们在接下来的章节中会深入学习这些原则。
过去,我们对组件在构建过程中要遵循的组合原则的理解要比 REP、CCP、CRP 这三个原则更有限。我们最初所理解的组合原则可能完全基于单一职责原则。然而,本章介绍的这三个原则为我们描述了一个更为复杂的决策过程。在决定将哪些类归为同一个组件时,必须要考虑到研发性与复用性之间的矛盾,并根据应用程序的需要来平衡这两个矛盾,这是一件很不容易的事。而且,这种平衡本身也在不断变化。也就是说,当下适用的分割方式可能明年就不再适用了。所以,组件的构成安排应随着项目重心的不同,以及研发性与复用性的不同而不断演化。
组件依赖关系图中不应该出现环。
1.应用依赖反转原则(DIP):在图 14.3 中,我们可以创建一个 User 类需要使用的接口,然后将这个接口放入 Entities 组件,并在 Authorizer 组件中继承它。这样就将 Entities 与 Authorizer 之间的依赖关系反转了,自然也就打破了循环依赖关系。
2.创建一个新的组件,并让 Entities 与 Authorize 这两个组件都依赖于它。将现有的这两个组件中互相依赖的类全部放入新组件(如图 14.4 所示)。
如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。因此,组件依赖关系是必须要随着项目的逻辑设计一起扩张和演进的。
稳定依赖原则(SDP)的要求是让每个组件的/指标都必须大于其所依赖组件的 I 指标。也就是说,组件结构依赖图中各组件的/指标必须要按其依赖关系方向递减。
Fan-in:入向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。
I:不稳定性,I=Fan-out/(Fan-in+Fan-out).该指标的范围是[0,1],I=0 意味着组件是最稳定的,I=1 意味着组件是最不稳定的。
在一个软件系统中,总有些部分是不应该经常发生变更的。这些部分通常用于表现该系统的高阶架构设计及一些策略相关的高阶决策。我们不想让这些业务决策和架构设计经常发生变更,因此这些代表了系统咼阶策略的组件应该被放到稳定组件(I=0)中,而不稳定的组件(I=1)中应该只包含那些我们想要快速和方便修改的部分。
然而,如果我们将高阶策略放入稳定组件中,那么用于描述那些策略的源代码就很难被修改了。这可能会导致整个系统的架构设计难于被修改。如何才能让一个无限稳定的组件(I=0)接受变更呢?开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造一个足够灵活、能够被扩展,而且不需要修改的类是可能的,而这正是我们所需要的。哪一种类符合这个原则呢?答案是抽象类。
稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的稳定性就可以通过具体的代码被轻易修改。
稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的稳定性就可以通过具体的代码被轻易修改。
因此,如果一个组件想要成为稳定组件,那么它就应该由接口和抽象类组成,以便将来做扩展。如此,这些既稳定又便于扩展的组件可以被组合成既灵活又不会受到过度限制的架构。
将 SAP 与 SDP 这两个原则结合起来,就等于组件层次上的 DIP。因为 SDP 要求的是让依赖关系指向更稳定的方向,而 SAP 则告诉我们稳定性本身就隐含了对抽象化的要求,即依赖关系应该指向更抽象的方向。
然而,DIP 毕竟是与类这个层次有关的原则——对类来说,设计是没有灰色地带的。一个类要么是抽象类,要么就不是。SDP 与 SAP 这对原则是应用在组件层面上的,我们要允许一个组件部分抽象,部分稳定。
在图 14.13 中,假设某个组件处于(0,0)位置,那么它应该是一个非常稳定但也非常具体的组件。这样的组件在设计上是不佳的,因为它很难被修改,这意味着该组件不能被扩展。这样一来,因为这个组件不是抽象的,而且它又由于稳定性的原因变得特别难以被修改,我们并不希望一个设计良好的组件贴近这个区域,因此(0,0)周围的这个区域被我们称为痛苦区(zone of pain)。
当然,有些软件组件确实会处于这个区域中,这方面的一个典型案例就是数据库的表结构(schema)。它在可变性上可谓臭名昭著,但是它同时又非常具体,并被非常多的组件依赖。这就是面向对象应用程序与数据库之间的接口这么难以管理,以及每次更新数据库的过程都那么痛苦的原因。
现在我们来看看靠近(1,1)这一位置点的组件。该位置上的组件不会是我们想要的,因为这些组件通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用。因此我们将这个区域称为无用区。
“架构”这个词给人的直观感受就充满了权力与神秘感,因此谈论架构总让人有一种正在进行责任重大的决策或者深度技术分析的感觉。毕竟,进阶到软件架构这一层次是我们走技术路线的人的终极目标。一个软件架构师总是给人一种权力非凡、广受尊敬的感觉,有哪个年轻的工程师没有梦想过成为一个软件架构师呢?
首先,软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议。不是这样的!软件架构师其实应该是能力最强的一群程序员,他们通常会在自身承接编程任务的同时。逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。也许软件架构师生产的代码量不是最多的,但是他们必须不停地承接编程任务。如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
基本上,所有的软件系统都可以降解为策略和细节这两种主要元素。策略体现的是软件中所有的业务规则与操作过程,因此它是系统真正的价值所在。
而细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互,但是又不会影响到策略本身的行为。它们包括 I/O 设备、数据库、Web 系统、服务器、框架、交互协议等。
软件架构师的目标是创建一种系统形态,该形态会以策略为最基本的元素,并让细节与策略脱离关系,以允许在具体决策过程中推迟或延迟与细节相关的内容。
在开发的早期阶段应该无须选择数据库系统,因为软件的高层策略不应该关心其底层到底使用哪一种数据库。事实上,如果软件架构师足够小心,软件的高层策略甚至可以不用关心该数据库是关系型数据库,还是分布式数据库,是多级数据库,还只是一些文本文件而已。
在开发的早期阶段也不应该选定使用的 Web 服务,因为高层策略并不应该知道自己未来要以网页形式发布。如果高层策略能够与 HTML、AJAX、JSP、JSF 或任何 Web 开发技术脱钩,那么我们就可以将对 Web 系统的选择推迟到项目的最后阶段。事实上,很有可能我们压根不需要考虑这个系统到底是不是以网页形式发布的。
在开发的早期阶段不应该过早地采用 REST 模式,因为软件的高层策略应该与外部接口无关。同样的,我们也不应该过早地考虑采用微服务框架、SOA 框架等。再说一遍,软件的高层策略压根不应该跟这些有关。
在开发的早期阶段不应过早地采用依赖注入框架(dependency injection framework),因为高层策略不应该操心如何解析系统的依赖关系。
--------new----
我们先来看第一个支持目标:用例。我们认为一个系统的架构必须能够支持其自身的设计意图。也就是说,如果某系统是一个购物车应用,那么该系统的架构就必须非常直观地支持这类应用可能会涉及的所有用例。事实上,这本来就是架构师们首先要关注的问题,也是架构设计过程中的首要工作。软件的架构必须为其用例提供支持。
在这种模式下,大部分组件可能还是依然运行在同一个地址空间内,通过彼此的函数调用通信。但有一些别的组件可能会运行在同一个处理器下的其他进程内,使用跨进程通信,或者通过 socket 或共享内存进行通信。这里最重要的是,这些组件的解耦产生出许多可独立部署的单元,例如 jar 文件、Gem 文件和 DLL 等。
服务层次:我们可以将组件间的依赖关系降低到数据结构级别’然后仅通过网络数据包来进行通信。这样系统的每个执行单元在源码层和二进制层都会是一个独立的个体,它们的变更不会影响其他地方(例如常见的服务或微服务就都是如此的)。
另一个解决方案(似乎也是目前最流行的方案)是,默认就采用服务层次的解耦。这种做法的问题主要在于它的成本很高,并且是在鼓励粗粒度的解耦。毕竟,无论微服务有多么“微”,其解耦的精细度都可能是不够的。
服务层次解耦的另一个问题是不仅系统资源成本高昂,而且研发成本更高。处理服务边界不仅非常耗费内存、处理器资源,而且更耗费人力。虽然内存和处理器越来越便宜,但是人力成本可一直都很高。
通常,我会倾向于将系统的解耦推行到某种一旦有需要就可以随时转变为服务的程度即可,让整个程序尽量长时间地保持单体结构,以便给未来留下可选项。
一个设计良好的架构应该允许一个系统从单体结构开始,以单一文件的形式部署,然后逐渐成长为一组相互独立的可部署单元,甚至是独立的服务或者微服务。最后还能随着情况的变化,允许系统逐渐回退到单体结构。
并且,一个设计良好的架构在上述过程中还应该能保护系统的大部分源码不受变更影响。对整个系统来说,解耦模式也应该是一个可选项。我们在进行大型部署时可以采用一种模式,而在进行小型部署时则可以釆用另一种模式。
软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。其中有一些边界是作项目初期——甚至在编写代码之前——就已经划分好,而其他的边界则是后来才划分的。在项目初期划分这些边界的目的是方便我们尽量将一些决策延后进行,并且确保未来这些决策不会对系统的核心业务逻辑产生干扰。
那么,怎样的决策会被认为是过早且不成熟的呢?答案是那些决策与系统的业务需求(也就是用例)无关。这部分决策包括我们要采用的框架、数据库、Web 服务器、工具库、依赖注入等。在一个设计良好的系统架构中,这些细节性的决策都应该是辅助性的,可以被推迟的。一个设计良好的系统架构不应该依赖于这些细节?而应该尽可能地推迟这些细节性的决策,并致力于将这种推迟所产生的影响降到最低。
在开发 FitNesse 的早期,我们在业务逻辑和数据库之间画了一条边界线。这条线有效地防止了业务逻辑对数据库产生依赖,它只能访问简单的数据访问方法。这个决策使我们将与数据库选型和实现的决策推迟了超过一年。同时我们还能用文件系统进行实验。使我们最终换了一个更好的解决方案。更重要的是,该架构在需求真的出现时,没有阻止任何人采用 MySQL,甚至没有为其制造任何障碍。
事实上,软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,而这些其他组件要么是可以去掉的,要么是有多种实现的
为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。
其实,这也是一种对依赖反转原则(DIP)和稳定抽象原则(SAP)的具体应用,依赖箭头应该由底层具体实现细节指向高层抽象的方向。
18.边界剖析
在图 18.2 中,控制流跨越边界的方向与之前是一样的,都是从左至右的。这里是高层组件 Client 通过 Service 接口调用了低层组件 Servicelmpl 上的函数 f()。但请读者注意,图 18.2 中所有的依赖关系却都是从右向左跨越边界的,方向是由低层组件指向高层组件的。同时,我们也应该注意到,这一次数据结构的定义是位于调用方这一侧的。
除单体结构以外,大部分系统都会同时采用多种边界划分策略。一个按照服务层次划分边界的系统也可能会在某一部分采用本地进程的边界划分模式。事实上,服务经常不过就是一系列互相作用的本地进程的某种外在形式。无论是服务还是本地进程,它们几乎肯定都是由一个或多个源码组件组成的单体结构,或者一组动态链接的可部署组件。
这也意味着一个系统中通常会同时包含高通信量、低延迟的本地架构边界和低通信量、高延迟的服务边界。
19.策略与层次
另外需要注意的是,图 19.1 中的数据流向和源码中的依赖关系并不总处于同一方向上。这也是软件架构设计工作的一部分。我们希望源码中的依赖关系与其数据流向脫钩,而与组件所在的层次挂钩。
综上所述,本章针对策略的讨论涉及单一职责原则(SRP)、开闭原则(OCP)、共同闭包原则(CCP)、依赖反转原则(DIP)、稳定依赖原则(SDP)以及稳定抽象原则(SAP)
20业务逻辑
关键业务逻辑和关键业务数据是紧密相关的,所以它们很适合被放在同一个对象中处理。我们将这种对象称为“业务实体(Entity)”。
有些读者可能会担心我在这里把业务实体解释成一个类。不是这样的,业务实体不一定非要用面向对象编程语言的类来实现。业务实体这个概念只要求我们将关键业务数据和关键业务逻辑绑定在一个独立的软件模块内。
那么,为什么业务实体属于高层概念,而用例属于低层概念呢?因为用例描述的是一个特定的应用情景,这样一来,用例必然会更靠近系统的输入和输出。而业务实体是一个可以适用于多个应用情景的一般化概念,相对地离系统的输入和输出更远。所以,用例依赖于业务实体,而业务实体并不依赖于用例。
可能有些读者会选择直接在数据结构中使用对业务实体对象的引用。毕竟,业务请求响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因、不同的速率发生变更。所以将它们以任何方式整合在一起都是对共同闭包原则(CCP)和单一职责原则(SRP)的违反。这样做的后果,往往会导致代码中出现很多分支判断语句和中间数据。
这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
21.SCREAMING ARCHITECTURE 尖叫的软件架构
在这里,再次推荐读者仔细阅读 Ivar Jacobson 关于软件架构设计的那本书:Object Oriented Software Engineering,请读者注意这本书的副标题 A Use Case Driven Approach(业务用例驱动的设计方式)。在这本书中,Jacobson 提出了一个观点:软件的系统架构应该为该系统的用例提供支持。这就像住宅和图书馆的建筑计划满篇都在非常明显地凸显这些建筑的用例一样,软件系统的架构设计图也应该非常明确地凸显该应用程序会有哪些用例。
架构设计不是(或者说不应该是)与框架相关的,这件事不应该是基于框架来完成的。对于我们来说,框架只是一个可用的工具和手段,而不是一个架构所规范的内容。如果我们的架构是基于框架来设计的,它就不能基于我们的用例来设计了。
一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。这就好像一个住宅建筑设计的首要目标应该是满足住宅的使用需求,而不是确保一定要用砖来构建这个房子。架构师应该花费更多的精力来确保该架构的设计在满足用例需求的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。
而且,良好的架构设计应该尽可能地允许用户推迟和延后决定釆用什么框架、数据库、Web 服务以及其他与环境相关的工具。框架应该是一个可选项,良好的架构设计应该允许用户在项目后期再决定是否采用 Rails、Spring、Hibernate、Tomcat、MySQL 这些工具。同时,良好的架构设计还应该让我们很容易改变这些决定。总之,良好的架构设计应该只关注用例,并能将它们与其他的周边因素隔离。
Web 究竟是不是一种架构?如果我们的系统需要以 Web 形式来交付,这是否意味着我们只能采用某种系统架构?当然不是!Web 只是一种交付手段——一种 IO 设备——这就是它在应用程序的架构设计中的角色。换句话说,应用程序采用 Web 方 式来交付只是一个实现细节,这不应该主导整个项目的结构设计。事实上,关于一个应用程序是否应该以 Web 形式来交付这件事,它本身就应该是一个被推迟和延后的决策。一个系统应该尽量保持它与交付方式之间的无关性。在不更改基础架构设计的情况下,我们应该可以将一个应用程序交付成命令行程序、Web 程序、富客户端程序、Web 服务程序等任何一种形式的程序。
我们一定要带着怀疑的态度审视每一个框架。是的,采用框架可能会很有帮助,但采用它们的成本呢?我们一定要懂得权衡如何使用一个框架,如何保护自己。无论如何,我们需要仔细考虑如何能保持对系统用例的关注,避免让框架主导我们的架构设计。
如果系统架构的所有设计都是围绕着用例来展开的,并且在使用框架的问题上保持谨慎的态度,那么我们就应该可以在不依赖任何框架的情况下针对这些用例进行单元测试。另外,我们在运行测试的时候不应该运行 Web 服务,也不应该需要连接数据库。我们测试的应该只是一个简单的业务实体对象,没有任何与框架、数据库相关的依赖关系。总而言之,我们应该通过用例对象来调度业务实体对象,确保所有的测试都不需要依赖框架。
一个系统的架构应该着重于展示系统本身的设计,而并非该系统所使用的框架。如果我们要构建的是一个医疗系统,新来的程序员第一次看到其源码时就应该知道这是一个医疗系统。新来的程序员应该先了解该系统的用例,而非系统的交付方式。
22.THE CLEAN ARCHITECTURE 整洁架构
六边形架构 (Hexagonal Architecture)(也称为端口与适配器架构,Ports and Adpaters): 该架构由 Alistair Cockburn 首先提出。Steve Freeman 和 Nat Pryce 在他们合写的著作 Growing Object oriented software with Tests 一书中对该架构做了隆重的推荐。
DCI 架构:由 James Coplien 和 Trygve Reenskaug 首先提出。
BCE 架构:由 Ivar Jacobson 在他的 Object Oriented Software Engineer: A Use-Case Driven Approach 一书中首先提出。
虽然这些架构在细节上各有不同,但总体来说是非常相似的。它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。
按照这些架构设计出来的系统,通常都具有以下特点:
独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。框架可以被当成工具来使用,但不需要让系统来适应框架。
可被测试:这些系统的业务逻辑可以脱离 UI、数据库、Web 服务以及其他的外部元素来进行测试。
独立于 UI:这些系统的 UI 变更起来很容易,不需要修改其他的系统部分。例如,我们可以在不修改业务逻辑的前提下将一个系统的 UI 由 Web 界面替换成命令行界面。
独立于数据库:我们可以轻易将这些系统使用的 Oracle 、SQL Server 替换成 Mongo、BigTable、CouchDB 之类的数据库。因为业务逻辑与数据库之间已经完成了解耦。
独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。
下面我们要通过图 22.1 将上述所有架构的设计理念综合成为一个独立的理念。
The clean architecture
图 22.1 中的同心圆分别代表了软件系统中的不同层次,通常越靠近中心,其所在的软件层次就越高。基本上,外层圆代表的是机制,内层圆代表的是策略。
当然这其中有一条贯穿整个架构设计的规则,即它的依赖关系规则:
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
换句话说,就是任何属于内层圆中的代码都不应该牵涉外层圆中的代码,尤其是内层圆中的代码不应该引用外层圆中代码所声明的名字,包括函数、类、变量以及一切其他有命名的软件实体。
同样的道理,外层圆中使用的数据格式也不应该被内层圆中的代码所使用,尤其是当数据格式是由外层圆的框架所生成。总之,我们不应该让外层圆中发生的任何变更影响到内层圆的代码。
图 22.1 中所显示的同心圆只是为了说明架构的结构,真正的架构很可能会超过四层。并没有某个规则约定一个系统的架构有且只能有四层。然而,这其中的依赖关系原则是不变的。也就是说,源码层面的依赖关系一定要指向同心圆的内侧。层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多。最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。
这里,我们通常釆用依赖反转原则(DIP)来解决这种相反性。例如,在 Java 这一类的语言中,可以通过调整代码中的接口和继承关系,利用源码中的依赖关系来限制控制流只能在正确的地方跨越架构边界。
我们可以采用这种方式跨越系统中所有的架构边界。利用动态多态技术,我们将源码中的依赖关系与控制流的方向进行反转。不管控制流原本的方向如何,我们都可以让它遵守架构的依赖关系规则。
23. PRESENTERS AND HUMBLE OBJECTS 展示器和谦卑对象
在每个系统架构的边界处,都有可能发现谦卑对象模式的存在。因为跨边界的通信肯定需要用到某种简单的数据结构,而边界会自然而然地将系统分割成难以测试的部分与容易测试的部分,所以通过在系统的边界处运用谦卑对象模式,我们可以大幅地提高整个系统的可测试性。