Chap17. BOUNDARIES: DRAWING LINES 划分边界

Chap17. 划分边界 - 图1

Software architecture is the art of drawing lines that I call boundaries. Those boundaries separate software elements from one another, and restrict those on one side from knowing about those on the other. Some of those lines are drawn very early in a project’s life—even before any code is written. Others are drawn much later. Those that are drawn early are drawn for the purposes of deferring decisions for as long as possible, and of keeping those decisions from polluting the core business logic.

软件架构设计本身就是一门划分边界的艺术。也界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。其中有一些边界是作项目初期——甚至在编写代码之前——就已经划分好,而其他的边界则是后来才划分的。在项目初期划分这些边界的目的是方便我们尽量将一些决策延后进行,并且确保未来这些决策不会对系统的核心业务逻辑产生干扰。

Recall that the goal of an architect is to minimize the human resources required to build and maintain the required system. What it is that saps this kind of people-power? Coupling—and especially coupling to premature decisions.

正如我们之前所说,架构师们所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。那么我们就需要了解一个系统最消耗人力资源的是什么?答案是系统中存在的耦合尤其是那些过早做出的、不成熟的决策所导致的耦合。

Which kinds of decisions are premature? Decisions that have nothing to do with the business requirements—the use cases—of the system. These include decisions about frameworks, databases, web servers, utility libraries, dependency injection, and the like. A good system architecture is one in which decisions like these are rendered ancillary and deferrable. A good system architecture does not depend on those decisions. A good system architecture allows those decisions to be made at the latest possible moment, without significant impact.

那么,怎样的决策会被认为是过早且不成熟的呢?答案是那些决策与系统的业务需求(也就是用例)无关。这部分决策包括我们要采用的框架、数据库、Web 服务器、工具库、依赖注入等。在一个设计良好的系统架构中,这些细节性的决策都应该是辅助性的,可以被推迟的。一个设计良好的系统架构不应该依赖于这些细节?而应该尽可能地推迟这些细节性的决策,并致力于将这种推迟所产生的影响降到最低。

A COUPLE OF SAD STORIES 几个悲伤的故事

Here’s the sad story of company P, which serves as a warning about making premature decisions. In the 1980s the founders of P wrote a simple monolithic desktop application. They enjoyed a great deal of success and grew the product through the 1990s into a popular and successful desktop GUI application.

下面要讲的是一个来自 P 公司的悲伤的故事,我们在这里是将它作为一个草率决策的反面案例给大家展示的。在 20 世纪 80 年代,P 公司的创始团队开发了一个单体结构的简单桌面应用。然后,产品获得了极大的成功,并在 20 世纪 90 年代成功地成长为一个广为人知的桌面 GUI 应用。

But then, in the late 1990s, the web emerged as a force. Suddenly everybody had to have a web solution, and P was no exception. P’s customers clamored for a version of the product on the web. To meet this demand, the company hired a bunch of hotshot twenty-something Java programmers and embarked upon a project to webify their product.

然而到了 20 世纪 90 年代末期,Web 卷起了一股浪潮,突然间每个公司都在推出自己的 Web 解决方案,P 公司自然也不能置身事外。P 公司的客户吵闹着要求它提供一个 Web 版的产品。为了应对这种需求,该公司雇用了一群二十几岁的 Java 程序员,开始着手将他们的产品 Web 化。

The Java guys had dreams of server farms dancing in their heads, so they adopted a rich three-tiered “architecture”1 that they could distribute through such farms. There would be servers for the GUI, servers for the middleware, and servers for the database. Of course.

结果,这群 Java 小子满脑子朝思暮想的就是如何将大规模服务器集群应用起来,所以他们采用了一个三层的富“架构”,将系统的各层应用分布到一个大型服务集群中,这样一来,GUI、中间件和数据库自然就都要运行在不同的服务器上。

The programmers decided, very early on, that all domain objects would have three instantiations: one in the GUI tier, one in the middleware tier, and one in the database tier. Since these instantiations lived on different machines, a rich system of interprocessor and inter-tier communications was set up. Method invocations between tiers were converted to objects, serialized, and marshaled across the wire.

这帮程序员在开发初期就决定系统中所有领域对象都要有三份实例:GUI 层一份、中间件层一份、数据库层一份。而由于这些实例要运行在不同的机器上,于是一套完整的跨处理器,跨层通信的富系统被设计了出来。该系统各层之间的函数调用都会被转变为对象,这些对象在经过序列化和编码处理之后进行网络传输。

Now imagine what it took to implement a simple feature like adding a new field to an existing record. That field had to be added to the classes in all three tiers, and to several of the inter-tier messages. Since data traveled in both directions, four message protocols needed to be designed. Each protocol had a sending and receiving side, so eight protocol handlers were required. Three executables had to be built, each with three updated business objects, four new messages, and eight new handlers.

现在假设我们想要增加一个特别简单的功能,为现有的记录添加一个字段。在上述情况下,这个字段必须被同步添加到系统所有三个分层的类,以及几个用于跨层通信的消息结构中。由于数据是双向流动的,这意味着我们要为此设计四个传输协议。而又由于每个协议都有各自的发送方和接收方,所以我们总共需要实现八个传输协议处理函数。总结一下,我们需要做的是同时构建三个可执行文件,每个文件都要包含三个变更过的业务对象、四个新的消息结构以及八个新的处理函数。

And think of what those executables had to do to implement the simplest of features. Think of all the object instantiations, all the serializations, all the marshaling and de-marshaling, all the building and parsing of messages, all the socket communications, timeout managers, retry scenarios, and all the other extra stuff that you have to do just to get one simple thing done.

这就是我们添加一个最简单的功能所需要做的事情。读者可以想想所有这些新增对象要进行的初始化、序列化、编码和反编码、消息构建和解析、socket 通信、超时管理、重试情况处理等过程,我们做所有的这些事情就只是为了完成一点小小的功能吗?这代价未免太大了点。

Of course, during development the programmers did not have a server farm. Indeed, they simply ran all three executables in three different processes on a single machine. They developed this way for several years. But they were convinced that their architecture was right. And so, even though they were executing in a single machine, they continued all the object instantiations, all the serializations, all the marshaling and de-marshaling, all the building and parsing of messages, all the socket communications, and all the extra stuff in a single machine.

当然,这帮程序员在开发过程中实际上是没有大型服务器集群可以用的。他们其实仍然是在一台机器上运行那三个可执行文件,而且就这样开发了几年时间。然而,即使这个系统只在一台机器上执行,也还是要经历所有这些对象的初始化、序列化、编码与反编码、消息的构建和解析、socket 通信等过程的,但他们直到最后还是坚信自己的架构是正确的。

The irony is that company P never sold a system that required a server farm. Every system they ever deployed was a single server. And in that single server all three executables continued all the object instantiations, all the serializations, all the marshaling and de-marshaling, all the building and parsing of messages, all the socket communications, and all the extra stuff, in anticipation of a server farm that never existed, and never would.

这里最讽刺的是,P 公司从来就没有销售过一个需要服务器集群的系统,他们所有曾经部署过的系统都是在一台机器上完成的。然后在这台机器上,系统中所有的三个可执行文件仍然继续着这些对象初始化、序列化、编码与反编码、消息的构建与解析、socket 通信等不必要的工作,就是为了适应一个并不存在且也永远不会存在的集群环境。

The tragedy is that the architects, by making a premature decision, multiplied the development effort enormously.

这个故事的悲剧之处在于,软件架构师通过一个草率的决定无谓地将开发成本放大了数倍之多。

The story of P is not isolated. I’ve seen it many times and in many places. Indeed, P is a superposition of all those places.

而且,P 公司的故事绝不是个案,我在很多地方看到过这个故事的各种版本。

But there are worse fates than P.

接下来,还有比 P 公司更糟糕的故事。

Consider W, a local business that manages fleets of company cars. They recently hired an “Architect” to get their rag-tag software effort under control. And, let me tell you, control was this guy’s middle name. He quickly realized that what this little operation needed was a full-blown, enterprise-scale, service-oriented “ARCHITECTURE.” He created a huge domain model of all the different “objects” in the business, designed a suite of services to manage these domain objects, and put all the developers on a path to Hell. As a simple example, suppose you wanted to add the name, address, and phone number of a contact person to a sales record. You had to go to the ServiceRegistry and ask for the service ID of the ContactService. Then you had to send a CreateContact message to the ContactService. Of course, this message had dozens of fields that all had to have valid data in them—data to which the programmer had no access, since all the programmer had was a name, address, and phone number. After faking the data, the programmer had to jam the ID of the newly created contact into the sales record and send the UpdateContact message to the SaleRecordService.

我们再来看一下 W 公司的故事。该公司有一项管理其租赁车队的本地业务,他们最近招聘了一位“架构师”来控制他们目前的软件开发成本。而且据说“控制”这个词就是这家伙的中间名- 总之这位“架构师”来了之后很快做了判断,认为运作这个小小的业务需要的是一个全套的、企业级的、面向服务的“架构”。于是,他就创建了一个巨大的域模型,其中包含了该业务所涉及的所有“对象”,并设计了一整套服务来管理这些对象,差点将所有开发人员逼疯。在他这套“架构”中,如果我们想在销售记录中添加一个联系人,提供名字、地址和电话号码,就必须先访问 ServiceRegistry,查询 ContactService 的 ID。然后需要发送一条 CreateContact 消息给 ContactService。当然,这个消息结构有几十个字段,每一个字段都需要有效的数据来填充——这些数据是普通程序员无法访问的,他们手里只有名字、地址和电话号码。只有在伪造数据之后,程序员才能将新建的 Contact 记录 ID 填入 UpdateContact 消息中,最后还要发送给 SaleRecordService。

Of course, to test anything you had to fire up all the necessary services, one by one, and fire up the message bus, and the BPel server, and … And then, there were the propagation delays as these messages bounced from service to service, and waited in queue after queue.

当然,为了测试这一切,我们还必须将所有的服务全部运行起来,同时还要运行消息总线、BPel 服务器等。更糟糕的是,这些消息还会在每个服务之间传递时出现延迟,需要一个队列接一个队列地等待。

And then if you wanted to add a new feature—well, you can imagine the coupling between all those services, and the sheer volume of WSDLs that needed changing, and all the redeployments those changes necessitated …

如果还需要添加新功能的话,读者可以想象一下所有这些服务之间的耦合关系、那些需要修改的大量 WSDL 文件以及需要进行的重新部署。

Hell starts to seem like a nice place by comparison.

说真的,地狱看起来也不过如此吧!

There’s nothing intrinsically wrong with a software system that is structured around services. The error at W was the premature adoption and enforcement of a suite of tools that promised SoA—that is, the premature adoption of a massive suite of domain object services. The cost of those errors was sheer person-hours—person-hours in droves—flushed down the SoA vortex.

按照服务来组织一个软件系统的结构这件事本身并没有什么问题,W 公司的错误主要在于太过草率地采用和强制推广了一套号称是 SOA 体系的工具——也就是说,他们过于草率地采用了一整套域对象服务体系。这个错误让公司大量的人力都耗费在了实现这个所谓的 SOA 架构上。

I could go on describing one architectural failure after another. But let’s talk about an architectural success instead.

我们还可以一个接一个地描述很多这类架构设计失败的案例,但还是先来讲一些成功的案例吧。

FITNESSE

My Son, Micah, and I started work on FitNesse in 2001. The idea was to create a simple wiki that wrapped Ward Cunningham’s FIT tool for writing acceptance tests.

我和我儿子 Micah 在 2001 年创立了一家叫 FitNesse 的公司。想法很简单,就是用一个简单的 wiki 来包装一下 Ward Cunningham 的 FIT 工具,以编写验收测试(acceptance test)。

This was back in the days before Maven “solved” the jar file problem. I was adamant that anything we produced should not require people to download more than one jar file. I called this rule, “Download and Go.” This rule drove many of our decisions.

这件事情发生在 Maven 工具面世并“解决了” jar 文件问题之前。我当时坚信我们的产品不应该让用户下载超过一个的 jar 文件,我称这条规则为“下载即可执行”。这条规则指导了我们之后的很多决策。

One of the first decisions was to write our own web server, specific to the needs of FitNesse. This might sound absurd. Even in 2001 there were plenty of open source web servers that we could have used. Yet writing our own turned out to be a really good decision because a bare-bones web server is a very simple piece of software to write and it allowed us to postpone any web framework decision until much later.2

第一个决策是根据 FitNesse 的需要专门编写了属于我们自己的 Web 服务器。这可能听起来很傻,即使在 2001 年,市面上也有足够多的开源的 Web 服务器可供选用。然而编写属于自己的 Web 服务器实际上是一个非常好的决策,因为一个只包含基本功能的 Web 服务器部署起来非常简单,它允许我们将任何与具体 Web 框架相关的决策延后。

Another early decision was to avoid thinking about a database. We had MySQL in the back of our minds, but we purposely delayed that decision by employing a design that made the decision irrelevant. That design was simply to put an interface between all data accesses and the data repository itself.

我们做的另一个早期决策是避免考虑数据库问题。我们当时确实考虑过使用 MySQL,但最后还是故意采用了一种与数据库无关的设计,而延后了这方面的决策。这部分的设计也很简单,就是在所有数据访问逻辑与数据仓库之间增加一个接口。

We put the data access methods into an interface named WikiPage. Those methods provided all the functionality we needed to find, fetch, and save pages. Of course, we didn’t implement those methods at first; we simply stubbed them out while we worked on features that didn’t involve fetching and saving the data.

我们将数据访问方法放在一个名为 WikiPage 的接口中。这部分方法负责提供所需的查找、获取和保存页面的功能。当然,我们最初并没有具体实现这些方法,在开发不需要获取和保存数据的那部分功能时,我们只写了一个占位方法。

Indeed, for three months we simply worked on translating wiki text into HTML. This didn’t require any kind of data storage, so we created a class named MockWikiPage that simply left the data access methods stubbed.

确实,我们花了三个月时间来解决 wiki 文本与 HTML 之间的转换问题,这部分的工作与任何类型的数据存储都无关。所以我们在创建名为 MockWikiPage 的模块时,只与了一些空的数据访问方法。

Eventually, those stubs became insufficient for the features we wanted to write. We needed real data access, not stubs. So we created a new derivative of WikiPage named InMemoryPage. This derivative implemented the data access method to manage a hash table of wiki pages, which we kept in RAM.

最终,当这些占位方法不再支持我们所要开发的功能时,我们才需要真正实现数据访问。为此我们创建了一个名为 InMemoryPage 的 WikiPage 的派生类。在这个派生类中,实现了一系列数据访问方法来管理与 wiki 页面相关的哈希表(该哈希表会一直存在于内存中)。

This allowed us to write feature after feature for a full year. In fact, we got the whole first version of the FitNesse program working this way. We could create pages, link to other pages, do all the fancy wiki formatting, and even run tests with FIT. What we couldn’t do was save any of our work.

就这样,我们一个接一个地开发这些业务功能,花了整整一年时间。事实上,FitNesse 程序能正常运行的第一个版本是这样的,它能让我们创建页面、链接其他页面、使用 wiki 格式完成所有的装饰,甚至用 FIT 工具运行测试。唯一不能做的就是真正地保存数据。

When it came time to implement persistence, we thought again about MySQL, but decided that wasn’t necessary in the short term, because it would be really easy to write the hash tables out to flat files. So we implemented FileSystemWikiPage, which just moved the functionality out to flat files, and then we continued developing more features.

当该系统需要实现持久化时,我们又仔细考虑了一下是否采用 MySQL,最终还是觉得短期内没有必要,因为将哈希表写入文件是非常简单的。所以我们实现了一个 FileSystemWikiPmge,用单一的大文件实现了这一功能,同时将我们的精力投入继续开发更多的新功能上。

Three months later, we reached the conclusion that the flat file solution was good enough; we decided to abandon the idea of MySQL altogether. We deferred that decision into nonexistence and never looked back.

三个月之后,我们得出一个结论,大文件的存储己经足够好了,完全没有必要使 MySQL。就这样,我们不断推迟这个决策,到最后发现这个决策不需要做了。

That would be the end of the story if it weren’t for one of our customers who decided that he needed to put the wiki into MySQL for his own purposes. We showed him the architecture of WikiPages that had allowed us to defer the decision. He came back a day later with the whole system working in MySQL. He simply wrote a MySqlWikiPage derivative and got it working.

如果不是我们的一个客户由于自己的需要而希望将数据存入 MySQL,我们的故事就到此为止了。事实上,在我们给他展示了 WikiPage 架构细节之后,他只用了一天时间就实现了一个能在 MySQL 上运行的系统,他所做的就是写了一个名为 MySQLWikiPage 的派生类,该系统就可以正常工作了。

We used to bundle that option with FitNesse, but nobody else ever used it, so eventually we dropped it. Even the customer who wrote the derivative eventually dropped it.

我们曾经将这个实现和 FitNesse 一起打包分发,但是后来没有人真正用到过它,甚至连实现这个功能的用户最终也不再使用它了。

Early in the development of FitNesse, we drew a boundary line between business rules and databases. That line prevented the business rules from knowing anything at all about the database, other than the simple data access methods. That decision allowed us to defer the choice and implementation of the database for well over a year. It allowed us to try the file system option, and it allowed us to change direction when we saw a better solution. Yet it did not prevent, or even impede, moving in the original direction (MySQL) when someone wanted it.

在开发 FitNesse 的早期,我们在业务逻辑和数据库之间画了一条边界线。这条线有效地防止了业务逻辑对数据库产生依赖,它只能访问简单的数据访问方法。这个决策使我们将与数据库选型和实现的决策推迟了超过一年。同时我们还能用文件系统进行实验。使我们最终换了一个更好的解决方案。更重要的是,该架构在需求真的出现时,没有阻止任何人采用 MySQL,甚至没有为其制造任何障碍。

The fact that we did not have a database running for 18 months of development meant that, for 18 months, we did not have schema issues, query issues, database server issues, password issues, connection time issues, and all the other nasty issues that raise their ugly heads when you fire up a database. It also meant that all our tests ran fast, because there was no database to slow them down.

在长达 18 个月的开发过程中,我们事实上没有用到过数据库,这意味着我们不需要面对表结构问题、查询问题、数据库服务器问题、密码问题、链接时间问题等一系列由于数据库带来的棘手问题。这样我们所有的测试都会进行得很快,因为它们都不依赖数据库。

In short, drawing the boundary lines helped us delay and defer decisions, and it ultimately saved us an enormous amount of time and headaches. And that’s what a good architecture should do.

简单来说,通过划清边界,我们可以推迟和延后一些细节性的决策,这最终会为我们节省大量的时间、避免大量的问题。这就是一个设计良好的架构所应该带来的助益。

WHICH LINES DO YOU DRAW, AND WHEN DO YOU DRAW THEM? 应在何时、何处画这些线

You draw lines between things that matter and things that don’t. The GUI doesn’t matter to the business rules, so there should be a line between them. The database doesn’t matter to the GUI, so there should be a line between them. The database doesn’t matter to the business rules, so there should be a line between them.

边界线应该画在那些不相关的事情中间。GUI 与业务逻辑无关,所以两者之间应该有一条边界线。数据库与 GUI 无关,这两者之间也应该有一条边界线。数据库只与业务逻辑无关,所以两者之间也应该有一条边界线。

Some of you may have rejected one or more of those statements, especially the part about the business rules not caring about the database. Many of us have been taught to believe that the database is inextricably connected to the business rules. Some of us have even been convinced that the database is the embodiment of the business rules.

上面这些话,尤其是关于业务逻辑与数据库无关的部分可能会遭到部分读者的反对。大部分人都已经习惯性地认为数据库是与业务逻辑不可分割的了,有些人甚至认为,数据库相关逻辑部分本身就是业务逻辑的具体体现。

But, as we shall see in another chapter, this idea is misguided. The database is a tool that the business rules can use indirectly. The business rules don’t need to know about the schema, or the query language, or any of the other details about the database. All the business rules need to know is that there is a set of functions that can be used to fetch or save data. This allows us to put the database behind an interface.

然而正如我们在第 18 章中将会讲到的,这个想法从根本上就是错误的。数据库应该是业务逻辑间接使用的一个工具。业务逻辑并不需要了解数据库的表结构、查询语言或其他任何数据库内部的实现细节。业务逻辑唯一需要知道的,就是有一组可以用来查询和保存数据的函数。这样一来,我们才可以将数据库隐藏在接口后面。

You can see this clearly in Figure 17.1. The BusinessRules use the DatabaseInterface to load and save data. The DatabaseAccess implements the interface and directs the operation of the actual Database.

我们可以从图 17.1 中清晰地看到,BusinessRules 是通过 DatabaseInterface 来加载和保存数据的。而 DatabaseAccess 则负责实现该接口,以及其与实际 Database 的交互。

The database behind an interface

The classes and interfaces in this diagram are symbolic. In a real application, there would be many business rule classes, many database interface classes, and many database access implementations. All of them, though, would follow roughly the same pattern.

这里的类与接口仅仅是一个例子。在一个真实的应用程序中,将会有很多业务逻辑类、很多数据库接口类以及很多数据库访问的实现。不过,所有一切所遵循的模式应该是相似的。

Where is the boundary line? The boundary is drawn across the inheritance relationship, just below the DatabaseInterface (Figure 17.2).

那么这里的边界线应该被画在哪里?边界应该穿过继承关系,在 DatabaseInterface 之下(见图 17.2)。

The boundary line

Note the two arrows leaving the DatabaseAccess class. Those two arrows point away from the DatabaseAccess class. That means that none of these classes knows that the DatabaseAccess class exists.

请注意,DatabaseAccess 类的那两个对外的箭头。这两个箭头都指向了远离 DatabaseAccess 类的方向,这意味着它们所指向的两个类都不知道 DatabaseAccess 类的存在。

Now let’s pull back a bit. We’ll look at the component that contains many business rules, and the component that contains the database and all its access classes (Figure 17.3).

下面让我们把抽象层次拉高一点,看一下包含多个业务逻辑类的组件与包含数据库及其访问类的组件之间是什么关系(见图 17.3)。

The business rules and database components

Note the direction of the arrow. The Database knows about the BusinessRules. The BusinessRules do not know about the Database. This implies that the DatabaseInterface classes live in the BusinessRules component, while the DatabaseAccess classes live in the Database component.

请注意,图 17.3 中的箭头指向,它说明了 Database 组件知道 BusinessRules 组件的存在,而 BusinessRules 组件则不知道 Database 组件的存在。这意味着 DatabaseInterface 类是包含在 BusinessRules 组件中的,而 DatabaseAccess 类则被包含在 Database 组件中。

The direction of this line is important. It shows that the Database does not matter to the BusinessRules, but the Database cannot exist without the BusinessRules.

这个箭头的方向很重要。因为它意味着 Database 组件不会对 BusinessRules 组件形成的干扰,但 Database 组件却不能脫离 BusinessRules 组件而存在。

If that seems strange to you, just remember this point: The Database component contains the code that translates the calls made by the BusinessRules into the query language of the database. It is that translation code that knows about the BusinessRules.

如果读者对上面这段话感到困惑,请记住一点,Database 组件中包含了将 BusinessRules 组件中的函数调用转化为具体数据库查询语言的代码。这些转换代码当然必须知道 BusinessRules 组件的存在。

Having drawn this boundary line between the two components, and having set the direction of the arrow toward the BusinessRules, we can now see that the BusinessRules could use any kind of database. The Database component could be replaced with many different implementations—the BusinessRules don’t care.

通过在这两个组件之间画边界线,并且让箭头指向 BusinessRules 组件,我们现在可以很容易地明白为什么 BusinessRules 组件可以使用任何一种数据库。在这里,Database 组件可以被替换为多种实现,BusinessRules 组件并不需要知道这件事。

The database could be implemented with Oracle, or MySQL, or Couch, or Datomic, or even flat files. The business rules don’t care at all. And that means that the database decision can be deferred and you can focus on getting the business rules written and tested before you have to make the database decision.

数据库可以用 Oracle、MySQL、Couch 或者 Datomic,甚至大文件来实现。业务逻辑并不需要关心这件事。这意味着我们叫以将与数据库相关的决策延后,先专注于编写业务逻辑的代码,进行测试,直到不得不选择数据库为止。

WHAT ABOUT INPUT AND OUTPUT? 输入和输出怎么办

Developers and customers often get confused about what the system is. They see the GUI, and think that the GUI is the system. They define a system in terms of the GUI, so they believe that they should see the GUI start working immediately. They fail to realize a critically important principle: The IO is irrelevant.

开发者和使用者经常会对系统边界究竟如何定义而感到困惑。由于 GUI 能够直观看到,就很自然地把 GUI 当成了系统本身。这些人以 GUI 的视角来定义整个系统,所以认为从系统开发一开始 GUI 部分就应该正常工作。这是错误的,这里他们没有意识到一个非常重要的原则,即 I/O 是无关紧要的。

This may be hard to grasp at first. We often think about the behavior of the system in terms of the behavior of the IO. Consider a video game, for example. Your experience is dominated by the interface: the screen, the mouse, the buttons, and the sounds. You forget that behind that interface there is a model—a sophisticated set of data structures and functions—driving it. More importantly, that model does not need the interface. It would happily execute its duties, modeling all the events in the game, without the game ever being displayed on the screen. The interface does not matter to the model—the business rules.

这个原则可能一开始比较难以理解,毕竟我们经常从直觉上会以 I/O 的行为来定义系统的行为。以视频游戏为例,我们的主观体验是以界面反应为主的,这些反应来自屏幕、鼠标、按钮和声音等。但请不要忘了,这些界面背后存在着一个模型——一套非常复杂的数据结构和函数,那才是游戏真正的核心驱动力。更重要的是,该模型并不一定非要有一个界面。就算该游戏不显不在屏幕上,其模型也应该可以完成所有的任务逻辑,处理所有的游戏事件。因此,界面对模型——也就是业务逻辑来说——一点都不重要。

And so, once again, we see the GUI and BusinessRules components separated by a boundary line (Figure 17.4). Once again, we see that the less relevant component depends on the more relevant component. The arrows show which component knows about the other and, therefore, which component cares about the other. The GUI cares about the BusinessRules.

所以,GUI 和 BusinessRules 这两个组件之间也应该有一条边界线(见图 17.4)。再强调一次,在这里不重要的组件依赖于较为重要的组件,箭头指向的方向代表着组件之间的关系,GUI 关心 BusinessRules。

The boundary between GUI and BusinessRules components

Having drawn this boundary and this arrow, we can now see that the GUI could be replaced with any other kind of interface—and the BusinessRules would not care.

通过这条边界线以及这个箭头,我们可以看出 GUI 可以用任何一种其他形式的界面来代替。BusinessRules 组件不需要了解这些细节。

PLUGIN ARCHITECTURE 插件式架构

Taken together, these two decisions about the database and the GUI create a kind of pattern for the addition of other components. That pattern is the same pattern that is used by systems that allow third-party plugins.

综上所述,我们似乎可以基于数据库和 GUI 这两个为例来建立一种向系统添加其他组件的模式。这种模式与支持第三方插件的系统模式是一样的。

Indeed, the history of software development technology is the story of how to conveniently create plugins to establish a scalable and maintainable system architecture. The core business rules are kept separate from, and independent of, those components that are either optional or that can be implemented in many different forms (Figure 17.5).

事实上,软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,而这些其他组件要么是可以去掉的,要么是有多种实现的(见图 17.5)。

Plugging in to the business rules

Because the user interface in this design is considered to be a plugin, we have made it possible to plug in many different kinds of user interfaces. They could be web based, client/server based, SOA based, Console based, or based on any other kind of user interface technology.

由于用户界面在这个设计中是以插件形式存在的,所以我们可以用插拔的方式切换很多不同类型的用户界面。可以是基于 Web 模式的、基于客户端/服务器端模式的、基于 SOA 模式的、基于命令行模式的或者基于其他任何类型的用户界面技术。

The same is true of the database. Since we have chosen to treat it as a plugin, we can replace it with any of the various SQL databases, or a NOSQL database, or a file system-based database, or any other kind of database technology we might deem necessary in the future.

数据库也类似。因为我们现在是将数据库作为插件来对待的,所以它就可以被替换成不同类型的 SQL 数据库、NoSQL 数据库,其至基于文件系统的数据库,以及未来任何一种我们认为有必要发展的数据库技术。

These replacements might not be trivial. If the initial deployment of our system was web-based, then writing the plugin for a client-server UI could be challenging. It is likely that some of the communications between the business rules and the new UI would have to be reworked. Even so, by starting with the presumption of a plugin structure, we have at very least made such a change practical.

当然,这些替换工作可能并不轻松,如果我们的系统一开始是按照 Web 方式部署的,那么为它写一个客户端/服务器端模型的 UI 插件就可能会比较困难一些。很可能业务逻辑与新 UI 之间的交互方式也要重新修改。但即使这样,插件式架构也至少为我们提供了这种实现的可能性。

THE PLUGIN ARGUMENT 插件式架构的好处

Consider the relationship between ReSharper and Visual Studio. These components are produced by completely different development teams in completely different companies. Indeed, JetBrains, the maker of ReSharper, lives in Russia. Microsoft, of course, resides in Redmond, Washington. It’s hard to imagine two development teams that are more separate.

我们可以来看一下 ReSharper 和 Visual Studio 之间的关系。这两部分组件是由两个完全不同公司的人各自独立开发的,ReSharper 的开发者是 JetBrains 公司,它位于俄罗斯,而 Microsoft 公司则位于华盛顿州的雷德蒙市。很难想象有哪两个开发团队会比它们隔离得更彻底的了。

Which team can damage the other? Which team is immune to the other? The dependency structure tells the story (Figure 17.6). The source code of ReSharper depends on the source code of Visual Studio. Thus there is nothing that the ReSharper team can do to disturb the Visual Studio team. But the Visual Studio team could completely disable the ReSharper team if they so desired.

那么他们之间,是哪个团队可以影响另一个团队的工作呢?又是哪个团队可以对另一个团队的工作免疫呢?我们可以通过图 17.6 中的依赖关系来回答。很显然,是 ReSharper 的源代码依赖于 Visual Studio 的源代码。因此,是 ReSharper 团队无法干扰 Visual Studio 团队的工作,而 Visual Studio 团队却可以单方面中止 ReSharper 团队的任何工作。

ReSharper depends on Visual Studio

That’s a deeply asymmetric relationship, and it is one that we desire to have in our own systems. We want certain modules to be immune to others. For example, we don’t want the business rules to break when someone changes the format of a web page, or changes the schema of the database. We don’t want changes in one part of the system to cause other unrelated parts of the system to break. We don’t want our systems to exhibit that kind of fragility.

这是一种非常不对称的关系,但我们确实希望在自己的系统中构建这样的关系因为这可以让部分组件对其他组件的变更免疫。例如,当有人修改 Web 页面格式或修改数据库表结构时,系统的业务逻辑部分就不应该受到影响。另外,我们也不希望系统中某一个部分发生的变更会导致其他不相关的部分出现问题,系统不应该这么脆弱。

Arranging our systems into a plugin architecture creates firewalls across which changes cannot propagate. If the GUI plugs in to the business rules, then changes in the GUI cannot affect those business rules.

将系统设计为插件式架构,就等于构建起了一面变更无法逾越的防火墙,换句话说,只要 GUI 是以插件形式插入系统的业务逻辑中的,那么 GUI 这边所发生的变更就不会影响系统的业务逻辑。

Boundaries are drawn where there is an axis of change. The components on one side of the boundary change at different rates, and for different reasons, than the components on the other side of the boundary.

所以,边界线也应该沿着系统的变更轴来画。也就是说,位于边界线两侧的组件应该以不同原因、不同速率变化着。

GUIs change at different times and at different rates than business rules, so there should be a boundary between them. Business rules change at different times and for different reasons than dependency injection frameworks, so there should be a boundary between them.

一个系统的 GUI 与业务逻辑的变更原因、变更速率显然是不同的,所以二者中间应该有一条边界线。同样的,一个系统的业务逻辑与依赖注入框架之间的变更原因和变更速度也会不同,它们之间也应该画边界线。

This is simply the Single Responsibility Principle again. The SRP tells us where to draw our boundaries.

这其实就是单一职责原则(SRP)的具体实现,SRP 的作用就是告诉我们应该在哪里画边界线。

CONCLUSION 本章小结

To draw boundary lines in a software architecture, you first partition the system into components. Some of those components are core business rules; others are plugins that contain necessary functions that are not directly related to the core business. Then you arrange the code in those components such that the arrows between them point in one direction—toward the core business.

为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。

You should recognize this as an application of the Dependency Inversion Principle and the Stable Abstractions Principle. Dependency arrows are arranged to point from lower-level details to higher-level abstractions.

其实,这也是一种对依赖反转原则(DIP)和稳定抽象原则(SAP)的具体应用,依赖箭头应该由底层具体实现细节指向高层抽象的方向。