第 11 章 Systems 系统
# 第 11 章 Systems 系统
by Dr. Kevin Dean Wampler
“Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build, and test.”
—Ray Ozzie, CTO, Microsoft Corporation
“复杂要人命。它消磨开发者的生命,让产品难以规划、构建和测试。”
——Ray Ozzie,微软公司首席技术官
# 11.1 HOW WOULD YOU BUILD A CITY? 如何建造一个城市
Could you manage all the details yourself? Probably not. Even managing an existing city is too much for one person. Yet, cities work (most of the time). They work because cities have teams of people who manage particular parts of the city, the water systems, power systems, traffic, law enforcement, building codes, and so forth. Some of those people are responsible for the big picture, while others focus on the details.
你能自己掌管一切细节吗?大概不行。即便是管理一个既存的城市,也是一个人无法做到的。不过,城市还是在运转(多数时候)。因为每个城市都有一组组人管理不同的部分,供水系统、供电系统、交通、执法、立法,诸如此类。有些人负责全局,其他人负责细节。
Cities also work because they have evolved appropriate levels of abstraction and modularity that make it possible for individuals and the “components” they manage to work effectively, even without understanding the big picture.
城市能运转,还因为它演化出恰当的抽象等级和模块,好让个人和他们所管理的“组件”即便在不了解全局时也能有效地运转。
Although software teams are often organized like that too, the systems they work on often don’t have the same separation of concerns and levels of abstraction. Clean code helps us achieve this at the lower levels of abstraction. In this chapter let us consider how to stay clean at higher levels of abstraction, the system level.
尽管软件团队往往也是这样组织起来,但他们所致力的工作却常常没有同样的关注面切分及抽象层级。整洁的代码帮助我们在较低层的抽象层级上达成这一目标。本章将讨论如何在较高的抽象层级——系统层级——上保持整洁。
# 11.2 SEPARATE CONSTRUCTING A SYSTEM FROM USING IT 将系统的构造与使用分开
First, consider that construction is a very different process from use. As I write this, there is a new hotel under construction that I see out my window in Chicago. Today it is a bare concrete box with a construction crane and elevator bolted to the outside. The busy people there all wear hard hats and work clothes. In a year or so the hotel will be finished. The crane and elevator will be gone. The building will be clean, encased in glass window walls and attractive paint. The people working and staying there will look a lot different too.
首先,构造与使用是非常不一样的过程。当我走笔至此,投目窗外的芝加哥,看到有一间酒店正在建设。今天,那只是个框架结构,起重机和升降机附着在外面。忙碌的人们身穿工作服,头戴安全帽。大概一年之后,酒店就将建成。起重机和升降机都会消失无踪。建筑物变得整洁,覆盖着玻璃幕墙和漂亮的漆色。在其中工作和住宿的人,会看到完全不同的景象。
Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup.
软件系统应将启始过程和启始过程之后的运行时逻辑分离开,在启始过程中构建应用对象,也会存在互相缠结的依赖关系。
The startup process is a concern that any application must address. It is the first concern that we will examine in this chapter. The separation of concerns is one of the oldest and most important design techniques in our craft.
每个应用程序都该留意启始过程。那也是本章中我们首先要考虑的问题。将关注的方面分离开,是软件技艺中最古老也最重要的设计技巧。
Unfortunately, most applications don’t separate this concern. The code for the startup process is ad hoc and it is mixed in with the runtime logic. Here is a typical example:
不幸的是,多数应用程序都没有做分离处理。启始过程代码很特殊,被混杂到运行时逻辑中。下例就是典型的情形:
public Service getService() {
if (service == null)
service = new MyServiceImpl(…); // Good enough default for most cases?
return service;
}
2
3
4
5
This is the LAZY INITIALIZATION/EVALUATION idiom, and it has several merits. We don’t incur the overhead of construction unless we actually use the object, and our startup times can be faster as a result. We also ensure that null is never returned.
这就是所谓延迟初始化/赋值,也有一些好处。在真正用到对象之前,无需操心这种架空构造,启始时间也会更短,而且还能保证永远不会返回 null 值。
However, we now have a hard-coded dependency on MyServiceImpl and everything its constructor requires (which I have elided). We can’t compile without resolving these dependencies, even if we never actually use an object of this type at runtime!
然而,我们也得到了 MyServiceImpl 及其构造器所需一切(我省略了那些代码)的硬编码依赖。不分解这些依赖关系就无法编译,即便在运行时永不使用这种类型的对象!
Testing can be a problem. If MyServiceImpl is a heavyweight object, we will need to make sure that an appropriate TEST DOUBLE1 or MOCK OBJECT gets assigned to the service field before this method is called during unit testing. Because we have construction logic mixed in with normal runtime processing, we should test all execution paths (for example, the null test and its block). Having both of these responsibilities means that the method is doing more than one thing, so we are breaking the Single Responsibility Principle in a small way.
如果 MyServiceImpl 是个重型对象,则测试也会是个问题。我们必须确保在单元测试调用该方法之前,就给 service 指派恰当的测试替身(TEST DOUBLE)[1]或仿制对象(MOCK OBJECT)。由于构造逻辑与运行过程相混杂,我们必须测试所有的执行路径(例如,null 值测试及其代码块)。有了这些权责,说明方法做了不止一件事,这样就略微违反了单一权责原则。
Perhaps worst of all, we do not know whether MyServiceImpl is the right object in all cases. I implied as much in the comment. Why does the class with this method have to know the global context? Can we ever really know the right object to use here? Is it even possible for one type to be right for all possible contexts?
最糟糕的大概是我们不知道 MyServiceImpl 在所有情形中是否都是正确的对象。我在代码注释中做了暗示。为什么该方法所属类必须知道全局情景?我们是否真能知道在这里要用到的正确对象?是否真有可能存在一种放之四海而皆准的类型?
One occurrence of LAZY-INITIALIZATION isn’t a serious problem, of course. However, there are normally many instances of little setup idioms like this in applications. Hence, the global setup strategy (if there is one) is scattered across the application, with little modularity and often significant duplication.
当然,仅出现一次的延迟初始化不算是严重问题。不过,在应用程序中往往有许多种类似的情况出现。于是,全局设置策略(如果有的话)在应用程序中四散分布,缺乏模块组织性,通常也会有许多重复代码。
If we are diligent about building well-formed and robust systems, we should never let little, convenient idioms lead to modularity breakdown. The startup process of object construction and wiring is no exception. We should modularize this process separately from the normal runtime logic and we should make sure that we have a global, consistent strategy for resolving our major dependencies.
如果我们勤于打造有着良好格式并且强固的系统,就不该让这类就手小技巧破坏模块组织性。对象构造的启始和设置过程也不例外。应当将这个过程从正常的运行时逻辑中分离出来,确保拥有解决主要依赖问题的全局性一贯策略。
# 11.2.1 Separation of Main 分解 main
One way to separate construction from use is simply to move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately. (See Figure 11-1.)
将构造与使用分开的方法之一是将全部构造过程搬迁到 main 或被称之为 main 的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置(如图 11-1 所示)。
The flow of control is easy to follow. The main function builds the objects necessary for the system, then passes them to the application, which simply uses them. Notice the direction of the dependency arrows crossing the barrier between main and the application. They all go one direction, pointing away from main. This means that the application has no knowledge of main or of the construction process. It simply expects that everything has been built properly.
控制流程很容易理解。main 函数创建系统所需的对象,再传递给应用程序,应用程序只管使用。注意看横贯 main 与应用程序之间隔篱的依赖箭头的方向。它们都从 main 函数向外走。这表示应用程序对 main 或者构造过程一无所知。它只是简单地指望一切已齐备。
# 11.2.2 Factories 工厂
Sometimes, of course, we need to make the application responsible for when an object gets created. For example, in an order processing system the application must create the
Figure 11-1 Separating construction in main()
LineItem instances to add to an Order. In this case we can use the ABSTRACT FACTORY2 pattern to give the application control of when to build the LineItems, but keep the details of that construction separate from the application code. (See Figure 11-2.)
当然,有时应用程序也要负责确定何时创建对象。比如,在某个订单处理系统中,应用程序必须创建 LineItem 实体,添加到 Order 对象。在这种情况下,我们可以使用抽象工厂模式让应用自行控制何时创建 LineItems,但构造的细节却隔离于应用程序代码之外。
Figure 11-2 Separation construction with factory
Again notice that all the dependencies point from main toward the OrderProcessing application. This means that the application is decoupled from the details of how to build a LineItem. That capability is held in the LineItemFactoryImplementation, which is on the main side of the line. And yet the application is in complete control of when the LineItem instances get built and can even provide application-specific constructor arguments.
再留意一下,所有依赖都是从 main 指向 OrderProcessing 应用程序。这代表应用程序与如何构建 LineItem 的细节是分离开来的。构建能力由 LineItemFactoryImplementation 持有,而 LineItemFactoryImplementation 又是在 main 这一边的。但应用程序能完全控制 LineItem 实体何时构建,甚至能传递应用特定的构造器参数。
# 11.2.3 Dependency Injection 依赖注入
A powerful mechanism for separating construction from use is Dependency Injection (DI), the application of Inversion of Control (IoC) to dependency management.3 Inversion of Control moves secondary responsibilities from an object to other objects that are dedicated to the purpose, thereby supporting the Single Responsibility Principle. In the context of dependency management, an object should not take responsibility for instantiating dependencies itself. Instead, it should pass this responsibility to another “authoritative” mechanism, thereby inverting the control. Because setup is a global concern, this authoritative mechanism will usually be either the “main” routine or a special-purpose container.
有一种强大的机制可以实现分离构造与使用,那就是依赖注入(Dependency Injection,DI),控制反转(Inversion of Control,IoC)在依赖管理中的一种应用手段。控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。在依赖管理情景中,对象不应负责实体化对自身的依赖。反之,它应当将这份权责移交给其他“有权力”的机制,从而实现控制的反转。因为初始设置是一种全局问题,这种授权机制通常要么是 main 例程,要么是有特定目的的容器。
JNDI lookups are a “partial” implementation of DI, where an object asks a directory server to provide a “service” matching a particular name.
JNDI 查找是 DI 的一种“部分”实现。在 JNDI 中,对象请求目录服务器提供一种符合某个特定名称的“服务”。
MyService myService = (MyService) (jndiContext.lookup("NameOfMyService"));
The invoking object doesn’t control what kind of object is actually returned (as long it implements the appropriate interface, of course), but the invoking object still actively resolves the dependency.
调用对象并不控制真正返回对象的类别(当然前提是它实现了恰当的接口),但调用对象仍然主动分解了依赖。
True Dependency Injection goes one step further. The class takes no direct steps to resolve its dependencies; it is completely passive. Instead, it provides setter methods or constructor arguments (or both) that are used to inject the dependencies. During the construction process, the DI container instantiates the required objects (usually on demand) and uses the constructor arguments or setter methods provided to wire together the dependencies. Which dependent objects are actually used is specified through a configuration file or programmatically in a special-purpose construction module.
真正的依赖注入还要更进一步。类并不直接分解其依赖,而是完全被动的。它提供可用于注入依赖的赋值器方法或构造器参数(或二者皆有)。在构造过程中,DI 容器实体化需要的对象(通常按需创建),并使用构造器参数或赋值器方法将依赖连接到一起。至于哪个依赖对象真正得到使用,是通过配置文件或在一个有特殊目的的构造模块中编程决定。
The Spring Framework provides the best known DI container for Java.4 You define which objects to wire together in an XML configuration file, then you ask for particular objects by name in Java code. We will look at an example shortly.
Spring 框架提供了最有名的 Java DI 容器。用户在 XML 配置文件中定义互相关联的对象,然后用 Java 代码请求特定的对象。稍后我们就会看到例子。
But what about the virtues of LAZY-INITIALIZATION? This idiom is still sometimes useful with DI. First, most DI containers won’t construct an object until needed. Second, many of these containers provide mechanisms for invoking factories or for constructing proxies, which could be used for LAZY-EVALUATION and similar optimizations.
但延后初始化的好处是什么呢?这种手段在 DI 中也有其作用。首先,多数 DI 容器在需要对象之前并不构造对象。其次,许多这类容器提供调用工厂或构造代理的机制,而这种机制可为延迟赋值或类似的优化处理所用。
# 11.3 SCALING UP 扩容
Cities grow from towns, which grow from settlements. At first the roads are narrow and practically nonexistent, then they are paved, then widened over time. Small buildings and empty plots are filled with larger buildings, some of which will eventually be replaced with skyscrapers.
城市由城镇而来,城镇由聚居而来。一开始,道路狭窄,几乎无人涉足,随后逐渐拓宽。小型建筑和空地渐渐被更大的建筑所取代,一些地方最终矗立起摩天大楼。
At first there are no services like power, water, sewage, and the Internet (gasp!). These services are also added as the population and building densities increase.
一开始,供电、供水、下水、互联网(哇!)等服务全部欠奉。随着人口和建筑密度的增加,这些服务也开始出现。
This growth is not without pain. How many times have you driven, bumper to bumper through a road “improvement” project and asked yourself, “Why didn’t they build it wide enough the first time!?”
这种成长并非全无阵痛。你有多少次开着车,艰难穿行过一个“道路改善”工程,问自己,“他们为什么不一开始就修条够宽的路呢?!”
But it couldn’t have happened any other way. Who can justify the expense of a six-lane highway through the middle of a small town that anticipates growth? Who would want such a road through their town?
不过那无论如何不可能实现。谁敢打包票说在一个小镇修建一条六车道的公路并不浪费呢?谁会想要这么一条穿过他们小镇的路呢?
It is a myth that we can get systems “right the first time.” Instead, we should implement only today’s stories, then refactor and expand the system to implement new stories tomorrow. This is the essence of iterative and incremental agility. Test-driven development, refactoring, and the clean code they produce make this work at the code level.
“一开始就做对系统”纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现。
But what about at the system level? Doesn’t the system architecture require preplanning? Certainly, it can’t grow incrementally from simple to complex, can it?
但在系统层面又如何?难道系统架构不需要预先做好计划吗?系统理所当然不可能从简单递增到复杂,它能行吗?
Software systems are unique compared to physical systems. Their architectures can grow incrementally, ifwe maintain the proper separation of concerns.
软件系统与物理系统可以类比。它们的架构都可以递增式地增长,只要我们持续将关注面恰当地切分。
The ephemeral nature of software systems makes this possible, as we will see. Let us first consider a counterexample of an architecture that doesn’t separate concerns adequately.
如我们将见到的那样,软件系统短生命周期本质使这一切变得可行。我们先来看一个没有充分隔离关注问题的架构反例。
The original EJB1 and EJB2 architectures did not separate concerns appropriately and thereby imposed unnecessary barriers to organic growth. Consider an Entity Bean for a persistent Bank class. An entity bean is an in-memory representation of relational data, in other words, a table row.
初始的 EJB1 和 EJB2 架构没有恰当地切分关注面,从而给有机增长压上了不必要的负担。比如一个持久 Bank 类的 Entity Bean。Entity bean 是关系数据在内存中的体现,换言之,是表格的一行。
First, you had to define a local (in process) or remote (separate JVM) interface, which clients would use. Listing 11-1 shows a possible local interface:
首先,你要定义一个本地(进程内)或远程(分离的 JVM)接口,供客户代码使用。
Listing 11-1 An EJB2 local interface for a Bank EJB
代码清单 11-1 就是一种可能的本地接口:代码清单 11-1 Bank EJB 的 EJB2 本地接口
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public interface BankLocal extends java.ejb.EJBLocalObject {
String getStreetAddr1() throws EJBException;
String getStreetAddr2() throws EJBException;
String getCity() throws EJBException;
String getState() throws EJBException;
String getZipCode() throws EJBException;
void setStreetAddr1(String street1) throws EJBException;
void setStreetAddr2(String street2) throws EJBException;
void setCity(String city) throws EJBException;
void setState(String state) throws EJBException;
void setZipCode(String zip) throws EJBException;
Collection getAccounts() throws EJBException;
void setAccounts(Collection accounts) throws EJBException;
void addAccount(AccountDTO accountDTO) throws EJBException;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
I have shown several attributes for the Bank’s address and a collection of accounts that the bank owns, each of which would have its data handled by a separate Account EJB. Listing 11-2 shows the corresponding implementation class for the Bank bean.
面列出了银行地址的几个属性,和一组该银行拥有的账户,其中每个账户的数据都由单独的 Account EJB 所持有。代码清单 11-2 展示了 Bank bean 的相应实现类。
Listing 11-2 The corresponding EJB2 Entity Bean Implementation
代码清单 11-2 相应的 EJB2 Entity Bean 实现
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public abstract class Bank implements javax.ejb.EntityBean {
// Business logic…
public abstract String getStreetAddr1();
public abstract String getStreetAddr2();
public abstract String getCity();
public abstract String getState();
public abstract String getZipCode();
public abstract void setStreetAddr1(String street1);
public abstract void setStreetAddr2(String street2);
public abstract void setCity(String city);
public abstract void setState(String state);
public abstract void setZipCode(String zip);
public abstract Collection getAccounts();
public abstract void setAccounts(Collection accounts);
public void addAccount(AccountDTO accountDTO) {
InitialContext context = new InitialContext();
AccountHomeLocal accountHome = context.lookup(”AccountHomeLocal”);
AccountLocal account = accountHome.create(accountDTO);
Collection accounts = getAccounts();
accounts.add(account);
}
// EJB container logic
public abstract void setId(Integer id);
public abstract Integer getId();
public Integer ejbCreate(Integer id) { …}
public void ejbPostCreate(Integer id) { …}
// The rest had to be implemented but were usually empty:
public void setEntityContext(EntityContext ctx) {
}
public void unsetEntityContext() {
}
public void ejbActivate() {
}
public void ejbPassivate() {
}
public void ejbLoad() {
}
public void ejbStore() {
}
public void ejbRemove() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
I haven’t shown the corresponding LocalHome interface, essentially a factory used to create objects, nor any of the possible Bank finder (query) methods you might add.
我没有列出对应的 LocalHome 接口,该接口基本上是用来创建对象的,也没有列出你可能添加的 Bank 查找器(查询)。
Finally, you had to write one or more XML deployment descriptors that specify the object-relational mapping details to a persistence store, the desired transactional behavior, security constraints, and so on.
最后,你要编写一个或多个 XML 部署说明,将对象相关映射细节指定给某个持久化存储空间,说明期望的事物行为、安全约束等。
The business logic is tightly coupled to the EJB2 application “container.” You must subclass container types and you must provide many lifecycle methods that are required by the container.
业务逻辑与 EJB2 应用“容器”紧密耦合。你必须子类化容器类型,必须提供许多个该容器所需要的生命周期方法。
Because of this coupling to the heavyweight container, isolated unit testing is difficult. It is necessary to mock out the container, which is hard, or waste a lot of time deploying EJBs and tests to a real server. Reuse outside of the EJB2 architecture is effectively impossible, due to the tight coupling.
由于存在这种与重量级容器的紧耦合,隔离单元测试就很困难。有必要模拟出容器(这很难),或者花费大量时间在真实服务器上部署 EJB 和测试。也由于耦合的存在,在 EJB2 架构之外的复用实际上变得不可能。
Finally, even object-oriented programming is undermined. One bean cannot inherit from another bean. Notice the logic for adding a new account. It is common in EJB2 beans to define “data transfer objects” (DTOs) that are essentially “structs” with no behavior. This usually leads to redundant types holding essentially the same data, and it requires boilerplate code to copy data from one object to another.
最终,连面向对象编程本身也被侵蚀。bean 不能继承自另一个 bean。留意添加新账号的逻辑。在 EJB2 bean 中,定义一种本质上是无行为 struct 的“数据传输对象”(DTO)很常见。这往往会导致拥有同样数据的冗余类型出现,而且也需要在对象之间复制数据的八股式代码。
Cross-Cutting Concerns
横贯式关注面
The EJB2 architecture comes close to true separation of concerns in some areas. For example, the desired transactional, security, and some of the persistence behaviors are declared in the deployment descriptors, independently of the source code.
在某些领域,EBJ2 架构已经很接近于真正的关注面切分。例如,在与源代码分离的部署描述中声明了期待的事务、安全及部分持久化行为。
Note that concerns like persistence tend to cut across the natural object boundaries of a domain. You want to persist all your objects using generally the same strategy, for example, using a particular DBMS6 versus flat files, following certain naming conventions for tables and columns, using consistent transactional semantics, and so on.
注意,持久化之类关注面倾向于横贯某个领域的天然对象边界。你会想用同样的策略来持久化所有对象,例如,使用 DBMS 而非平面文件,表名和列名遵循某种命名约定,采用一致的事务语义,等等。
In principle, you can reason about your persistence strategy in a modular, encapsulated way. Yet, in practice, you have to spread essentially the same code that implements the persistence strategy across many objects. We use the term cross-cutting concerns for concerns like these. Again, the persistence framework might be modular and our domain logic, in isolation, might be modular. The problem is the fine-grained intersection of these domains.
原则上,你可以从模块、封装的角度推理持久化策略。但在实践上,你却不得不将实现了持久化策略的代码铺展到许多对象中。我们用术语“横贯式关注面”来形容这类情况。同样,持久化框架和领域逻辑,孤立地看也可以是模块化的。问题在于横贯这些领域的情形。
In fact, the way the EJB architecture handled persistence, security, and transactions, “anticipated” aspect-oriented programming (AOP),7 which is a general-purpose approach to restoring modularity for cross-cutting concerns.
实际上,EJB 架构处理持久化、安全和事务的方法是“预期”面向方面编程(aspect-oriented programming,AOP),而 AOP 是一种恢复横贯式关注面模块化的普适手段。
In AOP, modular constructs called aspects specify which points in the system should have their behavior modified in some consistent way to support a particular concern. This specification is done using a succinct declarative or programmatic mechanism.
在 AOP 中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明或编程机制来实现的。
Using persistence as an example, you would declare which objects and attributes (or patterns thereof) should be persisted and then delegate the persistence tasks to your persistence framework. The behavior modifications are made noninvasively8 to the target code by the AOP framework. Let us look at three aspects or aspect-like mechanisms in Java.
以持久化为例,可以声明哪些对象和属性(或其模式)应当被持久化,然后将持久化任务委托给持久化框架。行为的修改由 AOP 框架以无损方式在目标代码中进行。下面来看看 Java 中的三种方面或类似方面的机制。
# 11.4 JAVA PROXIES Java 代理
Java proxies are suitable for simple situations, such as wrapping method calls in individual objects or classes. However, the dynamic proxies provided in the JDK only work with interfaces. To proxy classes, you have to use a byte-code manipulation library, such as CGLIB, ASM, or Javassist.
Java 代理适用于简单的情况,例如在单独的对象或类中包装方法调用。然而,JDK 提供的动态代理仅能与接口协同工作。对于代理类,你得使用字节码操作库,比如 CGLIB、ASM 或 Javassist。
Listing 11-3 shows the skeleton for a JDK proxy to provide persistence support for our Bank application, covering only the methods for getting and setting the list of accounts.
代码清单 11-3 展示了为我们的 Bank 应用程序提供持久化支持的 JDK 代理,代码仅覆盖设置和取得账号列表的方法。
Listing 11-3 JDK Proxy Example
代码清单 11-3 JDK 代理范例
// Bank.java (suppressing package names…)
import java.utils.*;
// The abstraction of a bank.
public interface Bank {
Collection<Account> getAccounts();
void setAccounts(Collection<Account> accounts);
}
// BankImpl.java
import java.utils.*;
// The “Plain Old Java Object" (POJO) implementing the abstraction.
public class BankImpl implements Bank {
private List<Account> accounts;
public Collection<Account> getAccounts() {
return accounts;
}
public void setAccounts(Collection<Account> accounts) {
this.accounts = new ArrayList<Account>();
for (Account account: accounts) {
this.accounts.add(account);
}
}
}
// BankProxyHandler.java
import java.lang.reflect.*;
import java.util.*;
// “InvocationHandler" required by the proxy API.
public class BankProxyHandler implements InvocationHandler {
private Bank bank;
public BankHandler (Bank bank) {
this.bank = bank;
}
// Method defined in InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if (methodName.equals("getAccounts")) {
bank.setAccounts(getAccountsFromDatabase());
return bank.getAccounts();
} else if (methodName.equals("setAccounts")) {
bank.setAccounts((Collection<Account>) args[0]);
setAccountsToDatabase(bank.getAccounts());
return null;
} else {
…
}
}
// Lots of details here:
protected Collection<Account> getAccountsFromDatabase() { … }
protected void setAccountsToDatabase(Collection<Account> accounts) { … }
}
// Somewhere else…
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] { Bank.class },
new BankProxyHandler(new BankImpl()));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
We defined an interface Bank, which will be wrapped by the proxy, and a Plain-Old Java Object (POJO), BankImpl, that implements the business logic. (We will revisit POJOs shortly.)
我们定义了将被代理包装起来的接口 Bank,还有旧式的 Java 对象(Plain-Old Java Object,POJO)BankImpl,该对象实现业务逻辑(稍后再来看 POJO)。
The Proxy API requires an InvocationHandler object that it calls to implement any Bank method calls made to the proxy. Our BankProxyHandler uses the Java reflection API to map the generic method invocations to the corresponding methods in BankImpl, and so on.
Proxy API 需要一个 InvocationHandler 对象,用来实现对代理的全部 Bank 方法调用。BankProxyHandler 使用 Java 反射 API 将一般方法调用映射到 BankImpl 中的对应方法,以此类推。
There is a lot of code here and it is relatively complicated, even for this simple case.10 Using one of the byte-manipulation libraries is similarly challenging. This code “volume” and complexity are two of the drawbacks of proxies. They make it hard to create clean code! Also, proxies don’t provide a mechanism for specifying system-wide execution “points” of interest, which is needed for a true AOP solution.
即便对于这样简单的例子,也有许多相对复杂的代码。使用那些字节操作类库也同样具有挑战性。代码量和复杂度是代理的两大弱点,创建整洁代码变得很难!另外,代理也没有提供在系统范围内指定执行点的机制,而那正是真正的 AOP 解决方案所必须的。
# 11.5 PURE JAVA AOP FRAMEWORKS 纯 Java AOP 框架
Fortunately, most of the proxy boilerplate can be handled automatically by tools. Proxies are used internally in several Java frameworks, for example, Spring AOP and JBoss AOP, to implement aspects in pure Java.12 In Spring, you write your business logic as Plain-Old Java Objects. POJOs are purely focused on their domain. They have no dependencies on enterprise frameworks (or any other domains). Hence, they are conceptually simpler and easier to test drive. The relative simplicity makes it easier to ensure that you are implementing the corresponding user stories correctly and to maintain and evolve the code for future stories.
幸运的是,编程工具能自动处理大多数代理模板代码。在数个 Java 框架中,代理都是内嵌的,如 Spring AOP 和 JBoss AOP 等,从而能够以纯 Java 代码实现面向方面编程。在 Spring 中,你将业务逻辑编码为旧式 Java 对象。POJO 自扫门前雪,并不依赖于企业框架(或其他域)。因此,它在概念上更简单、更易于测试驱动。相对简单性也较易于保证正确地实现相应的用户故事,并为未来的用户故事维护和改进代码。
You incorporate the required application infrastructure, including cross-cutting concerns like persistence, transactions, security, caching, failover, and so on, using declarative configuration files or APIs. In many cases, you are actually specifying Spring or JBoss library aspects, where the framework handles the mechanics of using Java proxies or byte-code libraries transparently to the user. These declarations drive the dependency injection (DI) container, which instantiates the major objects and wires them together on demand.
使用描述性配置文件或 API,你把需要的应用程序构架组合起来,包括持久化、事务、安全、缓存、恢复等横贯性问题。在许多情况下,你实际上只是指定 Spring 或 Jboss 类库,框架以对用户透明的方式处理使用 Java 代理或字节代码库的机制。这些声明驱动了依赖注入(DI)容器,DI 容器再实体化主要对象,并按需将对象连接起来。
Listing 11-4 shows a typical fragment of a Spring V2.5 configuration file, app.xml13:
代码清单 11-4 展示了 Spring V2.5 配置文件 app.xml 的典型片段。
Listing 11-4 Spring 2.X configuration file
代码清单 11-4 Spring 2.x 的配置文件
<beans>
…
<bean id="appDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="com.mysql.jdbc.Driver" p:url="jdbc:mysql://localhost:3306/mydb" p:username="me" />
<bean id="bankDataAccessObject" class="com.example.banking.persistence.BankDataAccessObject" p:dataSource-ref="appDataSource" />
<bean id="bank" class="com.example.banking.model.Bank" p:dataAccessObject-ref="bankDataAccessObject" />
…
</beans>
2
3
4
5
6
7
8
9
Each “bean” is like one part of a nested “Russian doll,” with a domain object for a Bank proxied (wrapped) by a data accessor object (DAO), which is itself proxied by a JDBC driver data source. (See Figure 11-3.)
每个 bean 就像是嵌套“俄罗斯套娃”中的一个,每个由数据存取器对象(DAO)代理(包装)的 Bank 都有个域对象,而 bean 本身又是由 JDBC 驱动程序数据源代理(如图 11-3 所示)。
Figure 11-3 The “Russian doll” of decorators
The client believes it is invoking getAccounts() on a Bank object, but it is actually talking to the outermost of a set of nested DECORATOR14 objects that extend the basic behavior of the Bank POJO. We could add other decorators for transactions, caching, and so forth.
客户代码以为调用的是 Bank 对象的 getAccount( )方法,其实它是在与一组扩展 Bank POJO 基础行为的油漆工(DECORATOR)对象中最外面的那个沟通。
In the application, a few lines are needed to ask the DI container for the top-level objects in the system, as specified in the XML file.
在应用程序中,只添加了少数几行代码,用来向 DI 容器请求系统中的顶层对象,如 XML 文件中所定义的那样。
XmlBeanFactory bf =
new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");
2
3
Because so few lines of Spring-specific Java code are required, the application is almost completely decoupled from Spring, eliminating all the tight-coupling problems of systems like EJB2.
只有区区几行与 Spring 相关的 Java 代码,应用程序几乎完全与 Spring 分离,消除了 EJB2 之类系统中那种紧耦合问题。
Although XML can be verbose and hard to read,15 the “policy” specified in these configuration files is simpler than the complicated proxy and aspect logic that is hidden from view and created automatically. This type of architecture is so compelling that frameworks like Spring led to a complete overhaul of the EJB standard for version 3. EJB3 largely follows the Spring model of declaratively supporting cross-cutting concerns using XML configuration files and/or Java 5 annotations.
尽管 XML 可能会冗长且难以阅读,配置文件中定义的“策略”还是要比那种隐藏在幕后自动创建的复杂的代理和方面逻辑来得简单。这种类型的架构是如此引人注目,Spring 之类的框架最终导致了 EJB 标准在第 3 版的彻底变化。使用 XML 配置文件和/或 Java 5 annotation,EJB3 很大程度上遵循了 Spring 通过描述性手段支持横贯式关注面的模型。代码清单 11-5 展示了用 EJB3 重写的 Bank 对象。
Listing 11-5 shows our Bank object rewritten in EJB3.
代码清单 11-5 展示了用 EJB3 重写的 Bank 对象。
Listing 11-5 An EBJ3 Bank EJB
代码清单 11-5 EJB3 版本的 Bank
package com.example.banking.model;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@Entity
@Table(name = "ANKS")
public class Bank implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Embeddable // An object “inlined" in Bank’s DB row
public class Address {
protected String streetAddr1;
protected String streetAddr2;
protected String city;
protected String state;
protected String zipCode;
}
@Embedded
private Address address;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
mappedBy = "bank")
private Collection<Account> accounts = new ArrayList<Account>();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public void addAccount(Account account) {
account.setBank(this);
accounts.add(account);
}
public Collection<Account> getAccounts() {
return accounts;
}
public void setAccounts(Collection<Account> accounts) {
this.accounts = accounts;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
This code is much cleaner than the original EJB2 code. Some of the entity details are still here, contained in the annotations. However, because none of that information is outside of the annotations, the code is clean, clear, and hence easy to test drive, maintain, and so on.
上列代码要比原本的 EJB2 代码整洁多了。有些实体细节仍然在 annotation 中存在。不过,因为没有任何信息超出 annotation 之外,代码依然整洁、清晰,也因此而易于测试驱动、易于维护。
Some or all of the persistence information in the annotations can be moved to XML deployment descriptors, if desired, leaving a truly pure POJO. If the persistence mapping details won’t change frequently, many teams may choose to keep the annotations, but with far fewer harmful drawbacks compared to the EJB2 invasiveness.
如果愿意的话,annotation 中有些或全部持久化信息可以转移到 XML 部署描述中,只留下真正的纯 POJO。如果持久化映射细节不会频繁改动,许多团队可能会选择保留 annotation,但与 EJB2 那种侵害性相比还是少了很多问题。
# 11.6 ASPECTJ ASPECTS AspectJ 的方面
Finally, the most full-featured tool for separating concerns through aspects is the AspectJ language,17 an extension of Java that provides “first-class” support for aspects as modularity constructs. The pure Java approaches provided by Spring AOP and JBoss AOP are sufficient for 80–90 percent of the cases where aspects are most useful. However, AspectJ provides a very rich and powerful tool set for separating concerns. The drawback of AspectJ is the need to adopt several new tools and to learn new language constructs and usage idioms.
通过方面来实现关注面切分的功能最全的工具是 AspectJ 语言,一种提供“一流的”将方面作为模块构造处理支持的 Java 扩展。在 80%~90%用到方面特性的情况下,Spring AOP 和 JBoss AOP 提供的纯 Java 实现手段足够使用。然而,AspectJ 却提供了一套用以切分关注面的丰富而强有力的工具。AspectJ 的弱势在于,需要采用几种新工具,学习新语言构造和使用方式。
The adoption issues have been partially mitigated by a recently introduced “annotation form” of AspectJ, where Java 5 annotations are used to define aspects using pure Java code. Also, the Spring Framework has a number of features that make incorporation of annotation-based aspects much easier for a team with limited AspectJ experience.
藉由 AspectJ 近期引入的“annotation form”(使用 Java 5 annotation 定义纯 Java 代码的方面),新工具采用的问题大大减少。另外,Spring Framework 也有一些让拥有较少 AspectJ 经验的团队更容易组合基于 annotation 的方面的特性。
A full discussion of AspectJ is beyond the scope of this book. See [AspectJ], [Colyer], and [Spring] for more information.
关于 AspectJ 的全面探讨已经超出本书范围。更多信息可参见[AspectJ]、[Colyer]和[Spring]。
# 11.7 TEST DRIVE THE SYSTEM ARCHITECTURE 测试驱动系统架构
The power of separating concerns through aspect-like approaches can’t be overstated. If you can write your application’s domain logic using POJOs, decoupled from any architecture concerns at the code level, then it is possible to truly test drive your architecture. You can evolve it from simple to sophisticated, as needed, by adopting new technologies on demand. It is not necessary to do a Big Design Up Front18 (BDUF). In fact, BDUF is even harmful because it inhibits adapting to change, due to the psychological resistance to discarding prior effort and because of the way architecture choices influence subsequent thinking about the design.
通过方面式的手段切分关注面的威力不可低估。假使你能用 POJO 编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构。采用一些新技术,就能将架构按需从简单演化到精细。没必要先做大设计(Big Design Up Front,BDUF)。实际上,BDUF 甚至是有害的,它阻碍改进,因为心理上会抵制丢弃既成之事,也因为架构上的方案选择影响到后续的设计思路。
Building architects have to do BDUF because it is not feasible to make radical architectural changes to a large physical structure once construction is well underway.19 Although software has its own physics,20 it is economically feasible to make radical change, if the structure of the software separates its concerns effectively.
建筑设计师不得不做 BDUF,因为一旦建造过程开始,就不可能对大型物理建筑的结构做根本性改动。尽管软件也有物理的一面,只要软件的构架有效切分了各个关注面,还是有可能做根本性改动的。
This means we can start a software project with a “naively simple” but nicely decoupled architecture, delivering working user stories quickly, then adding more infrastructure as we scale up. Some of the world’s largest Web sites have achieved very high availability and performance, using sophisticated data caching, security, virtualization, and so forth, all done efficiently and flexibly because the minimally coupled designs are appropriately simple at each level of abstraction and scope.
这意味着我们可以从“简单自然”但切分良好的架构开始做软件项目,快速交付可工作的用户故事,随着规模的增长添加更多基础架构。有些世界上最大的网站采用了精密的数据缓存、安全、虚拟化等技术,获得了极高的可用性和性能,在每个抽象层和范围之内,那些最小化耦合的设计都简单到位,效率和灵活性也随之而来。
Of course, this does not mean that we go into a project “rudderless.” We have some expectations of the general scope, goals, and schedule for the project, as well as the general structure of the resulting system. However, we must maintain the ability to change course in response to evolving circumstances.
当然,这不是说要毫无准备地进入一个项目。对于总的覆盖范围、目标、项目进度和最终系统的总体构架,我们会有所预期。不过,我们必须有能力随机应变。
The early EJB architecture is but one of many well-known APIs that are over-engineered and that compromise separation of concerns. Even well-designed APIs can be overkill when they aren’t really needed. A good API should largely disappear from view most of the time, so the team expends the majority of its creative efforts focused on the user stories being implemented. If not, then the architectural constraints will inhibit the efficient delivery of optimal value to the customer.
EJB 早期架构就是一种著名的过度工程化而没能有效切分关注面的 API。在没能真正得到使用时,设计得再好的 API 也等于是杀鸡用牛刀。优秀的 API 在大多数时间都该在视线之外,这样团队才能将创造力集中在要实现的用户故事上。否则,架构上的约束就会妨碍向客户交付优化价值的软件。
To recap this long discussion, An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java (or other) Objects. The different domains are integrated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code.
概言之,最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯 Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。
# 11.8 OPTIMIZE DECISION MAKING 优化决策
Modularity and separation of concerns make decentralized management and decision making possible. In a sufficiently large system, whether it is a city or a software project, no one person can make all the decisions.
模块化和关注面切分成就了分散化管理和决策。在巨大的系统中,不管是一座城市或一个软件项目,无人能做所有决策。
We all know it is best to give responsibilities to the most qualified persons. We often forget that it is also best to postpone decisions until the last possible moment. This isn’t lazy or irresponsible; it lets us make informed choices with the best possible information. A premature decision is a decision made with suboptimal knowledge. We will have that much less customer feedback, mental reflection on the project, and experience with our implementation choices if we decide too soon.
众所周知,最好是授权给最有资格的人。但我们常常忘记了,延迟决策至最后一刻也是好手段。这不是懒惰或不负责;它让我们能够基于最有可能的信息做出选择。提前决策是一种预备知识不足的决策。如果决策太早,就会缺少太多客户反馈、关于项目的思考和实施经验。
The agility provided by a POJO system with modularized concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced.
拥有模块化关注面的 POJO 系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂性也降低了。
# 11.9 USE STANDARDS WISELY, WHEN THEY ADD DEMONSTRABLE VALUE 明智使用添加了可论证价值的标准
Building construction is a marvel to watch because of the pace at which new buildings are built (even in the dead of winter) and because of the extraordinary designs that are possible with today’s technology. Construction is a mature industry with highly optimized parts, methods, and standards that have evolved under pressure for centuries.
建筑构造大有可观,既因为新建筑的构建过程(即便是在隆冬季节),也因为那些现今科技所能实现的超凡设计。建筑业是一个成熟行业,有着高度优化的部件、方法和久经岁月历练的标准。
Many teams used the EJB2 architecture because it was a standard, even when lighter-weight and more straightforward designs would have been sufficient. I have seen teams become obsessed with various strongly hyped standards and lose focus on implementing value for their customers.
即便是轻量级和更直截了当的设计已足敷使用,许多团队还是采用了 EJB2 架构,只因为 EJB2 是个标准。我见过一些团队,纠缠于这个或那个名声大噪的标准,却丧失了对为客户实现价值的关注。有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合。
Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.
# 11.10 SYSTEMS NEED DOMAIN-SPECIFIC LANGUAGES 系统需要领域特定语言
Building construction, like most domains, has developed a rich language with a vocabulary, idioms, and patterns21 that convey essential information clearly and concisely. In software, there has been renewed interest recently in creating Domain-Specific Languages (DSLs),22 which are separate, small scripting languages or APIs in standard languages that permit code to be written so that it reads like a structured form of prose that a domain expert might write.
建筑,与大多数其他领域一样,发展出一套丰富的语言,有词汇、熟语和清晰而简洁地表达基础信息的句式。在软件领域,领域特定语言(Domain-Specific Language,DSL)最近重受关注。DSL 是一种单独的小型脚本语言或以标准语言写就的 API,领域专家可以用它编写读起来像是组织严谨的散文一般的代码。
A good DSL minimizes the “communication gap” between a domain concept and the code that implements it, just as agile practices optimize the communications within a team and with the project’s stakeholders. If you are implementing domain logic in the same language that a domain expert uses, there is less risk that you will incorrectly translate the domain into the implementation.
优秀的 DSL 填平了领域概念和实现领域概念的代码之间的“壕沟”,就像敏捷实践优化了开发团队和甲方之间的沟通一样。如果你用与领域专家使用的同一种语言来实现领域逻辑,就会降低不正确地将领域翻译为实现的风险。
DSLs, when used effectively, raise the abstraction level above code idioms and design patterns. They allow the developer to reveal the intent of the code at the appropriate level of abstraction.
DSL 在有效使用时能提升代码惯用法和设计模式之上的抽象层次。它允许开发者在恰当的抽象层级上直指代码的初衷。
Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJOs, from high-level policy to low-level details.
领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用 POJO 来表达。
# 11.11 CONCLUSION 小结
Systems must be clean too. An invasive architecture overwhelms the domain logic and impacts agility. When the domain logic is obscured, quality suffers because bugs find it easier to hide and stories become harder to implement. If agility is compromised, productivity suffers and the benefits of TDD are lost.
系统也应该是整洁的。侵害性架构会湮灭领域逻辑,冲击敏捷能力。当领域逻辑受到困扰,质量也就堪忧,因为缺陷更易隐藏,用户故事更难实现。当敏捷能力受到损害时,生产力也会降低,TDD 的好处遗失殆尽。
At all levels of abstraction, the intent should be clear. This will only happen if you write POJOs and you use aspect-like mechanisms to incorporate other implementation concerns noninvasively.
在所有的抽象层级上,意图都应该清晰可辨。只有在编写 POJO 并使用类方面的机制来无损地组合其他关注面时,这种事情才会发生。
Whether you are designing systems or individual modules, never forget to use the simplest thing that can possibly work.
无论是设计系统或单独的模块,别忘了使用大概可工作的最简单方案。