六边形架构[双语]

时间:2022-08-31 13:49:50

原文链接:http://alistair.cockburn.us/Hexagonal+architecture
作者:Alistair Cockburn
译者:钟敬

【译者注】
1. 转载请注明出处、作者及译者。
2. “六边形架构”是 Cockburn大牛在2005年 提出的。该架构提供了一种将业务逻辑和具体输入输出技术分离的模式。我在《实现领域驱动设计》一书中看到该书作者将六边形架构应用于DDD(领域驱动设计),觉得很有启发,因此尝试将该架构的原文译出,供大家参考。本文中的业务逻辑相当于DDD中的domain和application两层,用户界面和数据库访问等相当于DDD中的Interface和Infrastructure。
3. 本文采用双语对照,因此在一些地方大胆地进行了意译,以求晓畅。有疑问的小伙伴可以直接参阅原文。
4. 原作中最后有一些读者的讨论,这里没有译出,有兴趣的同学可自行参考原文。
5. 兄弟水平有限,错漏在所难免,欢迎多提宝贵意见。

【以下为译文】


创建一个没有用户界面和数据库即可运行的程序,这样,你就可以对其进行自动化回归测试;在没有数据库时也可进行开发;同时,无需用户参与就可以将多个应用程序连接起来。

Create your application to work without either a UI or a database so you can run automated regression-tests against the application, work when the database becomes unavailable, and link applications together without any user involvement.

六边形架构[双语]

模式: 端口和适配器(“对象结构模式”)

The Pattern: Ports and Adapters (“Object Structural”)

别名:“端口和适配器”

Alternative name: “Ports & Adapters”

别名:“六边形架构”

Alternative name: “Hexagonal Architecture”

意图 Intent

让应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动;并且,可以在与实际运行的设备和数据库相隔离的情况下开发和测试。

Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.

当事件从外界经过一个“端口”传入时,相应的“适配器”会将其转化成合适的过程调用或消息,然后转发给应用程序,该适配器是由具体的技术决定的,而应用程序则对该技术一无所知。输出信息时,应用程序会将信息通过一个端口输出到适配器,适配器再针对信息接收者的具体技术(人或自动化程序)将其转化成合适的信号。应用程序在输入输出时,只和相应的适配器进行语义完整的交互,而并不知道适配器另一端的具体技术是什么。

As events arrive from the outside world at a port, a technology-specific adapter converts it into a usable procedure call or message and passes it to the application. The application is blissfully ignorant of the nature of the input device. When the application has something to send out, it sends it out through a port to an adapter, which creates the appropriate signals needed by the receiving technology (human or automated). The application has a semantically sound interaction with the adapters on all sides of it, without actually knowing the nature of the things on the other side of the adapters.

六边形架构[双语]
图1

动机 Motivation

近年来,业务逻辑渗入到用户界面代码中,已经成为一个老大难问题。这带来了三方面的后果:
One of the great bugaboos of software applications over the years has been infiltration of business logic into the user interface code. The problem this causes is threefold:

  • 首先,我们无法干净地对系统进行自动化测试。这是因为有一些需要被测试的逻辑依赖于可视化界面的细节,比如说字段长度和按钮位置,而这些细节常常发生变化。
    First, the system can’t neatly be tested with automated test suites because part of the logic needing to be tested is dependent on oft-changing visual details such as field size and button placement;

  • 由于同样的原因,我们不可能将一个人工操作的系统切换为批处理系统。
    For the exact same reason, it becomes impossible to shift from a human-driven use of the system to a batch-run system;

  • 仍然基于这个原因, 当我们想让一个程序被另一个程序驱动时,也会很困难甚至根本不可能。
    For still the same reason, it becomes difficult or impossible to allow the program to be driven by another program when that becomes attractive.

为了解决上述问题,很多组织尝试在系统架构中引入一个新层,并承诺这一次无论如何不会在新层中混入业务逻辑了。然而,由于缺乏检测这一规则的手段,几年以后,该组织将会发现新层又被业务逻辑搞得乱七八糟,老问题又回来了。

The attempted solution, repeated in many organizations, is to create a new layer in the architecture, with the promise that this time, really and truly, no business logic will be put into the new layer. However, having no mechanism to detect when a violation of that promise occurs, the organization finds a few years later that the new layer is cluttered with business logic and the old problem has reappeared。

现在我们设想应用程序的每一个功能都通过API(应用程序接口)或函数调用的方式提供。这样,测试或QA部门就可以通过运行自动化测试脚本,来检测新代码是否会破坏之前已经正常工作的功能。业务专家也可以在GUI细节确定之前就编写自动测试案例,作为程序员们检测是否正确完成工作的依据,同时,这也将成为测试部门所运行的测试的一部分。当发布应用程序时,可以采用这种“无头”的方式,也就是说只有API是可用的,其它程序则通过API调用这个程序的功能。这简化了包含多个应用程序的复杂系统的整体设计;同时,也让B2B(business-to-business)服务程序间可以在互联网上相互调用,而不需人工操作介入。最后,自动化功能回归测试可以检测到在展现层中混入业务逻辑的违规行为。组织利用这一技术可以发现和纠正这种逻辑泄露。

Imagine now that “every” piece of functionality the application offers were available through an API (application programmed interface) or function call. In this situation, the test or QA department can run automated test scripts against the application to detect when any new coding breaks a previously working function. The business experts can create automated test cases, before the GUI details are finalized, that tells the programmers when they have done their work correctly (and these tests become the ones run by the test department). The application can be deployed in “headless” mode, so only the API is available, and other programs can make use of its functionality — this simplifies the overall design of complex application suites and also permits business-to-business service applications to use each other without human intervention over the web. Finally, the automated function regression tests detect any violation of the promise to keep business logic out of the presentation layer. The organization can detect, and then correct, the logic leak.

与此类似,还有一个令人感兴趣的问题:在应用程序的“另一侧”,逻辑绑定在了外部数据库或其它服务上。因此,当数据库服务器关机,进行重大的修改甚至更换时,程序员们将无法工作,原因是他们的工作依赖于数据库的正常运行。这造成了延迟成本,也常常给人们带来糟糕的体验。

An interesting similar problem exists on what is normally considered “the other side” of the application, where the application logic gets tied to an external database or other service. When the database server goes down or undergoes significant rework or replacement, the programmers can’t work because their work is tied to the presence of the database. This causes delay costs and often bad feelings between the people.

上述两个问题间的关系看起来并不明显,但在后文的“解决方案”中,我们可以看到它们之间存在的一种“对称性”。

It is not obvious that the two problems are related, but there is a symmetry between them that shows up in the nature of the solution.

解决方案 Nature of the Solution

无论是用户接口还是数据库服务器方面的问题,实际上都是由设计和编码过程中的同一个错误引起的——将“业务逻辑”和“与外部实体间的交互”纠缠在了一起。我们关注的是系统“内部”和“外部”间的区别,而不是“左边”和“右边”的区别。应遵循以下规则:与“内部”相关的代码不能泄露到“外部”去。

Both the user-side and the server-side problems actually are caused by the same error in design and programming — the entanglement between the business logic and the interaction with external entities. The asymmetry to exploit is not that between “left” and “right” sides of the application but between “inside” and “outside” of the application. The rule to obey is that code pertaining to the “inside” part should not leak into the “outside” part.

如果暂时不去考虑应用程序“左右”或“上下”层次间的不区别,我们就会看到应用程序是通过“端口”和外界沟通的。“端口”一词应该会让人想到操作系统中的“端口”,任何遵循相应协议的设备都可以插在上面使用。同样,对于电气设备上的端口,任何符合相关机械和电气协议的设备也都可以插入。

Removing any left-right or up-down asymmetry for a moment, we see that the application communicates over “ports” to external agencies. The word “port” is supposed to evoke thoughts of “ports” in an operating system, where any device that adheres to the protocols of a port can be plugged into it; and “ports” on electronics gadgets, where again, any device that fits the mechanical and electrical protocols can be plugged in.

端口的协议是为了两个设备之间能够进行会话而设计的。
The protocol for a port is given by the purpose of the conversation between the two devices.

协议是以API(application program interface)的形式实现的。
The protocol takes the form of an application program interface (API).

每个外部设备都有一个对应的”适配器“,用于将API的定义转化为该设备所需的信号,反之亦然。例如,GUI(图形用户界面)就是一个适配器,它将人的操作映射到端口的API。适用于该端口的适配器还包括自动化测试装置(如FIT或Fitness),批处理程序,以及任何需要跨企业或网络进行应用程序间通信的代码。

For each external device there is an “adapter” that converts the API definition to the signals needed by that device and vice versa. A graphical user interface or GUI is an example of an adapter that maps the movements of a person to the API of the port. Other adapters that fit the same port are automated test harnesses such as FIT or Fitnesse, batch drivers, and any code needed for communication between applications across the enterprise or net.

在“另一侧”,应用程序和一个外部实体通信以获取数据。实现这一功能的典型协议就是数据库协议。从应用程序的角度来看,如果数据存储从SQL数据库换成普通文件或任何其他类型的数据库,通过API进行的会话不应发生改变。因此,适用于这一端口的适配器可以包括SQL适配器、普通文件适配器、以及,尤为重要的是,“mock( 模拟)”数据库适配器,它位于内存中,根本不依赖于实际的数据库。
【译者注:由于“mock”在测试驱动开发中已经是一个专有名词,译作“模拟的”反而显得不自然,因此在后面的译文中都直接使用“mock”原文。】

On another side of the application, the application communicates with an external entity to get data. The protocol is typically a database protocol. From the application’s perspective, if the database is moved from a SQL database to a flat file or any other kind of database, the conversation across the API should not change. Additional adapters for the same port thus include an SQL adapter, a flat file adapter, and most importantly, an adapter to a “mock” database, one that sits in memory and doesn’t depend on the presence of the real database at all.

很多应用程序只有两个端口:用户端会话和数据库端会话。在这种情况下,这两个端口表面上并不存在任何共性,从而在开发应用程序时,看起来可以自然地采用一维的层次架构,如三层、四层或五层。这样的架构设计有两个问题。首先,最遭的是,人们常常不能认真地遵循层次之间的分界。他们使应用逻辑在不同的层次间泄露,从而带来了上面提到的各种问题。其次,应用程序可能需要两个以上的端口,这时,一维的架构设计就不适用了。

Many applications have only two ports: the user-side dialog and the database-side dialog. This gives them an asymmetric appearance, which makes it seem natural to build the application in a one-dimensional, three-, four-, or five-layer stacked architecture.
There are two problems with these drawings. First and worst, people tend not to take the “lines” in the layered drawing seriously. They let the application logic leak across the layer boundaries, causing the problems mentioned above. Secondly, there may be more than two ports to the application, so that the architecture does not fit into the one-dimensional layer drawing.

“六边形(端口和适配器)架构”解决了上述问题。这是由于我们意识到,无论是用户端还是数据库端的交互,都可看做内部的应用程序通过一些端口和外部进行通信。因此,在应用程序外部的实体可以被系统化的处理。

The hexagonal, or ports and adapters, architecture solves these problems by noting the symmetry in the situation: there is an application on the inside communicating over some number of ports with things on the outside. The items outside the application can be dealt with symmetrically.

采用“六边形”是为了可视化地强调:
(a) 系统内部及外部的内在区别;不同端口间本质上的相似性;摆脱一维层次化结构的束缚,以及
(b) 呈现一定数量的不同端口,两个、三个、或四个(四个是我目前遇到过的最大数目了)。

The hexagon is intended to visually highlight
(a) the inside-outside asymmetry and the similar nature of ports, to get away from the one-dimensional layered picture and all that evokes, and
(b)the presence of a defined number of different ports – two, three, or four (four is most I have encountered to date).

之所以采用六边形,并不是因为“六”这个数字有多重要,而是为了让人们画图时有足够的空间摆放所需的端口和适配器,而不会受到一维层次结构的限制。术语“六边形架构”正是由这一图形效果而得名的。

The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as they need, not being constrained by a one-dimensional layered drawing. The term “hexagonal architecture” comes from this visual effect.

而术语“端口和适配器”则是由该架构的“目的”而命名的。一个“端口”确定了一个有目的的会话。一般情况下,一个端口会有多个适配器,以便将不同的技术实现插入这个端口。一般来说,这些适配器可以包括电话应答机,人工语音设备,按键式电话,图形用户界面,测试装置,批处理程序,http接口,跨程序接口,mock(内存中)数据库,以及真实数据库(在开发、测试和实际生产环境中可能采用不同的数据库)。

The term “port and adapters” picks up the “purposes” of the parts of the drawing. A port identifies a purposeful conversation. There will typically be multiple adapters for any one port, for various technologies that may plug into that port. Typically, these might include a phone answering machine, a human voice, a touch-tone phone, a graphical human interface, a test harness, a batch driver, an http interface, a direct program-to-program interface, a mock (in-memory) database, a real database (perhaps different databases for development, test, and real use).

在下文的“注释”一节中,将再次提到“左右”间的区别。不过,这个模式的主要目的是为了强调“内外”部的不同。所以在此我们姑且简单地认为,对于应用程序来说,所有的外部实体都具有同样的性质。

In the Application Notes, the left-right asymmetry will be brought up again. However, the primary purpose of this pattern is to focus on the inside-outside asymmetry, pretending briefly that all external items are identical from the perspective of the application.

结构 Structure

六边形架构[双语]
图2

图2展示了一个应用程序,它具有两个有效端口,每个端口有若干个适配器。这两个端口分别是应用控制端和数据获取端。该图显示了该应用程序可以以相同的方式被不同的技术驱动,如自动化回归测试、人工用户、远程http应用、以及另一个本地应用程序。在数据端,通过配置,应用程序可以用内存Oracle数据库(或者说mock数据库)代替外部数据库来运行,从而实现和外部数据库的解耦;同样,应用程序也可以在测试数据库和运行时数据库间切换。应用程序的功能规格说明书(可能采用用例的形式)是针对图中内部的六边形所代表的接口书写的,而与任何外部技术无关。

Figure 2 shows an application having two active ports and several adapters for each port. The two ports are the application-controlling side and the data-retrieval side. This drawing shows that the application can be equally driven by an automated, system-level regression test suite, by a human user, by a remote http application, or by another local application. On the data side, the application can be configured to run decoupled from external databases using an in-memory oracle, or “mock”, database replacement; or it can run against the test- or run-time database. The functional specification of the application, perhaps in use cases, is made against the inner hexagon’s interface and not against any one of the external technologies that might be used.

六边形架构[双语]
图3

图3展示了同一个应用程序对应到三层架构时的样子。为简化起见,只为每个端口画出了两个适配器。该图的目的是为了说明不同的适配器是怎样应用于架构的顶层(展示层)和底层(持久层)的;以及在系统开发过程中不同适配器的使用顺序。带数字的箭头说明了团队开发和使用该应用程序的一种可能的顺序:

Figure 3 shows the same application mapped to a three-layer architectural drawing. To simplify the drawing only two adapters are shown for each port. This drawing is intended to show how multiple adapters fit in the top and bottom layers, and the sequence in which the various adapters are used during system development. The numbered arrows show the order in which a team might develop and use the application:

  1. 用FIT测试装置驱动应用程序,并用mock(内存中)数据库来代替实际数据库;
    With a FIT test harness driving the application and using the mock (in-memory) database substituting for the real database;

  2. 为应用程序加上GUI,仍使用mock数据库;
    Adding a GUI to the application, still running off the mock database;

  3. 在集成测试阶段,用自动测试脚本(例如,通过Cruise Control触发)驱动应用程序,并使用包含测试数据的真实数据库。
    In integration testing, with automated test scripts (e.g., from Cruise Control) driving the application against a real database containing test data;

  4. 在实际使用中,由人来操作应用程序,并使用生产数据库。
    In real use, with a person using the application to access a live database.

示例代码 Sample Code

我们在FIT的文档中幸运地找到了这个最简单的应用程序,用于演示端口和适配器模式。这是一个简单的折扣计算程序:

The simplest application that demonstrates the ports & adapters fortunately comes with the FIT documentation. It is a simple discount computing application:

discount(amount) = amount * rate(amount);

在我们的应用中,amount(金额)来自用户而rate(折扣率)来自数据库。从而有两个端口。我们按以下步骤实现:
In our adaptation, the amount will come from the user and the rate will come from a database, so there will be two ports. We implement them in stages:

使用自动化测试来驱动,但用一个常量而不是mock数据库来存储rate,
然后编写GUI,
接着使用mock数据库,它可进一步切换到真实数据库。
With tests but with a constant rate instead of a mock database,
then with the GUI,
then with a mock database that can be swapped out for a real database.

感谢IHC的Gyan Sharma为这个例子提供了代码。
Thanks to Gyan Sharma at IHC for providing the code for this example.

步骤1: 以FIT 驱动应用程序,以常量代替mock数据库
Stage 1: FIT App constant-as-mock-database

首先,我们用HTML创建测试案例(参见FIT文档):
First we create the test cases as an HTML table (see the FIT documentation for this):

TestDiscounter
amount discount()
100 5
200 10

注意,上表中的列名将转化成程序中的类名和函数名。FIT有办法消除那些程序员才能理解的表达方式,不过本文中为了简单起见,并没有这么做。
Note that the column names will become class and function names in our program. FIT contains ways to get rid of this “programmerese”, but for this article it is easier just to leave them in.

有了测试数据,我们来创建用户端的适配器,这里采用了FIT自带的ColumnFixture:

Knowing what the test data will be, we create the user-side adapter, the ColumnFixture that comes with FIT as shipped:

import fit.ColumnFixture; 
public class TestDiscounter extends ColumnFixture { 
   private Discounter app = new Discounter(); 
   public double amount;
   public double discount() 
   { return app.discount(amount); } 
}

这就是适配器的全部代码。目前,这个测试是用命令行来运行的(命令行的路径可参考FIT文档)。我们使用的命令行如下:

That’s actually all there is to the adapter. So far, the tests run from the command line (see the FIT book for the path you’ll need). We used this one:

set FIT_HOME=/FIT/FitLibraryForFit15Feb2005
java -cp %FIT_HOME%/lib/javaFit1.1b.jar;%FIT_HOME%/dist/fitLibraryForFit.jar;src;bin
fit.FileRunner test/Discounter.html TestDiscount_Output.html

FIT将会生成一个输出文件,以颜色标明哪些测试案例成功了(或失败了,万一在上面的过程中有什么笔误的话)。

FIT produces an output file with colors showing us what passed (or failed, in case we made a typo somewhere along the way).

现在,上面的代码就可以提交到版本库,挂到Cruise Control或你自己的自动构建机制,以包含在“构建-测试”系统中了。

At this point the code is ready to check in, hook into Cruise Control or your automated build machine, and include in the build-and-test suite.

步骤2: 以UI驱动应用程序,以常量代替mock数据库
Stage 2: UI App constant-as-mock-database

我准备让你创建自己的UI(用户界面)来驱动这个应用程序。由于代码有点长,这里只列出一些关键的:

I’m going to let you create your own UI and have it drive the Discounter application, since the code is a bit long to include here. Some of the key lines in the code are these:

...
 Discounter app = new Discounter();
public void actionPerformed(ActionEvent event) 
{
    ...
   String amountStr = text1.getText();
   double amount = Double.parseDouble(amountStr);
   discount = app.discount(amount));
   text3.setText( "" + discount );
   ...

现在,这个应用程序既可以演示也可以进行自动化回归测试。用户端的两个适配器(FIT和用户界面)都可以运行了。

At this point the application can be both demoed and regression tested. The user-side adapters are both running.

步骤3: 以FIT或UI驱动应用程序,使用mock数据库
Stage 3: (FIT or UI) App mock database

为了创建一个可替换的数据库适配器,我们开发了一个repository接口(【译注】repository直译为“仓库”,在程序的持久层中,常采用这一术语表示数据库或文件等存储机制),于是,就可以通过”RepositoryFactory”生成mock数据库或者实际的数据库对象了。

To create a replaceable adapter for the database side, we create an “interface” to a repository, a “RepositoryFactory” that will produce either the mock database or the real service object, and the in-memory mock for the database.

public interface RateRepository {
   double getRate(double amount);
 }
public class RepositoryFactory {
   public RepositoryFactory() {  super(); }
   public static RateRepository getMockRateRepository() 
   {
      return new MockRateRepository();
   }
}
public class MockRateRepository implements RateRepository {
   public double getRate(double amount) 
   {
      if(amount <= 100) return 0.01;
      if(amount <= 1000) return 0.02;
      return 0.05;
    }
 }

为了将这个适配器挂接到Discounter应用程序,我们要修改该应用程序自身,使其接受一个repository适配器。然后,让用户端适配器(FIT或UI)将合用的repository(真实的或mock)传入应用程序的构造器。下面是修改过的应用程序以及一个FIT适配器,该适配器向应用程序传入一个mock repository(选择采用mock还是真实repository的FIT代码比较长,但不会带来什么新的信息,因此我在这里将其省略了)。

To hook this adapter into the Discounter application, we need to update the application itself to accept a repository adapter to use, and then have the (FIT or UI) user-side adapter pass the repository to use (real or mock) into the constructor of the application itself. Here is the updated application and a FIT adapter that passes in a mock repository (the FIT adapter code to choose whether to pass in the mock or real repository’s adapter is longer without adding much new information, so I omit that version here).

import repository.RepositoryFactory;
import repository.RateRepository;
public class Discounter {
   private RateRepository rateRepository;
   public Discounter(RateRepository r) 
   {
      super();
      rateRepository = r;
    }
   public double discount(double amount) 
   {
      double rate = rateRepository.getRate( amount ); 
      return amount * rate;
    }
}

import app.Discounter;
import fit.ColumnFixture;
public class TestDiscounter extends ColumnFixture {
   private Discounter app = 
       new Discounter(RepositoryFactory.getMockRateRepository());
   public double amount;
   public double discount() 
   {
      return app.discount( amount );
   }
}

这样,我们就得到了一个六边形架构的最简单的实现。
That concludes implementation of the simplest version of the hexagonal architecture.

还有一个使用Ruby和Rack,在浏览器中运行的实现,参见 https://github.com/totheralistair/SmallerWebHexagon

For a different implementation, using Ruby and Rack for browser usage, see https://github.com/totheralistair/SmallerWebHexagon

注释 Application Notes

“左-右”端的区别
The Left-Right Asymmetry

端口和适配器模式有意地装作所有端口在本质上是一样的。这有利于在架构层面说明问题。而在实现上,不同的端口和适配器则可分作两类。我称之为“主”、“从”适配器,也可称为“驱动者”和“被驱动者”,这种分类的原因我马上就会解释。

The ports and adapters pattern is deliberately written pretending that all ports are fundamentally similar. That pretense is useful at the architectural level. In implementation, ports and adapters show up in two flavors, which I’ll call “primary” and “secondary”, for soon-to-be-obvious reasons. They could be also called “driving” adapters and “driven” adapters.

留心的读者应该已经注意到,在上面给出的所有例子中,FIT装置总是用在左端,而mock则是在右端。在三层架构中,FIT位于顶层而mock位于底层。
The alert reader will have noticed that in all the examples given, FIT fixtures are used on the left-side ports and mocks on the right. In the three-layer architecture, FIT sits in the top layer and the mock sits in the bottom layer.

这与用例中的“主参与者”和“从参与者”的思想有关。“主参与者”驱动应用程序运行(使应用程序由不活跃的状态运行起来,完成一定的功能)。“从参与者”则被应用程序驱动,用于返回一个结果或仅仅被通知而已。“主”和“从”的区别在于,谁驱动(或者负责管理)应用程序的会话。

This is related to the idea from use cases of “primary actors” and “secondary actors”. A “primary actor” is an actor that drives the application (takes it out of quiescent state to perform one of its advertised functions). A “secondary actor” is one that the application drives, either to get answers from or to merely notify. The distinction between “primary ” and “secondary” lies in who triggers or is in charge of the conversation.

FIT可以自然地充当测试时的“主参与者”适配器,因为这一框架就是用来读取测试脚本,并驱动应用程序的。Mock数据库可以自然地充当测试时的“从参与者”适配器,因为它是用来响应应用程序的查询或记录来自应用程的事件的。

The natural test adapter to substitute for a “primary” actor is FIT, since that framework is designed to read a script and drive the application. The natural test adapter to substitute for a “secondary” actor such as a database is a mock, since that is designed to answer queries or record events from the application.

通过以上的观察,我们可以根据系统的“用例语境图”(【译注】use case context diagram, 参见 http://blog.casecomplete.com/post/Getting-Started-with-Use-Cases-The-Context-Diagram)绘制端口和适配器。将“主端口”和“主适配器”画在六边形的左方或上方;将“从端口”或“从适配器”画在六边形的右方或下方。

These observations lead us to follow the system’s use case context diagram and draw the “primary ports” and “primary adapters” on the left side (or top) of the hexagon, and the “secondary ports” and “secondary adapters” on the right (or bottom) side of the hexagon.

记住主从端口和适配器的关系,以及它们在FIT和mock中的实现是有价值的。但这是在使用了“端口及适配器”架构的过程中体现出来的,请不要绕过该架构本身。采用端口及适配器最大的好处是拥有使应用程序在彻底隔离的环境下运行的能力。

The relationship between primary and secondary ports/adapters and their respective implementation in FIT and mocks is useful to keep in mind, but it should be used as a consequence of using the ports and adapters architecture, not to short-circuit it. The ultimate benefit of a ports and adapters implementation is the ability to run the application in a fully isolated mode.

用例和应用程序的边界
Use Cases And The Application Boundary

使用六边形架构模式有利于促使人们采用下述建议的方式去书写用例。
It is useful to use the hexagonal architecture pattern to reinforce the preferred way of writing use cases.

一个常见错误在于,在用例中包含端口以外的技术细节。这样的用例冗长、费解、乏味、脆弱、且难以维护,因此在业界名声极差。

A common mistake is to write use cases to contain intimate knowledge of the technology sitting outside each port. These use cases have earned a justifiably bad name in the industry for being long, hard-to-read, boring, brittle, and expensive to maintain.

理解了端口和适配器架构,我们就可以看到,用例通常应该描述发生在应用程序边界(内部的六边形)上的功能和事件,而不应关心端口外部的技术。这样的用例更加简短、易读、易于维护和稳定。

Understanding the ports and adapters architecture, we can see that the use cases should generally be written at the application boundary (the inner hexagon), to specify the functions and events supported by the application, regardless of external technology. These use cases are shorter, easier to read, less expensive to maintain, and more stable over time.

多少个端口?
How Many Ports?

到底什么是(或不是)一个端口,这主要取决于个人口味。在一个极端,每个用例都拥有自己的端口,于是,很多应用程序会有数以百计的端口。在另一个极端,可以将所有主端口和从端口分别合并,于是,整个应用程序只剩下两个端口,一个在左端,一个在右端。

What exactly a port is and isn’t is largely a matter of taste. At the one extreme, every use case could be given its own port, producing hundreds of ports for many applications. Alternatively, one could imagine merging all primary ports and all secondary ports so there are only two ports, a left side and a right side.

看起来两个极端都不是最好的。
Neither extreme appears optimal.

在“已知应用”一节中将描述的气象系统,自然地使用了四个端口:气象数据获取、管理员、要通知的订阅者、以及订阅者数据库。咖啡机控制器自然地有四个端口:用户、存储配方和价格的数据库、分注器、钱箱。医院的药剂系统可以有三个端口:一个给护士使用、一个处方数据库、一个计算机控制的药物分注器。

The weather system described in the Known Uses has four natural ports: the weather feed, the administrator, the notified subscribers, the subscriber database. A coffee machine controller has four natural ports: the user, the database containing the recipes and prices, the dispensers, and the coin box. A hospital medication system might have three: one for the nurse, one for the prescription database, and one for the computer-controller medication dispensers.

我们看不出如果端口的数量“选错了”会对系统造成什么特别的危害,所以这不过是一个直觉判断的问题。我倾向于选择较小的数量,两个、三个或四个,就像上面给出的以及“已知应用”中将要描述的例子那样。

It doesn’t appear that there is any particular damage in choosing the “wrong” number of ports, so that remains a matter of intuition. My selection tends to favor a small number, two, three or four ports, as described above and in the Known Uses.

已知应用 Known Uses

六边形架构[双语]
图4

图4展示了一个具有四个端口,每个端口上有若干适配器的应用程序。这来源于一个真实的应用程序,它监听国家气象服务提供的有关地震、龙卷风、火灾和洪水的警报,然后通过电话或电话应答机通知用户。当初我们和开发人员讨论这个系统时,它的接口是根据“面向特定目的的具体技术”来确定和讨论的。它有一个通过有线设备接受触发数据的接口;一个向电话应答机发送通知数据的接口;一个以GUI实现的系统管理接口;和一个用于获取订阅者数据的数据库接口。

Figure 4 shows an application with four ports and several adapters at each port. This was derived from an application that listened for alerts from the national weather service about earthquakes, tornadoes, fires and floods, and notified people on their telephones or telephone answering machines. At the time we discussed this system, the system’s interfaces were identified and discussed by “technology, linked to purpose”. There was an interface for trigger-data arriving over a wire feed, one for notification data to be sent to answering machines, an administrative interface implemented in a GUI, and a database interface to get their subscriber data.

开发人员们一度干得很吃力,因为他们要为应用程序增加一个来自健康服务的http接口,一个给订阅者发送信息的电子邮件接口;此外,他们还要想办法根据不同客户的偏好,对日益复杂的程序进行分解和包装。不同功能的排列组合形成了各自独立的版本,需要分别进行实现、测试和维护,因此,他们担心自己正眼睁睁地陷入一场维护和测试的噩梦。

The people were struggling because they needed to add an http interface from the weather service, an email interface to their subscribers, and they had to find a way to bundle and unbundle their growing application suite for different customer purchasing preferences. They feared they were staring at a maintenance and testing nightmare as they had to implement, test and maintain separate versions for all combinations and permutations.

于是,他们改变了思路,即根据系统的“目的”而不是技术来进行架构设计;并且,通过采用适配器,使(在“主从”两端的)具体技术成为可替换的。这样,他们马上就能够在系统中增加通过http获取数据以及通过电子邮件发送通知的功能了(新的适配器在图中以虚线表示)。由于每个应用程序可以仅通过API运行,他们可以增加一个“程序到程序”的适配器,并且将应用程序分解,在需要时连接到子应用。最后,由于每个应用程序在完全隔离的情况下运行,通过替换到测试和mock适配器,他们就可以使用独立的测试脚本进行回归测试。

Their shift in design was to architect the system’s interfaces “by purpose” rather than by technology, and to have the technologies be substitutable (on all sides) by adapters. They immediately picked up the ability to include the http feed and the email notification (the new adapters are shown in the drawing with dashed lines). By making each application executable in headless mode through APIs, they could add an app-to-app adapter and unbundle the application suite, connecting the sub-applications on demand. Finally, by making each application executable completely in isolation, with test and mock adapters in place, they gained the ability to regression test their applications with stand-alone automated test scripts.

Mac, Windows, Google, Flickr, Web 2.0

在1990年代早期,Macintosh的应用程序,比如说字处理程序,需要具有被API驱动的接口,以便其它程序或用户写的脚本可以访问该程序的所有功能。Windows桌面应用也逐渐发展出了同样的功能(我不知道那一个在先,也不知道两者之间是否存在什么关系)。

In the early 1990s, MacIntosh applications such as word processor applications were required to have API-drivable interfaces, so that applications and user-written scripts could access all the functions of the applications. Windows desktop applications have evolved the same ability (I don’t have the historical knowledge to say which came first, nor is that relevant to the point).

目前(2005年),web应用的发展趋势是对外发布API,使别的web应用可以由此访问自己的功能。因此,可以在谷歌地图上发布本地犯罪数据,或者创建一个包含Flickr的照片存档和注释功能的应用程序。

The current (2005) trend in web applications is to publish an API and let other web applications access those APIs directly. Thus, it is possible to publish local crime data over a Google map, or create web applications that include Flickr’s photo archiving and annotating abilities.

上述例子都是关于使“主”端口的API可见的。我们在这里还看不到有关从端口的内容。

All of these examples are about making the “primary” ports’ APIs visible. We see no information here about the secondary ports.

对输出进行存储 Stored Outputs

这个例子是Willem Bogaerts在C2 wiki上写的:
This example written by Willem Bogaerts on the C2 wiki:

“我遇到了一些类似的问题,不过主要是因为我的应用程序层倾向于管理太多不该管的东西,好像要变成一个电话总机一样(【译注】国外有人用电话总机比喻管理各种事情的枢纽)。我的程序会将输出显示给用户,并且有可能将输出内容保存起来。问题在于,并不总是需要保存输出内容。所以,我的程序生成输出时,必须把输出内容缓存起来。然后,当用户决定要保存输出内容时,程序再从缓存中把数据取出,真的存储起来。

“I encountered something similar, but mainly because my application layer had a strong tendency to become a telephone switchboard that managed things it should not do. My application generated output, showed it to the user and then had some possibility to store it as well. My main problem was that you did not need to store it always. So my application generated output, had to buffer it and present it to the user. Then, when the user decided that he wanted to store the output, the application retrieved the buffer and stored it for real.

我十分不喜欢这种做法。后来我想到一个解决方案:开发一个具有存储功能的展现控制器。现在,应用程序不必将输出切换到不同的通道了,只需输出到展现控制器。由展现控制器将输出内容缓存并给用户提供存储这些内容的选择。

I did not like this at all. Then I came up with a solution: Have a presentation control with storage facilities. Now the application no longer channels the output in different directions, but it simply outputs it to the presentation control. It’s the presentation control that buffers the answer and gives the user the possibility to store it.

传统的分层架构强调‘UI’和‘存储’的区别。而在端口和适配器架构下,这两者不过都是“输出”的不同表现形式罢了。”

The traditional layered architecture stresses “UI” and “storage” to be different. The Ports and Adapters Architecture can reduce output to being simply “output” again. ”

C2-wiki上一个未署名的例子
Anonymous example from the C2-wiki

“在我参与过的一个项目中,我们将系统比喻成‘组合音响’。系统中的每个组件都定义了若干个接口,每个接口具有一个特定的目的。这样,我们就可以简单地使用‘电缆’和适配器,将各种组件以几乎无限种方式组合起来了。”

“In one project I worked on, we used the System Metaphor of a component stereo system. Each component has defined interfaces, each of which has a specific purpose. We can then connect components together in almost unlimited ways using simple cables and adapters.”

分布式的大型团队开发
Distributed, Large-Team Development

本例仍然在尝试阶段,因此不太适合算作该模式的应用。但思考一下还是挺有趣的。

This one is still in trial use and so does not properly count as a use of the pattern. However, it is interesting to consider.

分布在不同地区的团队都使用六边形架构。他们采用FIT和mock,以便使程序和组件能够被独立地测试。CruiseControl构建每半小时跑一次,使用FIT和mock运行所有的程序。当应用程序子系统和数据库完成后,就使用测试数据库代替mock数据库。

Teams in different locations all build to the Hexagonal architecture, using FIT and mocks so the applications or components can be tested in standalone mode. The CruiseControl build runs every half hour and runs all the applications using the FIT+mock combination. As application subsystem and databases get completed, the mocks are replaced with test databases.

对UI和应用程序逻辑分别进行开发
Separating Development of UI and Application Logic

本例仍然在尝试阶段,因此不太适合算作该模式的应用。但思考一下还是挺有趣的。

This one is still in early trial use and so does not count as a use of the pattern. However, it is interesting to consider.

UI设计尚不稳定,这时由于驱动UI的技术还没有确定。后端的服务架构也没有确定,实际上,可能在未来的六个月中多次变动。尽管如此,项目已经正式启动并开始计算时间了。

The UI design is unstable, as they haven’t decided on a driving technology or a metaphor yet. The back-end services architecture hasn’t been decided, and in fact will probably change several times over the next six months. Nonetheless, the project has officially started and time is ticking by.

开发团队通过创建FIT测试和使用mock,来将应用的不同层次隔离开来,并构建出可测试的,可以向用户演示的功能。一旦关于UI和后端服务的决策最终确定,应该可以“直接”将这些已经开发出的元素组成应用程序。让我们等待最终的结果(或者你自己也可以试一下,并把结果写给我)。

The application team creates FIT tests and mocks to isolate their application, and creates testable, demonstrable functionality to show their users. When the UI and back-end services decisions finally get met, it “should be straightforward” to add those elements the application. Stay tuned to learn how this works out (or try it yourself and write me to let me know).

Adapter 适配器

《设计模式》一书描述了通用的“适配器”模式:“将类的接口转换成客户端所需的另一个接口。”端口适配器模式是“适配器”模式的一个具体应用。

The “Design Patterns” book contains a description of the generic “Adapter” pattern: “Convert the interface of a class into another interace clients expect.” The ports-and-adapters pattern is a particular use of the “Adapter” pattern.

Model-View-Controller 模型-视图-控制器

早在1974年,Smalltalk项目中就使用了MVC模式。这些年来,它演化出了不同的形式。如Model-Interactor和Model-View-Presenter。所有形式都实现了端口适配器模式中的主端口,而不是从端口。

The MVC pattern was implemented as early as 1974 in the Smalltalk project. It has been given, over the years, many variations, such as Model-Interactor and Model-View-Presenter. Each of these implements the idea of ports-and-adapters on the primary ports, not the secondary ports.

Mock Objects and Loopback mock对象和loopback(回绕)

“Mock object是针对一个对象的‘双重代理’,用于测试其它对象的行为。首先,mock object模拟一个接口或对象的外部行为,充当一个虚拟的实现。其次,mock object观察其它对象是怎样和自己的方法进行交互的,并和预期的行为进行对比。当发现两者不相符时,mock object可以中断测试并报告错误。如果在测试过程中没有发现问题,还需要调用一个校验方法,以确保所有预期的条件都满足了,或发现并报告了错误。”
–引自 http://MockObjects.com

“A mock object is a ‘double agent’ used to test the behaviour of other objects. First, a mock object acts as a faux implementation of an interface or class that mimics the external behaviour of a true implementation. Second, a mock object observes how other objects interact with its methods and compares actual behaviour with preset expectations. When a discrepancy occurs, a mock object can interrupt the test and report the anomaly. If the discrepancy cannot be noted during the test, a verification method called by the tester ensures that all expectations have been met or failures reported.” — From http://MockObjects.com

如果完全按照mock-object模式的方法去做,mock object的使用应该贯穿在整个应用程序中,而不仅仅是用于外部接口。使用mock object的主要目的是为了清楚地描述和验证单个类和对象级的接口协议。为了简洁地描述为外部“从适配器“开发的运行于内存中的模拟实现,我借用了”mock”一词作为一种最好的表达。

Fully implemented according to the mock-object agenda, mock objects are used throughout an application, not just at the external interface. The primary thrust of the mock object movement is conformance to specified protocol at the individual class and object level. I borrow their word “mock” as the best short description of an in-memory substitute for an external secondary actor.

Lookback模式是一种显示地为外部设备创建内部替换实现的模式。

The Loopback pattern is an explicit pattern for creating an internal replacement for an external device.

Pedestals 基座

在“Patterns for Generating a Layered Architecture”一书中,Barry Rubel描述了一种在控制软件中创建“对称轴”的模式,这和“端口和适配器”模式很相像。这种被称为“基座”的模式要求为每个硬件设备实现一个代表该设备的对象,然后在控制层中将这些对象连在一起。“基座”模式可以用来表示六边形架构的“主”、“从”两侧,但并没有强调不同适配器间的相似性。并且,这一模式是用于机器控制系统的,因此不太容易将其用于IT程序。
In “Patterns for Generating a Layered Architecture”, Barry Rubel describes a pattern about creating an axis of symmetry in control software that is very similar to ports and adapters. The “Pedestal” pattern calls for implementing an object representing each hardware device within the system, and linking those objects together in a control layer. The “Pedestal” pattern can be used to describe either side of the hexagonal architecture, but does not yet stress the similarity across adapters. Also, being written for a mechanical control environment, it is not so easy to see how to apply the pattern to IT applications.

Checks 检验

这一在Ward Cunningham的模式语言中用于检测和处理用户输入错误的模式,可以很好的在内部六边形的边界上进行错误处理。

Ward Cunningham’s pattern language for detecting and handling user input errors, is good for error handling across the inner hexagon boundaries.

依赖反转(依赖注入)和Spring框架
Dependency Inversion (Dependency Injection) and SPRING

Bob Martin的依赖反转(Martin Fowler也将其称为“依赖注入”)认为,“高层的模块不应依赖于低层的模块。两者都应依赖于抽象。抽象不应依赖于细节。细节应依赖于抽象。”Martin Fowler的“依赖注入”模式给出了一些实现,展示了怎样创建可替换的“从”适配器。这样的代码可以像该文中的例子那样直接编写,也可以使用配置文件,并让Spring框架生成等价的代码。

Bob Martin’s Dependency Inversion Principle (also called Dependency Injection by Martin Fowler) states that “High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.” The ‘’Dependency Injection ‘’pattern by Martin Fowler gives some implementations. These show how to create swappable secondary actor adapters. The code can be typed in directly, as done in the sample code in the article, or using configuration files and having the SPRING framework generate the equivalent code.

Acknowledgements 致谢

Thanks to Gyan Sharma at Intermountain Health Care for providing the sample code used here. Thanks to Rebecca Wirfs-Brock for her book ‘’Object Design’’, which when read together with the ‘’Adapter’’ pattern from the ‘’Design Patterns’’ book, helped me to understand what the hexagon was about. Thanks also to the people on Ward’s wiki, who provided comments about this pattern over the years (e.g., particularly Kevin Rutherford’s http://silkandspinach.net/blog/2004/07/hexagonal_soup.html).

FIT, A Framework for Integrating Testing: Cunningham, W., online at http://fit.c2.com, and Mugridge, R. and Cunningham, W., ‘’Fit for Developing Software’’, Prentice-Hall PTR, 2005.

The “Adapter” pattern: in Gamma, E., Helm, R., Johnson, R., Vlissides, J., “Design Patterns”, Addison-Wesley, 1995, pp. 139-150.

The “Pedestal” pattern: in Rubel, B., “Patterns for Generating a Layered Architecture”, in Coplien, J., Schmidt, D., “PatternLanguages of Program Design”, Addison-Wesley, 1995, pp. 119-150.

The “Checks” pattern: by Cunningham, W., online at http://c2.com/ppr/checks.html

The “Dependency Inversion Principle”: Martin, R., in “Agile Software Development Principles Patterns and Practices”, Prentice Hall, 2003, Chapter 11: “The Dependency-Inversion Principle”, and online at http://www.objectmentor.com/resources/articles/dip.pdf

The “Dependency Injection” pattern: Fowler, M., online at http://www.martinfowler.com/articles/injection.html

The “Mock Object” pattern: Freeman, S. online at http://MockObjects.com

The “Loopback” pattern: Cockburn, A., online at http://c2.com/cgi/wiki?LoopBack

“Use cases:” Cockburn, A., “Writing Effective Use Cases”, Addison-Wesley, 2001, and Cockburn, A., “Structuring Use Cases with Goals”, online at http://alistair.cockburn.us/crystal/articles/sucwg/structuringucswithgoals.htm