Over the last several decades we’ve seen a whole range of ideas regarding the architecture of systems. These include:
在过去的几十年中,我们曾见证过一系列关于系统架构的想法被提出,列举如下。
- Hexagonal Architecture (also known as Ports and Adapters), developed by Alistair Cockburn, and adopted by Steve Freeman and Nat Pryce in their wonderful book Growing Object Oriented Software with Tests
- DCI from James Coplien and Trygve Reenskaug
- BCE, introduced by Ivar Jacobson from his book Object Oriented Software Engineering: A Use-Case Driven Approach
- 六边形架构 (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 一书中首先提出。
Although these architectures all vary somewhat in their details, they are very similar. They all have the same objective, which is the separation of concerns. They all achieve this separation by dividing the software into layers. Each has at least one layer for business rules, and another layer for user and system interfaces.
虽然这些架构在细节上各有不同,但总体来说是非常相似的。它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。
Each of these architectures produces systems that have the following characteristics:
按照这些架构设计出来的系统,通常都具有以下特点。
- Independent of frameworks. The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than forcing you to cram your system into their limited constraints.
- Testable. The business rules can be tested without the UI, database, web server, or any other external element.
- Independent of the UI. The UI can change easily, without changing the rest of the system. A web UI could be replaced with a console UI, for example, without changing the business rules.
- Independent of the database. You can swap out Oracle or SQL Server for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
- Independent of any external agency. In fact, your business rules don’t know anything at all about the interfaces to the outside world.
- 独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。框架可以被当成工具来使用,但不需要让系统来适应框架。
- 可被测试:这些系统的业务逻辑可以脱离 UI、数据库、Web 服务以及其他的外部元素来进行测试。
- 独立于 UI:这些系统的 UI 变更起来很容易,不需要修改其他的系统部分。例如,我们可以在不修改业务逻辑的前提下将一个系统的 UI 由 Web 界面替换成命令行界面。
- 独立于数据库:我们可以轻易将这些系统使用的 Oracle 、SQL Server 替换成 Mongo、BigTable、CouchDB 之类的数据库。因为业务逻辑与数据库之间已经完成了解耦。
- 独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。
The diagram in Figure 22.1 is an attempt at integrating all these architectures into a single actionable idea.
下面我们要通过图 22.1 将上述所有架构的设计理念综合成为一个独立的理念。
The clean architecture
The concentric circles in Figure 22.1 represent different areas of software. In general, the further in you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.
图 22.1 中的同心圆分别代表了软件系统中的不同层次,通常越靠近中心,其所在的软件层次就越高。基本上,外层圆代表的是机制,内层圆代表的是策略。
The overriding rule that makes this architecture work is the Dependency Rule:
当然这其中有一条贯穿整个架构设计的规则,即它的依赖关系规则:
Source code dependencies must point only inward, toward higher-level policies.
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes functions, classes, variables, or any other named software entity.
换句话说,就是任何属于内层圆中的代码都不应该牵涉外层圆中的代码,尤其是内层圆中的代码不应该引用外层圆中代码所声明的名字,包括函数、类、变量以及一切其他有命名的软件实体。
By the same token, data formats declared in an outer circle should not be used by an inner circle, especially if those formats are generated by a framework in an outer circle. We don’t want anything in an outer circle to impact the inner circles.
同样的道理,外层圆中使用的数据格式也不应该被内层圆中的代码所使用,尤其是当数据格式是由外层圆的框架所生成。总之,我们不应该让外层圆中发生的任何变更影响到内层圆的代码。
Entities encapsulate enterprise-wide Critical Business Rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities can be used by many different applications in the enterprise.
业务实体这一层中封装的是整个系统的关键业务逻辑,一个业务实体既可以是一个带有方法的对象,也可以是一组数据结构和函数的集合。无论如何,只要它能被系统中的其他不同应用复用就可以。
If you don’t have an enterprise and are writing just a single application, then these entities are the business objects of the application. They encapsulate the most general and high-level rules. They are the least likely to change when something external changes. For example, you would not expect these objects to be affected by a change to page navigation or security. No operational change to any particular application should affect the entity layer.
如果我们在写的不是一个大型系统,而是一个单一应用的话,那么我们的业务实体就是该应用的业务对象。这些对象封装了该应用中最通用、最高层的业务逻辑,它们应该属于系统中最不容易受外界影响而变动的部分。例如,一个针对页面导航方式或者安全问题的修改不应该触及这些对象,一个针对应用在运行时的行为所做的变更也不应该影响业务实体。
The software in the use cases layer contains application-specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their Critical Business Rules to achieve the goals of the use case.
软件的用例层中通常包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例。这些用例引导了数据在业务实体之间的流入/流出,并指挥着业务实体利用其中的关键业务逻辑来实现用例的设计目标。
We do not expect changes in this layer to affect the entities. We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. The use cases layer is isolated from such concerns.
我们既不希望在这一层所发生的变更影响业务实体,同时也不希望这一层受外部因素(譬如数据库、UI、常见框架)的影响。用例层应该与它们都保持隔离。
We do, however, expect that changes to the operation of the application will affect the use cases and, therefore, the software in this layer. If the details of a use case change, then some code in this layer will certainly be affected.
然而,我们知道应用行为的变化会影响用例本身,因此一定会影响用例层的代码。因为如果一个用例的细节发生了变化,这一层中的某些代码自然要受到影响。
The software in the interface adapters layer is a set of adapters that convert data from the format most convenient for the use cases and entities, to the format most convenient for some external agency such as the database or the web. It is this layer, for example, that will wholly contain the MVC architecture of a GUI. The presenters, views, and controllers all belong in the interface adapters layer. The models are likely just data structures that are passed from the controllers to the use cases, and then back from the use cases to the presenters and views.
软件的接口适配器层中通常是一组数据转换器,它们负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及 Web)最方便操作的格式。例如,这一层中应该包含整个 GUI MVC 框架。展示器、视图、控制器都应该属于接口适配器层。而模型部分则应该由控制器传递给用例,再由用例传回展示器和视图。
Similarly, data is converted, in this layer, from the form most convenient for entities and use cases, to the form most convenient for whatever persistence framework is being used (i.e., the database). No code inward of this circle should know anything at all about the database. If the database is a SQL database, then all SQL should be restricted to this layer—and in particular to the parts of this layer that have to do with the database.
同样的,这一层的代码也会负责将数据从对业务实体与用例而言最方便操作的格式,转化为对所采用的持久性框架(譬如数据库)最方便的格式。总之,在从该层再向内的同心圆中,其代码就不应该依赖任何数据库了。譬如说,如果我们采用的是 SQL 数据库,那么所有的 SQL 语句都应该被限制在这一层的代码中——而且是仅限于那些需要操作数据库的代码。
Also in this layer is any other adapter necessary to convert data from some external form, such as an external service, to the internal form used by the use cases and entities.
当然,这一层的代码也会负责将来自外部服务的数据转换成系统内用例与业务实体所需的格式。
The outermost layer of the model in Figure 22.1 is generally composed of frameworks and tools such as the database and the web framework. Generally you don’t write much code in this layer, other than glue code that communicates to the next circle inward.
图 22.1 中最外层的模型层一般是由工具、数据库、Web 框架等组成的。在这一层中,我们通常只需要编写一些与内层沟通的黏合性代码。
The frameworks and drivers layer is where all the details go. The web is a detail. The database is a detail. We keep these things on the outside where they can do little harm.
框架与驱动程序层中包含了所有的实现细节。Web 是一个实现细节,数据库也是一个实现细节。我们将这些细节放在最外层,这样它们就很难影响到其他层了。
The circles in Figure 22.1 are intended to be schematic: You may find that you need more than just these four. There’s no rule that says you must always have just these four. However, the Dependency Rule always applies. Source code dependencies always point inward. As you move inward, the level of abstraction and policy increases. The outermost circle consists of low-level concrete details. As you move inward, the software grows more abstract and encapsulates higher-level policies. The innermost circle is the most general and highest level.
图 22.1 中所显示的同心圆只是为了说明架构的结构,真正的架构很可能会超过四层。并没有某个规则约定一个系统的架构有且只能有四层。然而,这其中的依赖关系原则是不变的。也就是说,源码层面的依赖关系一定要指向同心圆的内侧。层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多。最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。
At the lower right of the diagram in Figure 22.1 is an example of how we cross the circle boundaries. It shows the controllers and presenters communicating with the use cases in the next layer. Note the flow of control: It begins in the controller, moves through the use case, and then winds up executing in the presenter. Note also the source code dependencies: Each points inward toward the use cases.
在图 22.1 的右下侧,我们示范的是在架构中跨边界的情况。具体来说就是控制器、展示器与下一层的用例之间的通信过程。请注意这里控制流的方向:它从控制器开始,穿过用例,最后执行展示器的代码。但同时我们也该注意到,源码中的依赖方向却都是向内指向用例的。
We usually resolve this apparent contradiction by using the Dependency Inversion Principle. In a language like Java, for example, we would arrange interfaces and inheritance relationships such that the source code dependencies oppose the flow of control at just the right points across the boundary.
这里,我们通常釆用依赖反转原则(DIP)来解决这种相反性。例如,在 Java 这一类的语言中,可以通过调整代码中的接口和继承关系,利用源码中的依赖关系来限制控制流只能在正确的地方跨越架构边界。
For example, suppose the use case needs to call the presenter. This call must not be direct because that would violate the Dependency Rule: No name in an outer circle can be mentioned by an inner circle. So we have the use case call an interface (shown in Figure 22.1 as “use case output port”) in the inner circle, and have the presenter in the outer circle implement it.
假设某些用例代码需要调用展示器,这里一定不能直接调用,因为这样做会违反依赖关系原则:内层圆中的代码不能引用其外层的声明。我们需要让业务逻辑代码调用一个内层接口(图 22.1 中的“用例输出端”),并让展示器来负责实现这个接口。
The same technique is used to cross all the boundaries in the architectures. We take advantage of dynamic polymorphism to create source code dependencies that oppose the flow of control so that we can conform to the Dependency Rule, no matter which direction the flow of control travels.
我们可以采用这种方式跨越系统中所有的架构边界。利用动态多态技术,我们将源码中的依赖关系与控制流的方向进行反转。不管控制流原本的方向如何,我们都可以让它遵守架构的依赖关系规则。
Typically the data that crosses the boundaries consists of simple data structures. You can use basic structs or simple data transfer objects if you like. Or the data can simply be arguments in function calls. Or you can pack it into a hashmap, or construct it into an object. The important thing is that isolated, simple data structures are passed across the boundaries. We don’t want to cheat and pass Entity objects or database rows. We don’t want the data structures to have any kind of dependency that violates the Dependency Rule.
一般来说,会跨越边界的数据在数据结构上都是很简单的。如果可以的话,我们会尽量采用一些基本的结构体或简单的可传输数据对象。或者直接通过函数调用的参数来传递数据。另外,我们也可以将数据放入哈希表,或整合成某种对象。这里最重要的是这个跨边界传输的对象应该有一个独立、简单的数据结构。总之,不要投机取巧直接传递业务实体或数据库记录对象。同时,这些传递的数据结构中也不应该存在违反依赖规则的依赖关系。
For example, many database frameworks return a convenient data format in response to a query. We might call this a “row structure.” We don’t want to pass that row structure inward across a boundary. Doing so would violate the Dependency Rule because it would force an inner circle to know something about an outer circle.
例如,很多数据库框架会返回一个便于查询的结果对象,我们称之为“行结构体”。这个结构体不应该跨边界向架构的内层传递。因为这等于让内层的代码引用外层代码,违反依赖规则。
Thus, when we pass data across a boundary, it is always in the form that is most convenient for the inner circle.
因此,当我们进行跨边界传输时,一定要采用内层最方便使用的形式。
The diagram in Figure 22.2 shows a typical scenario for a web-based Java system using a database. The web server gathers input data from the user and hands it to the Controller on the upper left. The Controller packages that data into a plain old Java object and passes this object through the InputBoundary to the UseCaseInteractor. The UseCaseInteractor interprets that data and uses it to control the dance of the Entities. It also uses the DataAccessInterface to bring the data used by those Entities into memory from the Database. Upon completion, the UseCaseInteractor gathers data from the Entities and constructs the OutputData as another plain old Java object. The OutputData is then passed through the OutputBoundary interface to the Presenter.
接下来,我们将会在图 22.2 中看到一个基于 Web 的使用数据库的 Java 系统。在该系统中,Web 服务器会从用户那里收集信息,并将它们交给左上角的 controller。然后,controller 将这些信息包装成一个简单的 Java 对象,并让该对象穿越 InputBoundary 被传递到 UseCaseInteractor。接下来,我们会让 UseCaseInteractor 解析数据,并通过它来控制与 Entities 的交互。同时,我们还会用 DataAccessInterface 将 Entities 需要用到的数据从 Database 加载到内存中。随后,UseCaselnteractor 会负责从 Entities 收集数据,并将 OutputData 组装成另一个简单的 Java 对象。最后,OutputData 会穿越 OutputBoundary 被传递给 Presenter。
A typical scenario for a web-based Java system utilizing a database
The job of the Presenter is to repackage the OutputData into viewable form as the ViewModel, which is yet another plain old Java object. The ViewModel contains mostly Strings and flags that the View uses to display the data. Whereas the OutputData may contain Date objects, the Presenter will load the ViewModel with corresponding Strings already formatted properly for the user. The same is true of Currency objects or any other business-related data. Button and MenuItem names are placed in the ViewModel, as are flags that tell the View whether those Buttons and MenuItems should be gray.
接下来,Presenter 的任务是将 OutPutData 重新打包成可展示的 ViewModel,这也是一个简单的 Java 对象。ViewModel 中基本上只包含字符串和一些 View 都会用到的开关数据。同时,OutputData 中可能会包含一些 D ate 对象,Presenter 会将其格式化成可对用户展示的字符串,并将其填充到 ViewModel 中。同理,Currency 对象和其他业务相关的数据也会经历类似的操作。如你所见,Button 和 Menuitems 的命名定义位于 ViewModel 中,并且其中还包括了用于告知 View 层 Button 和 Menuitems 是否可用的开关数据。
This leaves the View with almost nothing to do other than to move the data from the ViewModel into the HTML page.
我们可以看出,View 除了将 ViewModel 中的数据转换成 HTML 格式之外,并没有其他功能。
Note the directions of the dependencies. All dependencies cross the boundary lines pointing inward, following the Dependency Rule.
最后,读者必须注意一下这里的依赖关系方向。所有跨边界的依赖线都是指向内的,这很好地遵守了架构的依赖关系规则。
Conforming to these simple rules is not difficult, and it will save you a lot of headaches going forward. By separating the software into layers and conforming to the Dependency Rule, you will create a system that is intrinsically testable, with all the benefits that implies. When any of the external parts of the system become obsolete, such as the database, or the web framework, you can replace those obsolete elements with a minimum of fuss.
如你所见,遵守上面这些简单的规范并不困难,这样做能在未来避免各种令人头疼的问题。通过将系统划分层次,并确保这些层次遵守依赖关系规则,就可以构建出一个天生可测试的系统,这其中的好处是不言而喻的。而且,当系统外层的这些数据库或 Web 框架过时的时候,我们还可以很轻松地替换它们。