Skip to content

Latest commit

 

History

History
1469 lines (905 loc) · 147 KB

ch10.md

File metadata and controls

1469 lines (905 loc) · 147 KB

第 10 章 柔性设计

Ten. Supple Design

The ultimate purpose of software is to serve users. But first, that same software has to serve developers. This is especially true in a process that emphasizes refactoring. As a program evolves, developers will rearrange and rewrite every part. They will integrate the domain objects into the application and with new domain objects. Even years later, maintenance programmers will be changing and extending the code. People have to work with this stuff. But will they want to?

软件的最终目的是为用户服务。但首先它必须为开发人员服务。在强调重构的软件开发过程中尤其如此。随着程序的演变,开发人员将重新安排并重写每个部分。他们会把原有的领域对象集成到应用程序中,也会让它们与新的领域对象进行集成。甚至几年以后,负责维护的程序员还将修改和扩充代码。人们必须要做这些工作,但他们是否愿意呢?

When software with complex behavior lacks a good design, it becomes hard to refactor or combine elements. Duplication starts to appear as soon as a developer isn’t confident of predicting the full implications of a computation. Duplication is forced when design elements are monolithic, so that the parts cannot be recombined. Classes and methods can be broken down for better reuse, but it gets hard to keep track of what all the little parts do. When software doesn’t have a clean design, developers dread even looking at the existing mess, much less making a change that could aggravate the tangle or break something through an unforeseen dependency. In any but the smallest systems, this fragility places a ceiling on the richness of behavior it is feasible to build. It stops refactoring and iterative refinement.

当具有复杂行为的软件缺乏良好的设计时,重构或元素的组合会变得很困难。一旦开发人员不能十分肯定地预知计算的全部含意,就会出现重复。当设计元素都是整块的而无法重新组合的时候,重复就是一种必然的结果。我们可以对类和方法进行分解,这样可以更好地重用它们,但这些小部分的行为又变得很难跟踪。如果软件没有一个条理分明的设计,那么开发人员不仅不愿意仔细地分析代码,他们更不愿意修改代码,因为修改代码会产生问题——要么加重了代码的混乱状态,要么由于某种未预料到的依赖而破坏了某些东西。在任何一种系统中(除非是一些非常小的系统),这种不稳定性使我们很难开发出丰富的功能,而且限制了重构和迭代式的精化。

To have a project accelerate as development proceeds—rather than get weighed down by its own legacy—demands a design that is a pleasure to work with, inviting to change. A supple design.

为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于做出修改。

Supple design is the complement to deep modeling. Once you’ve dug out implicit concepts and made them explicit, you have the raw material. Through the iterative cycle, you hammer that material into a useful shape, cultivating a model that simply and clearly captures the key concerns, and shaping a design that allows a client developer to really put that model to work. Development of the design and code leads to insight that refines model concepts. Round and round—we’re back to the iterative cycle and refactoring toward deeper insight. But what kind of design are you trying to arrive at? What kind of experiments should you try along the way? That is what this chapter is about.

这就是柔性设计(supple design)。柔性设计是对深层建模的补充。一旦我们挖掘出隐式概念,并把它们显示地表达出来之后,就有了原料。通过迭代循环,我们可以把这些原料打造成有用的形式:建立的模型能够简单而清晰地捕获主要关注点;其设计可以让客户开发人员真正使用这个模型。在设计和代码的开发过程中,我们将获得新的理解,并通过这些理解改善模型概念。我们一次又一次回到迭代循环中,通过重构得到更深刻的理解。但我们究竟要获得什么样的设计呢?在这个过程中应该进行哪些实验?这正是本章要讨论的内容。

A lot of overengineering has been justified in the name of flexibility. But more often than not, excessive layers of abstraction and indirection get in the way. Look at the design of software that really empowers the people who handle it; you will usually see something simple. Simple is not easy. To create elements that can be assembled into elaborate systems and still be understandable, a dedication to MODEL-DRIVEN DESIGN has to be joined with a moderately rigorous design style. It may well require relatively sophisticated design skill to create or to use.

很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象层和间接设计常常成为项目的绊脚石。看一下真正为用户带来强大功能的软件设计,你常常会发现一些简单的东西。简单并不容易做到。为了把创建的元素装配到复杂系统中,而且在装配之后仍然能够理解它们,必须坚持模型驱动的设计方法,与此同时还要坚持适当严格的设计风格。要创建或使用这样的设计,可能需要我们掌握相对熟练的设计技巧。

Developers play two roles, each of which must be served by the design. The same person might well play both roles—even switch back and forth in minutes—but the relationship to the code is different nonetheless. One role is the developer of a client, who weaves the domain objects into the application code or other domain layer code, utilizing capabilities of the design. A supple design reveals a deep underlying model that makes its potential clear. The client developer can flexibly use a minimal set of loosely coupled concepts to express a range of scenarios in the domain. Design elements fit together in a natural way with a result that is predictable, clearly characterized, and robust.

开发人员扮演着两个角色,而设计必须要为这两个角色服务。同一个人可能会同时承担这两种角色,甚至在几分钟之内来回变换角色,但角色与代码之间的关系是不同的。一个角色是客户开发人员,负责将领域对象组织成应用程序代码或其他领域层代码,以便发挥设计的功能。柔性设计能够揭示深层次的底层模型,并把它潜在的部分明确地展现出来。客户开发人员可以灵活地使用一个最小化的、松散耦合的概念集合,并用这些概念来表示领域中的众多场景。设计元素非常自然地组合到一起,其结果也是健壮的,可以被清晰地刻画出来,而且也是可以预知的。

Equally important, the design must serve the developer working to change it. To be open to change, a design must be easy to understand, revealing that same underlying model that the client developer is drawing on. It must follow the contours of a deep model of the domain, so most changes bend the design at flexible points. The effects of its code must be transparently obvious, so the consequences of a change will be easy to anticipate.

同样重要的是,设计也必须为那些修改代码的开发人员服务。为了便于修改,设计必须易于理解,必须把客户开发人员正在使用的同一个底层模型表示出来。我们必须按照领域深层模型的轮廓进行设计,以便大部分修改都可以灵活地完成。代码的结果必须是完全清晰明了的,这样才容易预见到修改的影响。

Early versions of a design are usually stiff. Many never acquire any suppleness in the time frame or budget of the project. I’ve never seen a large program that had this quality throughout. But when complexity is holding back progress, honing the most crucial, intricate parts to a supple design makes the difference between getting sucked down into legacy maintenance and punching through the complexity ceiling.

早期的设计版本通常达不到柔性设计的要求。由于项目的时间期限和预算的缘故,很多设计一直就是僵化的。我也从未见过有哪个大型程序自始至终都是柔性的。但是,当复杂性阻碍了项目的前进时,就需要仔细修改最关键、最复杂的地方,使之变成一个柔性设计,这样才能突破复杂性带给我们的限制,而不会陷入遗留代码维护的麻烦中。

There is no formula for designing software like this, but I have culled a set of patterns that, in my experience, tend to lend suppleness to a design when they fit. These patterns and examples should give a feel for what a supple design is like and the kind of thinking that goes into it.

设计这样的软件并没有公式,但我精选了一组模式,从我自己的经验来看,这些模式如果运用得当的话,就有可能获得柔性设计。这些模式和示例展示了一个柔性设计应该是什么样的,以及在设计中所采取的思考方式。

10.1. Some patterns that contribute to supple design

10.1 INTENTION-REVEALING INTERFACES 模式:INTENTION-REVEALING INTERFACES

In domain-driven design, we want to think about meaningful domain logic. Code that produces the effect of a rule without explicitly stating the rule forces us to think of step-by-step software procedures. The same applies to a calculation that just results from running some code, but isn’t explicit. Without a clear connection to the model, it is difficult to understand the effect of the code or anticipate the effect of a change. The previous chapter delved into modeling rules and calculations explicitly. Implementing such objects requires a lot of understanding of the gritty details of the calculation or the fine print of the rule. The beauty of objects is their ability to encapsulate all that, so that client code is simple and can be interpreted in terms of higher-level concepts.

在领域驱动的设计中,我们希望看到有意义的领域逻辑。如果代码只是在执行规则后得到结果,而没有把规则显式地表达出来,那么我们就不得一步一步地去思考软件的执行步骤。那些只是运行代码然后给出结果的计算——没有显式地把计算逻辑表达出来,也有同样的问题。如果不把代码与模型清晰地联系起来,我们很难理解代码的执行效果,也很难预测修改代码的影响。前一章深入探讨了对规则和计算进行显式的建模。实现这样的对象要求我们深入理解计算或规则的大量细节。对象的强大功能是它能够把所有这些细节封装起来,如此一来,客户代码就能够很简单,而且可以用高层概念来解释。

But if the interface doesn’t tell the client developer what he needs to know in order to use the object effectively, he will have to dig into the internals to understand the details anyway. A reader of the client code will have to do the same. Then most of the value of the encapsulation is lost. We are always fighting cognitive overload: If the client developer’s mind is flooded with detail about how a component does its job, his mind isn’t clear to work out the intricacies of the client design. This is true even when the same person is playing both roles, developing and using his own code, because even if he doesn’t have to learn those details, there is a limit to how many factors he can consider at once.

但是,客户开发人员要想有效地使用对象,必须知道对象的一些信息,如果接口没有告诉开发人员这些信息,那么他就必须深入研究对象的内部机制,以便理解细节。阅读客户代码的人也需要做同样的事情。这样就失去了封装的大部分价值。我们需要避免出现“认识过载”的问题。如果客户开发人员必须总是思考组件工作方式的大量细节,那么就无暇理清思路来解决客户设计的复杂性。即便一个人同时扮演两种角色(既开发代码,也使用他自己的代码)的时候也是如此,因为他即使不必去了解那些细节,也不可能一次就把所有的因素都考虑全面。

If a developer must consider the implementation of a component in order to use it, the value of encapsulation is lost. If someone other than the original developer must infer the purpose of an object or operation based on its implementation, that new developer may infer a purpose that the operation or class fulfills only by chance. If that was not the intent, the code may work for the moment, but the conceptual basis of the design will have been corrupted, and the two developers will be working at cross-purposes.

如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。

To obtain the value of explicitly modeling a concept in the form of a class or method, we must give these program elements names that reflect those concepts. The names of classes and methods are great opportunities for improving communication between developers, and for improving the abstraction of the system.

当我们把概念显式地建模为类或方法时,为了真正从中获取价值,必须为这些程序元素赋予一个能够反映出其概念的名字。类和方法的名称为开发人员之间的沟通创造了很好的机会,也能够改善系统的抽象。

Kent Beck wrote of making method names communicate their purpose with an INTENTION-REVEALING SELECTOR (Beck 1997). All public elements of a design together make up its interface, and the name of each of those elements presents an opportunity to reveal the intention of the design. Type names, method names, and argument names all combine to form an INTENTION-REVEALING INTERFACE.

Kent Beck 曾经提出通过 INTENTION-REVEALING SELECTOR(释意命名选择器)来选择方法的名称,使名称表达出其目的[Beck 1997]。设计中的所有公共元素共同构成了接口,每个元素的名称都提供了揭示设计意图的机会。类型名称、方法名称和参数名称组合在一起,共同形成了一个 INTENTION-REVEALING INTERFACE(释意接口)。

Therefore:

因此:

Name classes and operations to describe their effect and purpose, without reference to the means by which they do what they promise. This relieves the client developer of the need to understand the internals. These names should conform to the UBIQUITOUS LANGUAGE so that team members can quickly infer their meaning. Write a test for a behavior before creating it, to force your thinking into client developer mode.

在命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与 UBIQUITOUS LANGUAGE 保持一致,以便团队成员可以迅速推断出它们的意义。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。

All the tricky mechanism should be encapsulated behind abstract interfaces that speak in terms of intentions, rather than means.

所有复杂的机制都应该封装到抽象接口的后面,接口只表明意图,而不表明方式。

In the public interfaces of the domain, state relationships and rules, but not how they are enforced; describe events and actions, but not how they are carried out; formulate the equation but not the numerical method to solve it. Pose the question, but don’t present the means by which the answer shall be found.

在领域的公共接口中,可以把关系和规则表述出来,但不要说明规则是如何实施的;可以把事件和动作描述出来,但不要描述它们是如何执行的;可以给出方程式,但不要给出解方程式的数学方法。可以提出问题,但不要给出获取答案的方法。

Example: Refactoring: A Paint-Mixing Application

重构:调漆应用程序

A program for paint stores can show a customer the result of mixing standard paints. Here is the initial design, which has a single domain class.

一家油漆商店的程序能够为客户显示出标准调漆的结果。下面是初始的设计,它有一个简单的领域类。

The only way to even guess what the paint(Paint) method does is to read the code.

paint(Paint) 方法的行为根本猜不出,想知道它的唯一方法就是阅读代码。

public void paint(Paint paint) {
   v = v + paint.getV(); //After mixing, volume is summed
   // Omitted many lines of complicated color mixing logic
   // ending with the assignment of new r, b, and y values.
}

OK, so it looks like this method combines two Paints together, the result having a larger volume and a mixed color.

从代码上看,这个方法是把两种油漆(Paint)混合到一起,结果是油漆的体积增加了,并变为混合颜色。

To shift our perspective, let’s write a test for this method. (This code is based on the JUnit test framework.)

为了换个角度来看问题,我们为这个方法编写一个测试(这段代码基于 JUnit 测试框架)。

public void testPaint() {
    // Create a pure yellow paint with volume=100
    Paint yellow = new Paint(100.0, 0, 50, 0);
    // Create a pure blue paint with volume=100
    Paint blue = new Paint(100.0, 0, 0, 50);

    // Mix the blue into the yellow
    yellow.paint(blue);

    // Result should be volume of 200.0 of green paint
    assertEquals(200.0, yellow.getV(), 0.01);
    assertEquals(25, yellow.getB());
    assertEquals(25, yellow.getY());
    assertEquals(0, yellow.getR());
}

The passing test is the starting point. It is unsatisfying at this point because the code in the test doesn’t tell us what it is doing. Let’s rewrite the test to reflect the way we would like to use the Paint objects if we were writing a client application. Initially, this test will fail. In fact, it won’t even compile. We are writing it to explore the interface design of the Paint object from the client developer’s point of view.

通过这个测试只是一个起点,这无法令我们满意,因为这段测试代码并没有告诉我们这个方法都做了什么。让我们来重新编写这个测试,看一下如果我们正在编写一个客户应用程序的话,将以何种方式来使用 Paint 对象。最初,这个测试会失败。实际上,它甚至不能编译。我们编写它的目的是从客户开发人员的角度来研究一下 Paint 对象的接口设计。

public void testPaint() {
    // Start with a pure yellow paint with volume=100
    Paint ourPaint = new Paint(100.0, 0, 50, 0);
    // Take a pure blue paint with volume=100
    Paint blue = new Paint(100.0, 0, 0, 50);

    // Mix the blue into the yellow
    ourPaint.mixIn(blue);

    // Result should be volume of 200.0 of green paint
    assertEquals(200.0, ourPaint.getVolume(), 0.01);
    assertEquals(25, ourPaint.getBlue());
    assertEquals(25, ourPaint.getYellow());
    assertEquals(0, ourPaint.getRed());
}

We should take our time to write a test that reflects the way we would like to talk to these objects. After that, we refactor the Paint class to make the test pass.

花时间编写这样的测试是非常必要的,因为它可以反映出我们希望以哪种方式与这些对象进行交互。在这之后,我们重构 Paint 类,使它通过测试,如图 10-3 所示。

The new method name may not tell the reader everything about the effect of “mixing in” another Paint (for that we’ll need ASSERTIONS, coming up in a few pages). But it will clue the reader in enough to get started using the class, especially with the example the test provides. And it will allow the reader of the client code to interpret the client’s intent. In the next few examples in this chapter, we’ll refactor this class again to make it even clearer.

新的方法名称可能不会告诉读者有关混合另一种油漆(Paint)的效果的所有信息(要达到这个目的需要使用断言,接下来我们就会讨论它)。但这个名称为读者提供了足够多的线索,使读者可以开始使用这个类,特别是从测试提供的示例开始。而且它还使客户代码的阅读者能够理解客户的意图。在本章接下来的几个示例中,我们将再次重构这个类,使它更清晰。


Entire subdomains can be carved off into separate modules and encapsulated behind INTENTION-REVEALING INTERFACES. Using such whittling to focus a project and manage the complexity of a large system will be discussed more in Chapter 15, “Distillation,” with COHESIVE MECHANISMS and GENERIC SUBDOMAINS.

整个子领域可以被划分到独立的模块中,并用一个表达了其用途的接口把它们封装起来。这种方法可以使我们把注意力集中在项目上,并控制大型系统的复杂性,这些内容将在第 15 章中的 COHESIVE MECHANISM 和 GENERIC SUBDOMAIN 部分进行更多的讨论。

But in the next two patterns, we’ll set out to make the consequences of using a method very predictable. Complex logic can be done safely in SIDE-EFFECT-FREE FUNCTIONS. Methods that change system state can be characterized with ASSERTIONS.

在接下来的两个模式中,我们将介绍如何令一个方法的执行结果变得易于预测。复杂的逻辑可以在 SIDE-EFFECT-FREE FUNCTION 中安全地执行,而改变系统状态的方法可以用 ASSERTION 来刻画。

10.2 SIDE-EFFECT-FREE FUNCTIONS 模式:SIDE-EFFECT-FREE FUNCTION

Operations can be broadly divided into two categories, commands and queries. Queries obtain information from the system, possibly by simply accessing data in a variable, possibly performing a calculation based on that data. Commands (also known as modifiers) are operations that affect some change to the systems (for a simple example, by setting a variable). In standard English, the term side effect implies an unintended consequence, but in computer science, it means any effect on the state of the system. For our purposes, let’s narrow that meaning to any change in the state of the system that will affect future operations.

我们可以宽泛地把操作分为两个大的类别:命令和查询。查询是从系统获取信息,查询的方式可能只是简单地访问变量中的数据,也可能是用这些数据执行计算。命令(也称为修改器)是修改系统的操作(举一个简单的例子,设置变量)。在标准英语中,“副作用”这个词暗示着“意外的结果”,但在计算机科学中,任何对系统状态产生的影响都叫副作用。这里为了便于讨论,我们把它的含义缩小一下,任何对未来操作产生影响的系统状态改变都可以称为副作用。

Why was the term side effect adopted and applied to quite intentional changes affected by operations? I assume this was based on experience with complex systems. Most operations call on other operations, and those called invoke still other operations. As soon as this arbitrarily deep nesting is involved, it becomes very hard to anticipate all the consequences of invoking an operation. The developer of the client may not have intended the effects of the second-tier and third-tier operations—they’ve become side effects in every sense of the phrase. Elements of a complex design interact in other ways that are likely to produce the same unpredictability. The use of the term side effect underlines the inevitability of that interaction.

为什么人们会采用“副作用”这个词来形容那些显然是有意影响系统状态的操作呢?我推测这大概是来自于复杂系统的经验。大多数操作都会调用其他的操作,而后者又会调用另外一些操作。一旦形成这种任意深度的嵌套,就很难预测调用一个操作将要产生的所有后果。第二层和第三层操作的影响可能并不是客户开发人员有意为之的,于是它们就变成了完全意义上的副作用。在一个复杂的设计中,元素之间的交互同样也会产生无法预料的结果。副作用这个词强调了这种交互的不可避免性。

Interactions of multiple rules or compositions of calculations become extremely difficult to predict. The developer calling an operation must understand its implementation and the implementation of all its delegations in order to anticipate the result. The usefulness of any abstraction of interfaces is limited if the developers are forced to pierce the veil. Without safely predictable abstractions, the developers must limit the combinatory explosion, placing a low ceiling on the richness of behavior that is feasible to build.

多个规则的相互作用或计算的组合所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。

Operations that return results without producing side effects are called functions. A function can be called multiple times and return the same value each time. A function can call on other functions without worrying about the depth of nesting. Functions are much easier to test than operations that have side effects. For these reasons, functions lower risk.

返回结果而不产生副作用的操作称为函数。一个函数可以被多次调用,每次调用都返回相同的值。一个函数可以调用其他函数,而不必担心这种嵌套的深度。函数比那些有副作用的操作更易于测试。由于这些原因,使用函数可以降低风险。

Obviously, you can’t avoid commands in most software systems, but the problem can be mitigated in two ways. First, you can keep the commands and queries strictly segregated in different operations. Ensure that the methods that cause changes do not return domain data and are kept as simple as possible. Perform all queries and calculations in methods that cause no observable side effects (Meyer 1988).

显然,在大多数软件系统中,命令的使用都是不可避免的,但有两种方法可以减少命令产生的问题。首先,可以把命令和查询严格地放在不同的操作中。确保导致状态改变的方法不返回领域数据,并尽可能保持简单。在不引起任何可观测到的副作用的方法中执行所有查询和计算[Meyer1988]。

Second, there are often alternative models and designs that do not call for an existing object to be modified at all. Instead, a new VALUE OBJECT, representing the result of the computation, is created and returned. This is a common technique, which will be illustrated in the example that follows. A VALUE OBJECT can be created in answer to a query, handed off, and forgotten—unlike an ENTITY, whose life cycle is carefully regulated.

第二,总是有一些替代的模型和设计,它们不要求对现有对象做任何修改。相反,它们创建并返回一个 VALUE OBJECT,用于表示计算结果。这是一种很常见的技术,在接下来的示例中我们就会演示它的使用。VALUE OBJECT 可以在一次查询的响应中被创建和传递,然后被丢弃——不像 ENTITY,实体的生命周期是受到严格管理的。

VALUE OBJECTS are immutable, which implies that, apart from initializers called only during creation, all their operations are functions. VALUE OBJECTS, like functions, are safer to use and easier to test. An operation that mixes logic or calculations with state change should be refactored into two separate operations (Fowler 1999, p. 279). But by definition, this segregation of side effects into simple command methods only applies to ENTITIES. After completing the refactoring to separate modification from querying, consider a second refactoring to move the responsibility for the complex calculations into a VALUE OBJECT. The side effect often can be completely eliminated by deriving a VALUE OBJECT instead of changing existing state, or by moving the entire responsibility into a VALUE OBJECT.

VALUE OBJECT 是不可变的,这意味着除了在创建期间调用的初始化程序之外,它们的所有操作都是函数。像函数一样,VALUE OBJECT 使用起来很安全,测试也很简单。如果一个操作把逻辑或计算与状态改变混合在一起,那么我们就应该把这个操作重构为两个独立的操作[Fowler1999, p. 279]。但从定义上来看,这种把副作用隔离到简单的命令方法中的做法仅适用于 ENTITY。在完成了修改和查询的分离之后,可以考虑再进行一次重构,把复杂计算的职责转移到 VALUE OBJECT 中。通过派生出一个 VALUE OBJECT(而不是改变现有状态),或者通过把职责完全转移到一个 VALUE OBJECT 中,往往可以完全消除副作用。

Therefore:

因此:

Place as much of the logic of the program as possible into functions, operations that return results with no observable side effects. Strictly segregate commands (methods that result in modifications to observable state) into very simple operations that do not return domain information. Further control side effects by moving complex logic into VALUE OBJECTS when a concept fitting the responsibility presents itself.

尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到 VALUE OBJECT 中,这样可以进一步控制副作用。

SIDE-EFFECT-FREE FUNCTIONS, especially in immutable VALUE OBJECTS, allow safe combination of operations. When a FUNCTION is presented through an INTENTION-REVEALING INTERFACE, a developer can use it without understanding the detail of its implementation.

SIDE-EFFECT-FREE FUNCTION,特别是在不变的 VALUE OBJECT 中,允许我们安全地对多个操作进行组合。当通过 INTENTION-REVEALING INTERFACE 把一个 FUNCTION 呈现出来的时候,开发人员就可以在无需理解其实现细节的情况下使用它。

Example: Refactoring the Paint-Mixing Application Again

示例再次重构调漆应用程序

A program for paint stores can show a customer the result of mixing standard paints. Picking up where we left off in the last example, here is the single domain class.

一家油漆商店的程序能够为客户显示出标准调漆的结果。我们继续前面的例子,下面是上次重构后得到的领域类:

public void mixIn(Paint other) {
   volume = volume.plus(other.getVolume());
   // Many lines of complicated color-mixing logic
   // ending with the assignment of new red, blue,
   // and yellow values.
}

The side effects of the mixIn() method

A lot is happening in the mixIn() method, but this design does follow the rule of separating modification from querying. One concern, which we’ll take up later, is that the volume of the paint 2 object, the argument of the mixIn() method, has been left in limbo. Paint 2’s volume is unchanged by the operation, which doesn’t seem quite logical in the context of this conceptual model. This was not a problem for the original developers because, as near as we can tell, they had no interest in the paint 2 object after the operation, but it is hard to anticipate the consequences of side effects or their absence. We’ll return to this question soon in the discussion of ASSERTIONS. For now, let’s look at color.

mixIn() 方法中发生了很多事情,但这个设计确实遵循了“修改和查询分离”这条原则。有一点需要注意(下面会具体讨论),这里并没有对 paint 2 对象(mixIn() 方法的一个参数)的体积做过多的考虑。操作不改变 Paint 2 的体积,在这个概念模型的上下文中,这看起来并不是十分合乎逻辑。就我们所知,这在原来的开发人员看来并不是问题,因为他们对操作之后的 paint 2 对象不感兴趣,但我们很难预测副作用会产生什么后果。在接下来要讨论的 ASSERTION 中我们很快会回头再讨论这个问题。现在,我们先来看一下颜色。

Color is an important concept in this domain. Let’s try the experiment of making it an explicit object. What should it be called? “Color” comes to mind first, but earlier knowledge crunching had already yielded the important insight that color mixing is different for paint than it is for the more familiar RGB light display. The name needs to reflect this.

在这个领域中,颜色是一个重要的概念。让我们试着把它变成一个显式的对象。它应该叫什么名字呢?首先想到的就是 Color(颜色),但我们通过先前的知识消化已经认识到了一个重要的知识,即油漆的调色与我们所熟悉的 RGB 调色是不同的。名称必须反映出这一点。

Factoring out Pigment Color does communicate more than the earlier version, but the computation is the same, still in the mixIn() method. When we moved out the color data, we should have taken related behavior with it. Before we do, note that Pigment Color is a VALUE OBJECT. Therefore, it should be treated as immutable. When we mixed paint, the Paint object itself was changed. It was an ENTITY with an ongoing life story. In contrast, a Pigment Color representing a particular shade of yellow is always exactly that. Instead, mixing will result in a new Pigment Color object representing the new color.

把 Pigment Color(颜料颜色)分离出来之后,确实比先前表达了更多信息,但计算还是相同的,仍然是在 mixIn() 方法中进行计算。当把颜色数据移出来后,与这些数据有关的行为也应该一起移出来。但是在做这件事之前,要注意 Pigment Color 是一个 VALUE OBJECT。因此,它应该是不可变的。当我们调漆时,Paint 对象本身被改变了,它是一个具有生命周期的实体。相反,表示某个色调(如黄色)的 Pigment Color 则一直表示那种颜色。调漆的结果是产生一个新的 Pigment Color 对象,用于表示新的颜色。

public class PigmentColor {

   public PigmentColor mixedWith(PigmentColor other,
                                       double ratio) {
      // Many lines of complicated color-mixing logic
      // ending with the creation of a new PigmentColor object
      // with appropriate new red, blue, and yellow values.
   }
}

public class Paint {

   public void mixIn(Paint other) {
      volume = volume + other.getVolume();
      double ratio = other.getVolume() / volume;
      pigmentColor =
         pigmentColor.mixedWith(other.pigmentColor(), ratio);
   }
}

10.8

Now the modification code in Paint is as simple as possible. The new Pigment Color class captures knowledge and communicates it explicitly, and it provides a SIDE-EFFECT-FREE FUNCTION whose result is easy to understand, easy to test, and safe to use or combine with other operations. Because it is so safe, the complex logic of color mixing is truly encapsulated. Developers using this class don’t have to understand the implementation.

现在,Paint 中的代码已经尽可能简单了。新的 Pigment Color 类捕获了知识,并显式地把这些知识表达出来,而且它还提供了一个 SIDE-EFFECT-FREE FUNCTION,这个函数的计算结果很容易理解,也很容易测试,因此可以安全地使用或与其他操作进行组合。由于它的安全性很高,因此复杂的调色逻辑真正被封装起来了。使用这个类的开发人员不必理解其实现。


10.3 ASSERTIONS 模式:ASSERTION

Separating complex computations into SIDE-EFFECT-FREE FUNCTIONS cuts the problem down to size, but there is still a residue of commands on the ENTITIES that produce side effects, and anyone using them must understand their consequences. ASSERTIONS make side effects explicit and easier to deal with.

把复杂的计算封装到 SIDE-EFFECT-FREE FUNCTION 中可以简化问题,但实体仍然会留有一些有副作用的命令,使用这些 ENTITY 的人必须了解使用这些命令的后果。在这种情况下,使用 ASSERTION(断言)可以把副作用明确地表示出来,使它们更易于处理。


True, a command containing no complex computations may be fairly easy to interpret by inspection. But in a design where larger parts are built of smaller ones, a command may invoke other commands. The developer using the high-level command must understand the consequences of each underlying command. So much for encapsulation. And because object interfaces do not restrict side effects, two subclasses that implement the same interface can have different side effects. The developer using them will want to know which is which to anticipate the consequences. So much for abstraction and polymorphism.

确实,一条不包含复杂计算的命令只需查看一下就能够理解。但是,在一个软件设计中,如果较大的部分是由较小部分构成的,那么一个命令可能会调用其他命令。开发人员在使用高层命令时,必须了解每个底层命令所产生的后果,这时封装也就没有什么价值了。而且,由于对象接口并不会限制副作用,因此实现相同接口的两个子类可能会产生不同的副作用。使用它们的开发人员需要知道哪个副作用是由哪个子类产生的,以便预测后果。这样,抽象和多态也就失去了意义。

When the side effects of operations are only defined implicitly by their implementation, designs with a lot of delegation become a tangle of cause and effect. The only way to understand a program is to trace execution through branching paths. The value of encapsulation is lost. The necessity of tracing concrete execution defeats abstraction.

如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行。封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。

We need a way of understanding the meaning of a design element and the consequences of executing an operation without delving into its internals. INTENTION-REVEALING INTERFACES carry us part of the way there, but informal suggestions of intentions are not always enough. The “design by contract” school goes the next step, making “assertions” about classes and methods that the developer guarantees will be true. This style is discussed in detail in Meyer 1988. Briefly, “post-conditions” describe the side effects of an operation, the guaranteed outcome of calling a method. “Preconditions” are like the fine print on the contract, the conditions that must be satisfied in order for the post-condition guarantee to hold. Class invariants make assertions about the state of an object at the end of any operation. Invariants can also be declared for entire AGGREGATES, rigorously defining integrity rules.

我们需要在不深入研究内部机制的情况下理解设计元素的意义和执行操作的后果。INTENTION-REVEALING INTERFACE 可以起到一部分作用,但这样的接口只能非正式地给出操作的用途,这常常是不够的。“契约式设计”(design by contract)向前推进了一步,通过给出类和方法的“断言”使开发人员知道肯定会发生的结果。[Meyer 1988]中详细讨论了这种设计风格。简言之,“后置条件”描述了一个操作的副作用,也就是调用一个方法之后必然会发生的结果。“前置条件”就像是合同条款,即为了满足后置条件而必须要满足的前置条件。类的固定规则规定了在操作结束时对象的状态。也可以把 AGGREGATE 作为一个整体来为它声明固定规则,这些都是严格定义的完整性规则。

All these assertions describe state, not procedures, so they are easier to analyze. Class invariants help characterize the meaning of a class, and simplify the client developer’s job by making the objects more predictable. If you trust the guarantee of a post-condition, you don’t have to worry about how a method works. The effects of delegations should already be incorporated into the assertions.

所有这些断言都描述了状态,而不是过程,因此它们更易于分析。类的固定规则在描述类的意义方面起到帮助作用,并且使客户开发人员能够更准确地预测对象的行为,从而简化他们的工作。如果你确信后置条件的保证,那么就不必考虑方法是如何工作的。断言应该已经把调用其他操作的效果考虑在内了。

Therefore:

因此:

State post-conditions of operations and invariants of classes and AGGREGATES. If ASSERTIONS cannot be coded directly in your programming language, write automated unit tests for them. Write them into documentation or diagrams where it fits the style of the project’s development process.

把操作的后置条件和类及 AGGREGATE 的固定规则表述清楚。如果在你的编程语言中不能直接编写 ASSERTION,那么就把它们编写成自动的单元测试。还可以把它们写到文档或图中(如果符合项目开发风格的话)。

Seek models with coherent sets of concepts, which lead a developer to infer the intended ASSERTIONS, accelerating the learning curve and reducing the risk of contradictory code.

寻找在概念上内聚的模型,以便使开发人员更容易推断出预期的 ASSERTION,从而加快学习过程并避免代码矛盾。

Even though many object-oriented languages don’t currently support ASSERTIONS directly, ASSERTIONS are still a powerful way of thinking about a design. Automated unit tests can partially compensate for the lack of language support. Because ASSERTIONS are all in terms of states, rather than procedures, they make tests easy to write. The test setup puts the preconditions in place; then, after execution, the test checks to see if the post-conditions hold.

尽管很多面向对象的语言目前都不支持直接使用 ASSERTION,但 ASSERTION 仍然不失为一种功能强大的设计方法。自动单元测试在一定程度上弥补了缺乏语言支持带来的不足。由于 ASSERTION 只声明状态,而不声明过程,因此很容易编写测试。测试首先设置前置条件,在执行之后,再检查后置条件是否被满足。

Clearly stated invariants and pre- and post-conditions allow a developer to understand the consequences of using an operation or object. Theoretically, any noncontradictory set of assertions would work. But humans don’t just compile predicates in their heads. They will be extrapolating and interpolating the concepts of the model, so it is important to find models that make sense to people as well as satisfying the needs of the application.

把固定规则、前置条件和后置条件清楚地表述出来,这样开发人员就能够理解使用一个操作或对象的后果。从理论上讲,如果一组断言之间互不矛盾,那么就可以发挥作用。但人的大脑并不会一丝不苟地把这些断言编译到一起。人们会推断和补充模型的概念,因此找到一个既易于理解又满足应用程序需求的模型是至关重要的。

Example: Back to Paint Mixing

示例回到调漆应用程序

Recall that in the previous example I was concerned about the ambiguity of what happens to the argument of the mixIn(Paint) operation on the Paint class.

在前面的示例中,我们曾注意到:在 Paint 类中 mixIn(Paint) 操作的参数到底会发生什么变化,这还存在着一些不明之处。

The receiver’s volume is increased by the amount of the argument’s volume. Drawing on our general understanding of physical paint, this mixing process should deplete the other paint by the same amount, draining it to zero volume, or eliminating it completely. The current implementation does not modify the argument, and modifying arguments is a particularly risky kind of side effect anyway.

接受者(即被混合的油漆)的所增加的体积就是参数的体积。根据我们对油漆的了解,这个混合过程应该使另一种油漆减少同样的体积,把它的体积减为零或完全删除。目前的实现并没有修改这个参数,而修改参数无疑是有产生副作用的风险的。

To start on a solid footing, let’s state the post-condition of the mixIn() method as it is:

第一步,我们先把 mixIn() 方法的后置条件声明如下:

After p1.mixIn(p2):

p1.volume is increased by amount of p2.volume.

p2.volume is unchanged.

The trouble is, developers are going to make mistakes, because these properties don’t fit the concepts we have invited them to think about. The straightforward fix would be change the volume of the other paint to zero. Changing an argument is a bad practice, but it would be easy and intuitive. We could state an invariant:

问题在于开发人员将会犯错,因为这些属性与实际概念不符。简单的修改方法是让另一种油漆的体积变为零。虽然修改参数不是一种好的行为,但这里的修改简单而直观。我们可以声明一个固定规则:

Total volume of paint is unchanged by mixing.

混合之后油漆的总体积保持不变。

But wait! While developers were pondering this option, they made a discovery. It turns out that there was a compelling reason the original designers made it this way. At the end, the program reports the list of unmixed paints that were added. After all, the ultimate purpose of this application is to help a user figure out which paints to put into a mixture.

但先等一下!当开发人员考虑这种选择时,他们有了一个新发现。最初的设计人员这样设计原来是有充分理由的。程序在最后会报告被混合之前的油漆清单。毕竟,这个程序的最终目的是帮助用户弄清楚把哪几种油漆混合到一起。

So, to make the volume model logically consistent would make it unsuitable for its application requirements. There seems to be a dilemma. Are we stuck with documenting the weird post-condition and trying to compensate with good communication? Not everything in this world is intuitive, and sometimes that is the best answer. But in this case, the awkwardness seems to point to missing concepts. Let’s look for a new model.

因此,如果要使体积模型的逻辑保持一致,那么它就无法满足这个应用程序的需求了。这看上去是一种进退两难的境况。我们是否仍使用这个不合常理的后置条件,并为了弥补这个不足而清楚地说明这样做的理由呢?世界上并不是一切事物都是直观的,有时那就是最好的答案。但在这个例子中,这种尴尬局面似乎是由于丢失概念而造成的。让我们去寻找一个新的模型。

We Can See Clearly Now

寻找更清晰的模型

As we search for a better model, we have significant advantages over the original designers, because of the knowledge crunching and refactoring to deeper insight that has happened in the interim. For example, we compute color using a SIDE-EFFECT-FREE FUNCTION on a VALUE OBJECT. This means we can repeat the calculation any time we need to. We should take advantage of that.

我们在寻找更好的模型的时候,会比原来的设计人员更有优势,因为我们在研究的过程中消化了更多知识,而且通过重构得到了更深层的理解。例如,我们用一个 VALUE OBJECT 上的 SIDE-EFFECT-FREE FUNCTION 来计算颜色。这意味着可以在任何需要的时候重复进行这个计算。我们应该利用这种优势。

We seem to be giving Paint two different basic responsibilities. Let’s try splitting them.

我们似乎为 Paint 分配了两种不同的基本职责。让我们试着把它们分开。

Now there is only one command, mixIn(). It just adds an object to a collection, an effect apparent from an intuitive understanding of the model. All other operations are SIDE-EFFECT-FREE FUNCTIONS.

现在只有一个命令,即 mixIn()。从对模型的直观理解可以看出,它只是把一个对象加入到一个集合中。所有其他操作都是 SIDE-EFFECT-FREE FUNCTION。

A test method confirming one of the ASSERTIONS listed in Figure 10.10 could look something like this (using the JUnit test framework):

下面的测试方法(使用了 JUnit 测试框架)用来确认图 10-10 中列出的一个 ASSERTION 是否满足:

public void testMixingVolume {
   PigmentColor yellow = new PigmentColor(0, 50, 0);
   PigmentColor blue = new PigmentColor(0, 0, 50);

   StockPaint paint1 = new StockPaint(1.0, yellow);
   StockPaint paint2 = new StockPaint(1.5, blue);
   MixedPaint mix = new MixedPaint();

   mix.mixIn(paint1);
   mix.mixIn(paint2);
   assertEquals(2.5, mix.getVolume(), 0.01);
}

This model captures and communicates more of the domain. The invariants and post-conditions make common sense, which will make them easier to maintain and use.

这个模型捕捉并传递了更多领域知识。固定规则和后置条件符合常识,这使得它们更易于维护和使用。


The communicativeness of the INTENTION-REVEALING INTERFACES, combined with the predictability given by SIDE-EFFECT-FREE FUNCTIONS and ASSERTIONS, should make encapsulation and abstraction safe.

INTENTION-REVEALING INTERFACE 清楚地表明了用途,SIDE-EFFECT-FREE FUNCTION 和 ASSERTION 使我们能够更准确地预测结果,因此封装和抽象更加安全。

The next ingredient in recombinable elements is effective decomposition. . . .

可重组元素的下一个因素是有效的分解……

10.4 CONCEPTUAL CONTOURS 模式:CONCEPTUAL CONTOUR

Sometimes people chop functionality fine to allow flexible combination. Sometimes they lump it large to encapsulate complexity. Sometimes they seek a consistent granularity, making all classes and operations to a similar scale. These are oversimplifications that don’t work well as general rules. But they are motivated by a basic set of problems.

有时,人们会对功能进行更细的分解,以便灵活地组合它们,有时却要把功能合成大块,以便封装复杂性。有时,人们为了使所有类和操作都具有相似的规模而寻找一种一致的粒度。这些方法都过于简单了,并不能作为通用的规则。但使用这些方法的动机都来自于一系列基本的问题。

When elements of a model or design are embedded in a monolithic construct, their functionality gets duplicated. The external interface doesn’t say everything a client might care about. Their meaning is hard to understand, because different concepts are mixed together.

如果把模型或设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复。外部接口无法给出客户可能关心的全部信息。由于不同的概念被混合在一起,它们的意义变得很难理解。

On the other hand, breaking down classes and methods can pointlessly complicate the client, forcing client objects to understand how tiny pieces fit together. Worse, a concept can be lost completely. Half of a uranium atom is not uranium. And of course, it isn’t just grain size that counts, but just where the grain runs.

而另一方面,把类和方法分解开也可能是毫无意义的,这会使客户更复杂,迫使客户对象去理解各个细微部分是如何组合在一起的。更糟的是,有的概念可能会完全丢失。铀原子的一半并不是铀。而且,粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。

Cookbook rules don’t work. But there is a logical consistency deep in most domains, or else they would not be viable in their own sphere. This is not to say that domains are perfectly consistent, and certainly the ways people talk about them are not consistent. But there is rhyme and reason somewhere, or else modeling would be pointless. Because of this underlying consistency, when we find a model that resonates with some part of the domain, it is more likely to be consistent with other parts that we discover later. Sometimes the new discovery isn’t easy for the model to adapt to, in which case we refactor to deeper insight, and hope to conform to the next discovery.

菜谱式的规则是没有用的。但大部分领域都深深隐含着某种逻辑一致性,否则它们就形不成领域了。这并不是说领域就是绝对一致的,而且人们讨论领域的方式肯定也不一样。但是领域中一定存在着某种十分复杂的原理,否则建模也就失去了意义。由于这种隐藏在底层的一致性,当我们找到一个模型,它与领域的某个部分特别吻合时,这个模型很可能也会与我们后续发现的这个领域的其他部分一致。有时,新的发现可能与模型不符,在这种情况下,就需要对模型进行重构,以便获取更深层的理解,并希望下一次新发现能与模型一致。

This is one reason why repeated refactoring eventually leads to suppleness. The CONCEPTUAL CONTOURS emerge as the code is adapted to newly understood concepts or requirements.

通过反复重构最终会实现柔性设计,以上就是其中的一个原因。随着代码不断适应新理解的概念或需求,CONCEPTUAL CONTOUR(概念轮廓)也就逐渐形成了。

The twin fundamentals of high cohesion and low coupling play a role in design at all scales, from individual methods up through classes and MODULES to large-scale structures (see Chapter 16). These two principles apply to concepts as much as to code. To avoid slipping into a mechanistic view of them, temper your technical thinking by frequently touching base with your intuition for the domain. With each decision, ask yourself, “Is this an expedient based on a particular set of relationships in the current model and code, or does it echo some contour of the underlying domain?”

从单个方法的设计,到类和 MODULE 的设计,再到大型结构的设计(参见第 16 章),高内聚低耦合这一对基本原则都起着重要的作用。这两条原则既适用于代码,也适用于概念。为了避免机械化地遵循它,我们必须经常根据我们对领域的直观认识来调整技术思路。在做每个决定时,都要问自己:“这是根据当前模型和代码中的特定关系做出的权宜之计呢,还是反映了底层领域的某种轮廓?”

Find the conceptually meaningful unit of functionality, and the resulting design will be both flexible and understandable. For example, if an “addition” of two objects has a coherent meaning in the domain, then implement methods at that level. Don’t break the add() into two steps. Don’t proceed to the next step within the same operation. On a slightly larger scale, each object should be a single complete concept, a “WHOLE VALUE.”1

寻找在概念上有意义的功能单元,这样可以使得设计既灵活又易懂。例如,如果领域中对两个对象的“相加”(addition)是一个连贯的整体操作,那么就把它作为整体来实现。不要把[插图]。

By the same token, there are areas in any domain where detail isn’t interesting to the kind of people the software serves. The users of our hypothetical paint mixing application don’t add red pigment or blue pigment; they combine complete paints, which contain all three pigments. Clumping things that don’t need to be dissected or rearranged avoids clutter and makes it easier to see the elements that really are meant to recombine. If our users’ physical equipment allowed individual pigments to be added, the domain would be altered, and the individual pigments might be manipulated. A paint chemist would need still finer control, which would involve a whole other analysis, probably producing a much more detailed model of the makeup of paint than our abstracted pigment color that serves paint mixing. But it is simply irrelevant to anyone involved in the paint mixing application project.

出于同样的原因,在任何领域中,都有一些细节是用户不感兴趣的。前面假想的那个调漆应用程序的用户不会添加红色颜料或蓝色颜料,他们只是把已经做好的油漆拿来调,而油漆包含所有 3 种颜料。把那些没必要分解或重组的元素作为一个整体,这样可以避免混乱,并且使人们更容易看到那些真正需要重组的元素。如果用户的物理设备允许加入颜料,那么领域就改变了,而且我们可能需要分别对每种颜料进行控制。专门研究油漆的化学家将需要更精细的控制,这就需要进行完全不同的分析了,有可能会产生一个比我们的调漆应用程序中的颜料颜色更精细的油漆构成模型。但是这些与我们的调漆应用程序项目中的任何人都无关。

Therefore:

因此:

Decompose design elements (operations, interfaces, classes, and AGGREGATES) into cohesive units, taking into consideration your intuition of the important divisions in the domain. Observe the axes of change and stability through successive refactorings and look for the underlying CONCEPTUAL CONTOURS that explain these shearing patterns. Align the model with the consistent aspects of the domain that make it a viable area of knowledge in the first place.

把设计元素(操作、接口、类和 AGGREGATE)分解为内聚的单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层 CONCEPTUAL CONTOUR。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。

The goal is a simple set of interfaces that combine logically to make sensible statements in the UBIQUITOUS LANGUAGE, and without the distraction and maintenance burden of irrelevant options. This is typically an outcome of refactoring: it’s hard to produce up front. But it may never emerge from technically oriented refactoring; it emerges from refactoring toward deeper insight.

我们的目标是得到一组可以在逻辑上组合起来的简单接口,使我们可以用 UBIQUITOUS LANGUAGE 进行合理的表述,并且使那些无关的选项不会分散我们的注意力,也不增加维护负担。但这通常是通过重构才能得到的结果,很难在前期就实现。而且如果仅仅是从技术角度进行重构,可能永远也不会出现这种结果;只有通过重构得到更深层的理解,才能实现这样的目标。

Even when the design follows CONCEPTUAL CONTOURS, there will need to be modifications and refactoring. When successive refactoring tends to be localized, not shaking multiple broad concepts of the model, it is an indicator of model fit. Encountering a requirement that forces extensive changes in the breakdown of the objects and methods is a message: Our understanding of the domain needs refinement. It presents an opportunity to deepen the model and make the design more supple.

设计即使是按照 CONCEPTUAL CONTOUR 进行,也仍然需要修改和重构。当连续的重构往往只是做出一些局部修改(而不是对模型的概念产生大范围的影响)时,这就是模型已经与领域相吻合的信号。如果遇到了一个需求,它要求我们必须大幅度地修改对象和方法的划分,那么这就在向我们传递这样一条信息:我们对领域的理解还需要精化。它提供了一个深化模型并且使设计变得更具柔性的机会。

Example: The CONTOURS of Accruals

示例应计项目的 CONCEPTUAL CONTOUR

In Chapter 9, a loan tracking system was refactored based on deeper insight into accounting concepts:

在第 9 章中,基于对会计概念的更深层理解,我们对一个贷款跟踪系统进行了重构,如图 10-11 所示。

The new model contained only one more object than the old one, yet the partitioning of responsibility had been greatly changed.

新模型比原来的模型只多出一个对象,但职责的划分却发生了很大的变化。

Schedules, which had been worked out through case logic in the Calculator classes, were exploded into discrete classes for different types of fees and interest. On the other hand, payments of fees and interest, previously kept separate, were lumped together.

Schedule 原来是在 Calculator 类中通过逻辑判断计算的,现在被分散到不同的类中,用于不同类型的手续费和利息计算。另一方面,手续费和利息的支付原来是分开的,现在也被合并到一起了。

Because of the resonance of the newly explicit concepts and the cohesiveness of the Accrual Schedule hierarchy, the developer believed that this model better follows some of the domain’s CONCEPTUAL CONTOURS.

由于新发现的显式概念与领域非常吻合,而且 Accrual Schedule 的层次结构具有内聚性,因此开发人员认为这个模型更符合领域的 CONCEPTUAL CONTOUR,如图 10-12 所示。

This model accommodates adding new kinds of Accrual Schedules.

The one change the developer could confidently predict was the addition of new Accrual Schedules. Those requirements were already waiting in the wings. So in addition to making existing functionality clearer and simpler, she chose a model that would make it easy to introduce new schedules. But had she found a CONCEPTUAL CONTOUR that will help the domain design change and grow as the application and the business evolve? There can be no guarantees about how a design will handle unanticipated change, but she thought it had improved the odds.

新的 Accrual Schedule 的加入是开发人员早就预料到的,因为有一些需求早已等待它来处理了。这样,她选择的模型除了使现有功能更清晰、简单之外,还很容易引入新的 Schedule。但是,她是否找到了一个 CONCEPTUAL CONTOUR,使得领域设计可以随着应用程序和业务的演变而改变和发展呢?我们无法确定一个设计如何处理意料之外的改变,但她认为她的设计中一些不合适的地方已经有所改进了。

An Unanticipated Change

一个未预料到的改变

As the project proceeded, a requirement emerged for detailed rules for handling early and late payments. As she studied the problem, the developer was pleased to see that virtually the same rules applied to payments on interest and to payments on fees. This meant that the new model elements would connect naturally to the single Payment class.

随着项目向前进展,又出现了一个新的需求——需要制定一些详细的规则来处理提早付款和延迟付款。这位开发人员在研究问题的时候,很高兴地发现利息付款和手续费付款实际上使用相同的规则。这意味着新的模型元素可以很自然地使用 Payment 类。

The old design would have forced duplication between the two Payment History classes. (This difficulty might have triggered an insight that the Payment class should be shared, leading by another path to a similar model.) This ease of extension did not come because she anticipated the change. Nor did it come because she made a design so versatile it could accommodate any conceivable change. It happened because in the previous refactoring, the design was aligned with underlying concepts of the domain.

原有的设计导致两个 Payment History 类之间必然出现重复(这个难题可能使得开发人员认识到 Payment 类应该被共享,这样就会从另外一条途径得到类似的模型)。新元素之所以很容易就添加进来了,并不是因为她预料到了这个改变,也不是因为她的设计灵活到了足以容纳任何可能修改的程度。真正的原因是经过前面的重构,设计能够很好地与领域的基本概念相契合。


INTENTION-REVEALING INTERFACES allow clients to present objects as units of meaning rather than just mechanisms. SIDE-EFFECT-FREE FUNCTIONS and ASSERTIONS make it safe to use those units and make complex combinations. The emergence of CONCEPTUAL CONTOURS stabilizes parts of the model and also makes the units more intuitive to use and combine.

INTENTION-REVEALING INTERFACE 使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。SIDE-EFFECT-FREE FUNCTION 和 ASSERTION 使我们可以安全地使用这些单元,并对它们进行复杂的组合。CONCEPTUAL CONTOUR 的出现使模型的各个部分变得更稳定,也使得这些单元更直观,更易于使用和组合。

We can still run into conceptual overload when interdependencies force us to think about too many of these things at a time. . . .

然而,我们仍然会遇到“概念过载”(conceptual overload)的问题——当模型中的互相依赖过多时,我们就必须把大量问题放在一起考虑。

10.5 STANDALONE CLASSES 模式:STANDALONE CLASS

Interdependencies make models and designs hard to understand. They also make them hard to test and maintain. And interdependencies pile up easily.

互相依赖使模型和设计变得难以理解、测试和维护。而且,互相依赖很容易越积越多。

Every association is, of course, a dependency, and understanding a class requires understanding what it is attached to. Those attached things will be attached to still more things, and they have to be understood too. The type of every argument of every method is also a dependency. So is every return value.

当然,每个关联都是一种依赖,要想理解一个类,必须理解它与哪些对象有联系。与这个类有联系的其他对象还会与更多的对象发生联系,而这些联系也是必须要弄清楚的。每个方法的每个参数的类型也是一个依赖,每个返回值也都是一个依赖。

With one dependency, you have to think about two classes at the same time, and the nature of their relationship. With two dependencies, you have to think about each of the three classes, the nature of the class’s relationship to each of them, and any relationship they might have to each other. If they in turn have dependencies, you have to be wary of those also. With three dependencies . . . it snowballs.

如果有一个依赖关系,我们必须同时考虑两个类以及它们之间关系的本质。如果某个类依赖另外两个类,我们就必须考虑这 3 个类当中的每一个、这个类与其他两个类之间的相互关系的本质,以及这 3 个类可能存在的其他相互关系。如果它们之间依次存在依赖关系,那么我们还必须考虑这些关系。如果一个类有 3 个依赖关系……问题就会像滚雪球一样越来越多。

Both MODULES and AGGREGATES are aimed at limiting the web of interdependencies. When a highly cohesive subdomain is carved out into a MODULE, a set of objects are decoupled from the rest of the system, so there are a finite number of interrelated concepts. But even a MODULE can be a lot to think about without an almost fanatical commitment to controlling dependencies within it.

MODULE 和 AGGREGATE 的目的都是为了限制互相依赖的关系网。当我们识别出一个高度内聚的子领域并把它提取到一个 MODULE 中的时候,一组对象也随之与系统的其他部分解除了联系,这样就把互相联系的概念的数量控制在一个有限的范围之内。但是,即使把系统分成了各个 MODULE,如果不严格控制 MODULE 内部的依赖的话,那么 MODULE 也一样会让我们耗费很多精力去考虑依赖关系。

Even within a MODULE, the difficulty of interpreting a design increases wildly as dependencies are added. This adds to mental overload, limiting the design complexity a developer can handle. Implicit concepts contribute to this load even more than explicit references.

即使是在 MODULE 内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担更大。

Refined models are distilled until every remaining connection between concepts represents something fundamental to the meaning of those concepts. In an important subset, the number of dependencies can be reduced to zero, resulting in a class that can be fully understood all by itself, along with a few primitives and basic library concepts.

我们可以将模型一直精炼下去,直到每个剩下的概念关系都表示出概念的基本含义为止。在一个重要的子集中,依赖关系的个数可以减小到零,这样就得到一个完全独立的类,它只有很少的几个基本类型和基础库概念。

In every programming environment, a few basics are so pervasive that they are always in mind. For example, in Java development, primitives and a few standard libraries provide basics like numbers, strings, and collections. Practically speaking, “integers” don’t add to the intellectual load. Beyond that, every additional concept that has to be held in mind in order to understand an object contributes to mental overload.

在每种编程环境中,都有一些非常基本的概念,它们经常用到,以至于已经根植于我们的大脑中。例如,在 Java 开发环境中,基本类型和一些标准类库提供了数字、字符串和集合等基本概念。从实际来讲,“整数”这个概念是不会增加思考负担的。除此之外,为了理解一个对象而必须保留在大脑中的其他概念都会增加思考负担。

Implicit concepts, recognized or unrecognized, count just as much as explicit references. Although we can generally ignore dependencies on primitive values such as integers and strings, we can’t ignore what they represent. For example, in the first paint mixing examples, the Paint object held three public integers representing red, yellow, and blue color values. The creation of the Pigment Color object did not increase the number of concepts involved or the dependencies. It did make the ones that were already there more explicit and easier to understand. On the other hand, the Collection size() operation returns an int that is simply a count, the basic meaning of an integer, so no new concept is implied.

隐式概念,无论是否已被识别出来,都与显式引用一样会加重思考负担。虽然我们通常可以忽略像整数和字符串这样的基本类型值,但无法忽略它们所表示的意义。例如,在第一个调漆应用程序的例子中,Paint 对象包含 3 个公共的整数,分别表示红、黄、蓝 3 种颜色值。Pigment Color 对象的创建并没有增加所涉及的概念数量,也没有增加依赖关系。但它确实使现有概念更明晰、更易于理解了。另一方面,Collection 的 size() 操作返回一个整数(只是一个简单的合计数),它只表示整数的基本含义,因此并不产生隐式的新概念。

Every dependency is suspect until proven basic to the concept behind the object. This scrutiny starts with the factoring of the model concepts themselves. Then it requires attention to each individual association and operation. Model and design choices can chip away at dependencies—often to zero.

我们应该对每个依赖关系提出质疑,直到证实它确实表示对象的基本概念为止。这个仔细检查依赖关系的过程从提取模型概念本身开始。然后需要注意每个独立的关联和操作。仔细选择模型和设计能够大幅减少依赖关系——常常能减少到零。

Low coupling is fundamental to object design. When you can, go all the way. Eliminate all other concepts from the picture. Then the class will be completely self-contained and can be studied and understood alone. Every such self-contained class significantly eases the burden of understanding a MODULE.

低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解 MODULE 而带来的负担。

Dependencies on other classes within the same module are less harmful than those outside. Likewise, when two objects are naturally tightly coupled, multiple operations involving the same pair can actually clarify the nature of the relationship. The goal is not to eliminate all dependencies, but to eliminate all nonessential ones. If every dependency can’t be eliminated, each one that is removed frees the developer to concentrate on the remaining conceptual dependencies.

当一个类与它所在的模块中的其他类存在依赖关系时,比它与模块外部的类有依赖关系要好得多。同样,当两个对象具有自然的紧密耦合关系时,这两个对象共同涉及的多个操作实际上能够把它们的关系本质明确地表示出来。我们的目标不是消除所有依赖,而是消除所有不重要的依赖。当无法消除所有的依赖关系时,每清除一个依赖对开发人员而言都是一种解脱,使他们能够集中精力处理剩下的概念依赖关系。

Try to factor the most intricate computations into STANDALONE CLASSES, perhaps by modeling VALUE OBJECTS held by the more connected classes.

尽力把最复杂的计算提取到 STANDALONE CLASS(独立的类)中,实现此目的的一种方法是从存在大量依赖的类中将 VALUE OBJECT 建模出来。

The concept of paint is fundamentally related to the concept of color. But color, even of pigment, can be considered without paint. By making these two concepts explicit and distilling the relationship, the remaining one-way association says something important, and the Pigment Color class, where most of the computational complexity lies, can be studied and tested alone.

从根本上讲,油漆的概念与颜色的概念紧密相关。但在考虑颜色(甚至是颜料)的时候却与不必去考虑油漆。通过把这两个概念变为显式概念并精炼它们的关系,所得到的单向关联就可以表达出重要的信息,同时我们可以对 Pigment Color 类(大部分计算复杂性都隐藏在这个类中)进行独立的分析和测试。


Low coupling is a basic way to reduce conceptual overload. A STANDALONE CLASS is an extreme of low coupling.

低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。

Eliminating dependencies should not mean dumbing down the model by arbitrarily reducing everything to primitives. The final pattern of this chapter, CLOSURE OF OPERATIONS, is an example of a technique for reducing dependency while keeping a rich interface. . . .

消除依赖性并不是说要武断地把模型中的一切都简化为基本类型,这样只会削弱模型的表达能力。本章要讨论的最后一个模式 CLOSURE OF OPERATION(闭合操作)就是一种在减小依赖性的同时保持丰富接口的技术。

10.6 CLOSURE OF OPERATIONS 模式:CLOSURE OF OPERATION

If we take two real numbers and multiply them together, we get another real number. [The real numbers are all the rational numbers and all the irrational numbers.] Because this is always true, we say that the real numbers are “closed under the operation of multiplication”: there is no way to escape the set. When you combine any two elements of the set, the result is also included in the set.

—The Math Forum, Drexel University

两个实数相乘,结果仍为实数(实数是所有有理数和所有无理数的集合)。由于这一点永远成立,因此我们说实数的“乘法运算是闭合的”:乘法运算的结果永远无法脱离实数这个集合。当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。

——The Math Forum, Drexel University

Of course, there will be dependencies, and that isn’t a bad thing when the dependency is fundamental to the concept. Stripping interfaces down to deal with nothing but primitives can impoverish them. But a lot of unnecessary dependencies, and even entire concepts, get introduced at interfaces.

当然,依赖是必然存在的,当依赖是概念的一个基本属性时,它就不是坏事。如果把接口精简到只处理一些基本类型,那么会极大地削弱接口的能力。但我们也经常为接口引入很多不必要的依赖,甚至是整个不必要的概念。

Most interesting objects end up doing things that can’t be characterized by primitives alone.

大部分引起我们兴趣的对象所产生的行为仅用基本类型是无法描述的。

Another common practice in refined designs is what I call “CLOSURE OF OPERATIONS.” The name comes from that most refined of conceptual systems, mathematics. 1 + 1 = 2. The addition operation is closed under the set of real numbers. Mathematicians are fanatical about not introducing extraneous concepts, and the property of closure provides them a way of defining an operation without involving any other concepts. We are so accustomed to the refinement of mathematics that it can be hard to grasp how powerful its little tricks are. But this one is used extensively in software designs as well. The basic use of XSLT is to transform one XML document into another XML document. This sort of XSLT operation is closed under the set of XML documents. The property of closure tremendously simplifies the interpretation of an operation, and it is easy to think about chaining together or combining closed operations.

另一种对设计进行精化的常见方法就是我所说的 CLOSURE OF OPERATION(闭合操作)。这个名字来源于最精炼的概念体系,即数学。1 + 1 = 2。加法运算是实数集中的闭合运算。数学家们都极力避免去引入无关的概念,而闭合运算的性质正好为他们提供了这样一种方式,可用来定义一种不涉及其他任何概念的运算。我们都非常熟悉数学中的精炼,因此很难注意到一些小技巧会有多么强大。但是,这些技巧在软件设计中也广为应用。例如,XSLT 的基本用法是把一个 XML 文档转换为另一个 XML 文档。这种 XSLT 操作就是 XML 文档集合中的闭合操作。闭合的性质极大地简化了对操作的理解,而且闭合操作的链接或组合也很容易理解。

Therefore:

因此:

Where it fits, define an operation whose return type is the same as the type of its argument(s). If the implementer has state that is used in the computation, then the implementer is effectively an argument of the operation, so the argument(s) and return value should be of the same type as the implementer. Such an operation is closed under the set of instances of that type. A closed operation provides a high-level interface without introducing any dependency on other concepts.

在适当的情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。

This pattern is most often applied to the operations of a VALUE OBJECT. Because the life cycle of an ENTITY has significance in the domain, you can’t just conjure up a new one to answer a question. There are operations that are closed under an ENTITY type. You could ask an Employee object for its supervisor and get back another Employee. But in general, ENTITIES are not the sort of concepts that are likely to be the result of a computation. So, for the most part, this is an opportunity to look for in the VALUE OBJECTS.

这种模式更常用于 VALUE OBJECT 的操作。由于 ENTITY 的生命周期在领域中十分重要,因此我们不能为了解决某一问题而草率创建一个 ENTITY。有一些操作是 ENTITY 类型之下的闭合操作。我们可以通过查询一个 Employee(员工)对象来返回其主管,而返回的将是另一个 Employee 对象。但是,ENTITY 通常不会成为计算结果。因此,大部分闭合操作都应该到 VALUE OBJECT 中去寻找。

An operation can be closed under an abstract type, in which case specific arguments can be of different concrete classes. After all, addition is closed under real numbers, which can be either rational or irrational.

一个操作可能是在某一抽象类型之下的闭合操作,在这种情况下,具体的参数可能是不同的具体类型。例如,加法是实数之下的闭合运算,而实数既可以是有理数,也可以是无理数。

As you’re experimenting, looking for ways to reduce interdependence and increase cohesion, you sometimes get halfway to this pattern. The argument matches the implementer, but the return type is different, or the return type matches the receiver and the argument is different. These operations are not closed, but they do give some of the advantages of CLOSURE. When the extra type is a primitive or basic library class, it frees the mind almost as much as CLOSURE.

在尝试和寻找减少互相依赖并提高内聚的过程中,有时我们会遇到“半个闭合操作”这种情况。参数类型与实现者的类型一致,但返回类型不同;或者返回类型与接收者(receiver)的类型相同但参数类型不同。这些操作都不是闭合操作,但它们确实具有 CLOSURE OF OPERATION 的某些优点。当没有形成闭合操作的那个多出来的类型是基本类型或基础库类时,它几乎与 CLOSURE OF OPERATION 一样减轻了我们的思考负担。

In the earlier example, the Pigment Color mixedWith() operation was closed under Pigment Colors, and there are several other examples scattered through the book. Here’s an example that shows how useful this idea can be, even when true CLOSURE isn’t reached.

在前面的示例中,Pigment Color 的 mixedWith() 操作是 Pigment Color 之下的闭合操作,本书中还零星地穿插着几个这样的示例。以下示例显示了即使在没有达到真正 CLOSURE OFOPERATION 的时候,这种思想也发挥了强大的作用。

Example: Selecting from Collections

示例从集合中选择子集

In Java, if you want to select a subset of elements from a Collection, you request an Iterator. Then you iterate through the elements, testing each one, probably accumulating the matches into a new Collection.

在 Java 中,如果想从 Collection(集合)中选择一个元素子集,需要使用 Iterator(迭代器)。用迭代器遍历这些元素,测试每个元素,把匹配的元素收集到一个新的 Collection 中。

Set employees = (some Set of Employee objects);
Set lowPaidEmployees = new HashSet();
Iterator it = employees.iterator();
while (it.hasNext()) {
   Employee anEmployee = it.next();
   if (anEmployee.salary() < 40000)
lowPaidEmployees.add(anEmployee);
}

Conceptually, I’ve selected a subset of a set. What do I need with this extra concept, Iterator, and all its mechanical complexity? In Smalltalk, I would call the “select” operation on the Collection, passing in the test as an argument. The return would be a new Collection containing just the elements that passed the test.

从概念上讲,上段代码只是从集合中选择了一个子集。是否真的有必要使用 Iterator 这个额外的概念以及它所带来的所有机制上的复杂性呢?如果是使用 Smalltalk,我将在 Collection 上调用“select”操作,把测试作为参数传递给它。返回值将是一个新的 Collection,其中只包含通过测试的那些元素。

employees := (some Set of Employee objects).
lowPaidEmployees := employees select:
         [:anEmployee | anEmployee salary < 40000].

The Smalltalk Collections provide other such FUNCTIONS that return derived Collections, which can be of several concrete classes. The operations are not closed, because they take a “block” as an argument. But blocks are a basic library type in Smalltalk, so they don’t add to the developer’s mental load. Because the return value matches the implementer, they can be strung together, like a series of filters. They are easy to write and easy to read. They do not introduce extraneous concepts that are irrelevant to the problem of selecting subsets.

Smalltalk 的 Collection 还提供了其他一些这样的函数,它们返回新生成的 Collection(可能是几种不同的具体类)。这些操作并不是闭合操作,因为它们把一个 block(块)作为参数。但 block 在 Smalltalk 中是一个基础库类型,因此它们并不会增加开发人员的思考负担。由于返回值与实现者的类型相匹配,因此它们可以像一系列过滤器一样被串接在一起。读写代码都变得很容易。它们并没有引入与选择子集无关的外来概念。


The patterns presented in this chapter illustrate a general style of design and a way of thinking about design. Making software obvious, predictable, and communicative makes abstraction and encapsulation effective. Models can be factored so that objects are simple to use and understand yet still have rich, high-level interfaces.

本章介绍的模式演示了一个通用的设计风格和一种思考设计的方式。把软件设计得意图明显、容易预测且富有表达力,可以有效地发挥抽象和封装的作用。我们可以对模型进行分解,使得对象更易于理解和使用,同时仍具有功能丰富的、高级的接口。

These techniques require fairly advanced design skills to apply and sometimes even to write a client. The usefulness of a MODEL-DRIVEN DESIGN is sensitive to the quality of the detailed design and implementation decisions, and it only takes a few confused developers to derail a project from the goal.

运用这些技术需要掌握相当高级的设计技巧,甚至有时编写客户端代码也需要掌握高级技巧才能运用这些技术。MODEL-DRIVEN DESIGN 的作用受细节设计的质量和实现决策的质量影响很大,而且只要有少数几个开发人员没有弄清楚它们,整个项目就会偏离目标。

That said, for the team willing to cultivate its modeling and design skills, these patterns and the way of thinking they reflect yield software that developers can work and rework to create complex software.

尽管如此,团队只要愿意培养这些建模和设计技巧,那么按照这些模式的思考方式就能够开发出可以反复重构的软件,从而最终创建出非常复杂的软件。

10.7 DECLARATIVE DESIGN 声明式设计

ASSERTIONS can lead to much better designs, even with our relatively informal way of testing them. But there can be no real guarantees in handwritten software. To name just one way of evading ASSERTIONS, code could have additional side effects that were not specifically excluded. No matter how MODEL-DRIVEN our design is, we still end up writing procedures to produce the effect of the conceptual interactions. And we spend so much of our time writing boilerplate code that doesn’t really add any meaning or behavior. This is tedious and fraught with error, and the bulk of it obscures the meaning of our model. (Some languages are better than others, but all require us to do a lot of grunt work.) INTENTION-REVEALING INTERFACES and the other patterns in this chapter help, but they can never give conventional object-oriented programs formal rigor.

使用 ASSERTION 可以得到更好的设计,虽然我们只是用一些相对非正式的方式来检查这些 ASSERTION。但实际上我们无法保证手写软件的正确性。举个简单例子,只要代码还有其他一些没有被 ASSERTION 专门排除在外的副作用,断言就失去了作用。无论我们的设计多么遵守 MODEL-DRIVEN 开发方法,最后仍要通过编写过程代码来实现概念交互的结果。而且我们花费了大量时间来编写样板代码,但是这些代码实际上不增加任何意义或行为。这些代码冗长乏味而且易出错,此外还掩盖了模型的意义(虽然有的编程语言会相对好一些,但都需要我们做大量繁琐的工作)。本章介绍的 INTENTION-REVEALING INTERFACE 和其他模式虽然有一定的帮助作用,但它们永远也不会使传统的面向对象技术达到非常严密的程度。

These are some of the motivations behind declarative design. This term means many things to many people, but usually it indicates a way to write a program, or some part of a program, as a kind of executable specification. A very precise description of properties actually controls the software. In its various forms, this could be done through a reflection mechanism or at compile time through code generation (producing conventional code automatically, based on the declaration). This approach allows another developer to take the declaration at face value. It is an absolute guarantee.

以上这些正是采用声明式设计的部分动机。声明式设计对于不同的人来说具有不同的意义,但通常是指一种编程方式——把程序或程序的一部分写成一种可执行的规格(specification)。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。声明式设计有多种实现方式,例如,可以通过反射机制来实现,或在编译时通过代码生成来实现(根据声明来自动生成传统代码)。这种方法使其他开发人员能够根据字面意义来使用声明。它是一种绝对的保证。

Generating a running program from a declaration of model properties is a kind of Holy Grail of MODEL-DRIVEN DESIGN, but it does have its pitfalls in practice. For example, here are just two particular problems I’ve encountered more than once.

从模型属性的声明来生成可运行的程序是 MODEL-DRIVEN DESIGN 的理想目标,但在实践中这种方法也有自己的缺陷。例如,下面就是我多次遇到的两个具体问题:

  • A declaration language not expressive enough to do everything needed, but a framework that makes it very difficult to extend the software beyond the automated portion
  • Code-generation techniques that cripple the iterative cycle by merging generated code into handwritten code in a way that makes regeneration very destructive

  • 声明式语言并不足以表达一切所需的东西,它把软件束缚在一个由自动部分构成的框架之内,使软件很难扩展到这个框架之外。
  • 代码生成技术破坏了迭代循环——它把生成的代码合并到手写的代码中,使得代码重新生成具有巨大的破坏作用。

The unintended consequence of many attempts at declarative design is the dumbing-down of the model and application, as developers, trapped by the limitations of the framework, enact design triage in order to get something delivered.

许多声明式设计的尝试带来了意想不到的后果,由于开发人员受到框架局限性的约束,为了交付工作只能先处理重要问题,而搁置其他一些问题,这导致模型和应用程序的质量严重下降。

Rule-based programming with an inference engine and a rule base is another promising approach to declarative design. Unfortunately, subtle issues can undermine this intention.

基于规则的编程(带有推理引擎和规则库)是另一种有望实现的声明式设计方法。但遗憾的是,一些微妙的问题会影响它的实现。

Although a rules-based program is declarative in principle, most systems have “control predicates” that were added to allow performance tuning. This control code introduces side effects, so that the behavior is no longer dictated completely by the declared rules. Adding, removing, or reordering the rules can cause unexpected, incorrect results. Therefore, a logic programmer has to be careful to keep the effect of code obvious, just as an object programmer does.

尽管基于规则的程序原则上是声明式的,但大多数系统都有一些用于性能优化的“控制谓词”(control predicate)。这种控制代码引入了副作用,这样行为就不再完全由声明式规则来控制了。添加、删除规则或重新排序可能导致预料不到的错误结果。因此,编写逻辑的程序员必须确保代码的效果是显而易见的,就像对象程序员所做的那样。

Many declarative approaches can be corrupted if the developers bypass them intentionally or unintentionally. This is likely when the system is difficult to use or overly restrictive. Everyone has to follow the rules of the framework in order to get the benefits of a declarative program.

很多声明式方法被开发人员有意或无意忽略之后会遭到破坏。当系统很难使用或限制过多时,就会发生这种情况。为了获得声明式程序的好处,每个人都必须遵守框架的规则。

The greatest value I’ve seen delivered has been when a narrowly scoped framework automates a particularly tedious and error-prone aspect of the design, such as persistence and object-relational mapping. The best of these unburden developers of drudge work while leaving them complete freedom to design.

据我所知,声明式设计发挥的最大价值是用一个范围非常窄的框架来自动处理设计中某个特别单调且易出错的方面,如持久化和对象关系映射。最好的声明式设计能够使开发人员不必去做那些单调乏味的工作,同时又完全不限制他们的设计自由。

Domain-Specific Languages

领域特定语言

An interesting approach that is sometimes declarative is the domain-specific language. In this style, client code is written in a programming language tailored to a particular model of a particular domain. For example, a language for shipping systems might include terms such as cargo and route, along with syntax for associating them. The program is then compiled, often into a conventional object-oriented language, where a library of classes provides implementations for the terms in the language.

领域特定语言是一种有趣的方法,它有时也是一种声明式语言。采用这种编码风格时,客户代码是用一种专门为特定领域的特定模型定制的语言编写的。例如,运输系统的语言可能包括 cargo(货物)和 route(路线)这样的术语,以及一些用于组合这些术语的语法。然后,程序通常会被编译成传统的面向对象语言,由一个类库为这些术语提供实现。

In such a language, programs can be extremely expressive, and make the strongest connection with the UBIQUITOUS LANGUAGE. This is an exciting concept, but domain-specific languages also have their drawbacks in the approaches I’ve seen based on object-oriented technology.

在这样的语言中,程序可能具有极强的表达能力,并且与 UBIQUITOUS LANGUAGE 之间形成最紧密的结合。领域特定语言是一个令人振奋的概念,但就我所见,在基于面向对象技术进行实现时,这种语言也存在自身的缺陷。

To refine the model, a developer needs to be able to modify the language. This may involve modifying grammar declarations and other language-interpreting features, as well as modifying underlying class libraries. I’m all in favor of learning advanced technology and design concepts, but we have to soberly assess the skills of a particular team, as well as the likely skills of future maintenance teams. Also, there is value in the seamlessness of an application and a model implemented in the same language. Another drawback is that it can be difficult to refactor client code to conform to a revised model and its associated domain-specific language. Of course, someone may come up with a technical fix for the refactoring problems.

为了精化模型,开发人员需要修改语言。这可能涉及修改语法声明和其他语言解释功能,以及修改底层类库。虽然我对学习高级技术和设计概念是完全赞同的,但我们必须冷静地评估团队当前的技术水平,以及将来维护团队可能的技术水平。此外,用同一种语言实现的应用程序和模型之间是“无缝”的,这一点很有价值。另一个缺点是当模型被修改时,很难对客户代码进行重构,使之与修改之后的模型及与其相关的领域特定语言保持一致。当然,也许有人可以通过技术方法来解决重构问题。

From the Ground Up

一种完全不同的语言

A different paradigm might handle domain-specific languages better than objects. In the Scheme programming language, a representative of the “functional programming” family, something very similar is part of standard programming style, so that the expressiveness of a domain-specific language can be created without bifurcating the system.

有一种不同的范式能够比对象更好地实现领域特定语言。在 Scheme 编程语言中(它是“函数式编程”家族的一个代表),有些部分非常类似于标准的编程风格,因此既具有领域特定语言的表达能力,又不会造成系统的分裂。

This technique might be most useful for very mature models, perhaps where client code is being written by a different team. Generally, such setups lead to the poisonous distinction between highly technical framework builders and technically unskilled application builders, but it doesn’t have to be that way.

这种技术也许能在非常成熟的模型中发挥出最大的作用,在这种情况下,客户代码可能是由不同的团队编写的。但一般情况下,这样的设置会产生有害的结果——团队被分成两部分,框架由那些技术水平较高的人来构建,而应用程序则由那些技术水平较差的人来构建了,但也并不是非得如此。

In the scheme programming language, something very similar is part of standard programming style, so that the expressiveness of a domain-specific language can be created without bifurcating the system.

10.8 A DECLARATIVE STYLE OF DESIGN 声明式设计风格

Once your design has INTENTION-REVEALING INTERFACES, SIDE-EFFECT-FREE FUNCTIONS, and ASSERTIONS, you are edging into declarative territory. Many of the benefits of declarative design are obtained once you have combinable elements that communicate their meaning, and have characterized or obvious effects, or no observable effects at all.

一旦你的设计中有了 INTENTION-REVEALING INTERFACE、SIDE-EFFECT-FREE FUNCTION 和 ASSERTION,那么你就具备了使用声明式设计的条件。当我们有了可以组合在一起来表达意义的元素,并且使其作用具体化或明朗化,甚或是完全没有明显的副作用,我们就可以获得声明式设计的很多益处。

A supple design can make it possible for the client code to use a declarative style of design. To illustrate, the next section will bring together some of the patterns in this chapter to make the SPECIFICATION more supple and declarative.

柔性设计使得客户代码可以使用声明式的设计风格。为了说明这一点,下一节将会把本章介绍的一些模式结合起来使用,从而使 SPECIFICATION 更灵活,更符合声明式设计的风格。

Extending SPECIFICATIONS in a Declarative Style

用声明式的风格来扩展 SPECIFICATION

Chapter 9 covered the basic concept of SPECIFICATION, the roles it can play in a program, and some sense of what is involved in implementation. Now let’s take a look at a few bells and whistles that can be very useful in some situations with complicated rules.

第 9 章介绍了 SPECIFICATION 的基本概念、它在程序中扮演的角色,以及它在实现中的意义。现在,让我们来看看几个额外的、有吸引力的技巧,它们在规则很复杂的情况下可能非常有用。

SPECIFICATION is an adaptation of an established formalism, the predicate. Predicates have other useful properties that we can draw on, selectively.

SPECIFICATION 是由“谓词”(predicate)这个众所周知的形式化概念演变来的。谓词还有其他一些有用的特性,我们可以对这些特性进行有选择的利用。

Combining SPECIFICATIONS Using Logical Operators

使用逻辑运算对 SPECIFICATION 进行组合

When using SPECIFICATIONS, you quickly come across situations in which you would like to combine them. As just mentioned, a SPECIFICATION is an example of a predicate, and predicates can be combined and modified with the operations “AND,” “OR,” and “NOT.” These logical operations are closed under predicates, so SPECIFICATION combinations will exhibit CLOSURE OF OPERATIONS.

当使用 SPECIFICATION 时,我们很容易就会遇到需要把它们组合起来使用的情况。正如我们刚刚提到的那样,SPECIFICATION 是谓词的一个例子,而谓词可以用“AND”、“OR”和“NOT”等运算进行组合和修改。这些逻辑运算都是谓词这个类别之下的闭合操作,因此 SPECIFICATION 组合也是 CLOSURE OF OPERATION。

As significant generalized capability is built into SPECIFICATIONS, it becomes very useful to create an abstract class or interface that can be used for SPECIFICATIONS of all sorts. This means typing arguments as some high-level abstract class.

随着 SPECIFICATION 的通用性逐渐提高,创建一个可用于各种类型的 SPECIFICATION 的抽象类或接口会变得很有用。这需要把参数类型定义为某种高层的抽象类。

public interface Specification {
   boolean isSatisfiedBy(Object candidate);
}

This abstraction calls for a guard clause at the beginning of the method, but otherwise it does not affect functionality. For example, the Container Specification (from the example in Chapter 9, on page 236) would be modified this way:

这个抽象要求在方法的开始处放置一条卫语句(guard clause),但是没有卫语句也不影响它的功能。例如,可以对 Container Specification(参见图 9-16 以及后面的相关表格、代码等)做如下修改:

public class ContainerSpecification implements Specification {
   private ContainerFeature requiredFeature;

   public ContainerSpecification(ContainerFeature required) {
      requiredFeature = required;
   }

   boolean isSatisfiedBy(Object candidate){
      if (!candidate instanceof Container) return false;

      return
(Container)candidate.getFeatures().contains(requiredFeature);
   }
}

Now, let’s extend the Specification interface by adding the three new operations:

现在,让我们扩展 Specification 接口,加入 3 个新操作:

public interface Specification {
   boolean isSatisfiedBy(Object candidate);

   Specification and(Specification other);
   Specification or(Specification other);
   Specification not();
}

Recall that some Container Specifications were configured to require ventilated Containers and others to require armored Containers. A chemical that is both volatile and explosive would, presumably, need both of these SPECIFICATIONS. Easily done, using the new methods.

回忆一下,有些 Container Specification 需要通风性的 Container(容器),而有些则需要有防爆性。如果一种化学药品既易挥发又易爆炸,那么它可能同时需要这两种规格。如果使用新的方法,这就很容易实现。

Specification ventilated = new ContainerSpecification(VENTILATED);
Specification armored = new ContainerSpecification(ARMORED);

Specification both = ventilated.and(armored);

The declaration defines a new Specification object with the expected properties. This combination would have required a more complicated Container Specification, and would still have been special purpose.

这段声明定义了一个具有期望属性的新的 Specification 对象。这种组合将需要一个用于某种特殊目的的、更复杂的 Container Specification。

Suppose we had more than one kind of ventilated Container. It might not matter for some items which kind they were packed into. They could be placed in either type.

假设我们有多种通风容器。对于有些物品来说,把它们放进哪种容器中都没问题。它们可以放在任何一种通风容器中。

Specification ventilatedType1 =
      new ContainerSpecification(VENTILATED_TYPE_1);
Specification ventilatedType2 =
      new ContainerSpecification(VENTILATED_TYPE_2);

Specification either = ventilatedType1.or(ventilatedType2);

If it was considered wasteful to store sand in specialized containers, we could prohibit it by SPECIFYING a “cheap” container with no special features.

如果我们认为把砂存放在特殊容器中是一种浪费,那么可以通过指定一种没有特殊性质的“便宜的”容器来禁止把砂存放在特殊容器中。

Specification cheap = (ventilated.not()).and(armored.not());

This constraint would have prevented some of the suboptimal behavior of the prototype warehouse packer discussed in Chapter 9.

这个约束将阻止第 9 章中所讨论的仓库打包程序原型的某些不优化的行为。

The ability to build complex specifications out of simple elements increases the expressiveness of the code. The combinations are written in a declarative style.

从简单元素构建复杂规格的能力提高了代码的表达能力。以上组合是以声明式的风格编写的。

Depending on how SPECIFICATIONS are implemented, these operators may be easy or difficult to provide. What follows is a very simple implementation, which would be inefficient in some situations and quite practical in others. It is meant as an explanatory example. Like any pattern, there are many ways to implement it.

由于 SPECIFICATION 实现的方法存在不同,提供这些运算符的难易程度也不同。下面是一个非常简单的实现,在有些情况下它的效率很差,而有些情况下则很实用。举这个例子只是为了起到说明的作用。像任何模式一样,它也有很多实现方式。

public abstract class AbstractSpecification implements
      Specification {
   public Specification and(Specification other) {
      return new AndSpecification(this, other);
   }
   public Specification or(Specification other) {
      return new OrSpecification(this, other);
   }
   public Specification not() {
      return new NotSpecification(this);
   }
}

public class AndSpecification extends AbstractSpecification {
   Specification one;
   Specification other;
   public AndSpecification(Specification x, Specification y) {
      one = x;
      other = y;
   }
   public boolean isSatisfiedBy(Object candidate) {
      return one.isSatisfiedBy(candidate) &&
         other.isSatisfiedBy(candidate);
   }
}

public class OrSpecification extends AbstractSpecification {
   Specification one;
   Specification other;
   public OrSpecification(Specification x, Specification y) {
      one = x;
      other = y;
   }
   public boolean isSatisfiedBy(Object candidate) {
      return one.isSatisfiedBy(candidate) ||
         other.isSatisfiedBy(candidate);
   }
}

public class NotSpecification extends AbstractSpecification {
   Specification wrapped;

   public NotSpecification(Specification x) {
      wrapped = x;
   }
   public boolean isSatisfiedBy(Object candidate) {
      return !wrapped.isSatisfiedBy(candidate);
   }
}

COMPOSITE design of SPECIFICATION

This code was written to be as easy as possible to read in a book. As I said, there may be situations in which this is inefficient. However, other implementation options are possible that would minimize object count or boost speed, or perhaps be compatible with idiosyncratic technologies present in some project. The important thing is a model that captures the key concepts of the domain, along with an implementation that is faithful to that model. That leaves a lot of room to solve performance problems.

为了便于阅读,上面这段代码写得尽可能简单。如前所述,它在有些情况下是低效的。可能会有一些其他的实现选择,使得对象的数目减至最少,或极大地提高速度,或者与某个项目的特定技术兼容。重要的是模型捕捉到领域的关键概念,同时有一个忠实于该模型的实现。这就为解决性能问题预留了很大的空间。

Also, this full generality is not needed in many cases. In particular, AND tends to be used a lot more than the others, and it also tends to create less implementation complexity. Don’t be afraid to implement only AND, if that is all you need.

此外,这样完全的通用性在很多情况下并不需要。特别是 AND 可能比其他运算用得更多,而且它的实现的复杂程度也较小。如果你只需要 AND,那么完全可以只实现它,这没有什么可担心的。

Way back in Chapter 2, in the example dialog on page 30, the developers had apparently not implemented the “satisfied by” behavior of their SPECIFICATION. Up to that point, the SPECIFICATION had been used only for building to order. Even so, the abstraction was intact, and adding functionality was relatively easy. Using a pattern doesn’t mean building features you don’t need. They can be added later, as long as the concepts don’t get muddled.

我们回顾一下第 2 章示例中的对话,开发人员显然没有实现他们 SPECIFICATION 中的“satisfiedby”行为。在他们进行那段讨论的时候,SPECIFICATION 只是“根据需要来构建”(buildingto order)。尽管如此,抽象仍然完整,而且功能添加起来也相对简单。使用模式并不意味着构建你不需要的特性。它们可以过后再添加,只要不引起概念混淆即可。

Example: One Alternative Implementation of COMPOSITE SPECIFICATION

示例 COMPOSITE SPECIFICATION 的另一种实现

Some implementation environments don’t accommodate very fine grained objects very well. I once worked on a project with an object database that insisted on giving an object ID to every object and then tracking it. Each object had lots of overhead in memory space and performance, and total address space was a limiting factor. I employed SPECIFICATIONS at some important points in the domain design, which I think was a good decision. But I used a slightly more elaborate version of the implementation described in this chapter, which was definitely a mistake. It resulted in millions of very fine grained objects that contributed to bogging the system down.

有些实现环境不能使用粒度很小的对象。我曾经遇到过一个项目,它有一个对象数据库,这个数据库为每个对象分配一个 ID 并跟踪这个 ID。每个对象都占有很大的内存空间,并且产生很大的性能开销,因此总的地址空间成为一个限制因素。我在领域设计中的一些重要地方使用了 SPECIFICATION,当时我认为这是一个很好的决定。但我使用了一个过于细致的实现(像本章中描述的那样),这无疑是个错误。它产生了数百万个粒度非常小的对象,使整个系统的速度变得非常缓慢。

Here is an example of an alternative implementation that encodes the composite SPECIFICATION as a string or array encoding the logical expression, to be interpreted at runtime.

下面的例子给出了一种替代实现,它把组合 SPECIFICATION 编码为一个字符串或者数组(这个数组对逻辑表达式进行了编码),然后在运行时进行解析。

(Don’t worry if you do not see how you would implement this. The important thing is to realize that there are many ways of implementing a SPECIFICATION with logical operators, and so if the simple one is not practical in your situation, you have options.)

(即使你没明白它的实现也不要紧,重要的是认识到用逻辑运算符来实现 SPECIFICATION 的方式有很多。如果最简单的方法不适用于你的情况,可以选择其他的方法。)

When you want to test a candidate, you have to interpret this structure, which can be done by popping off each element, then evaluating it or popping off the next as required by an operator. You would end up with this:

当我们想测试一种候选方案时,必须解释这个结构,这可以通过把每个元素弹出来并计算它(或者是根据运算符的需要弹出下一个元素)来实现。最后将得到如下结果:

and(not(armored), not(ventilated))

This design has pros (+) and cons (–):

这种设计有一些优点(+)和缺点(-)

+ Low object count

+ Efficient use of memory

– Requires more sophisticated developers

You have to find an implementation with trade-offs that work for your circumstances. The same pattern and model can underlie very different implementations.

你必须根据自己的实际情况做出权衡,找到一种适合你的实现。基于相同的模式和模型可以创建出完全不同的实现。

Subsumption

包容

This final feature is not usually needed and can be difficult to implement, but every now and then it solves a really hard problem. It also elucidates the meaning of a SPECIFICATION.

最后要讲的这个包容特性并不是经常需要,而且实现起来也很难,但有时它确实能够解决很困难的问题。它还能够表达出一个 SPECIFICATION 的含义。

Consider again the chemical warehouse packer from the example on page 235. Recall that each Chemical had a Container Specification, and the Packer SERVICE guaranteed that all these would be satisfied when Drums are assigned to Containers. All is well... until someone changes the regulations.

再次考虑一下前面的化学仓库打包程序的例子。每个 Chemical 都有一个 Container Specification,而且 Packer SERVICE 确保当把 Drum 分配到 Container 中时,所有这些 Container Specification 都被满足,一切都没有问题……直到有人改变了规则。

Every few months a new set of rules is issued, and our users would like to be able to produce a list of the chemical types that now have more stringent requirements.

每隔几个月都会发布一组新的规则,我们的用户希望能够生成一个列表,把那些已经有了更严格要求的化学品列出来。

Of course, we could give a partial answer (and one the users probably also want) by running a validation of each Drum in the inventory, with the new SPECIFICATIONS in place, and finding all those that no longer meet the SPEC. This would tell the users which Drums in the existing inventory they need to move.

当然,通过运行一个验证,用新实施的规格来检查仓库中的每个 Drum,并找到所有不再满足新 SPECIFICATION 的化学品,这样可以把一部分化学品列出来,而且这可能也是用户需要的。这可以告诉用户现在仓库中有哪些 Drum 是需要转移的。

But what they asked for was a list of chemicals whose handling has become more stringent. Perhaps there are none in-house right now, or perhaps they just happened to be packed into a more stringent container. In either case, the report just described would not list them.

但用户要求的是把所有那些存放要求变得更严格的化学品都列出来。或许仓库里目前还没有这样的化学品,或者它们碰巧被装到了一个更严格的容器中。无论是哪种情况,刚才的那个报告都不会列出它们。

Let’s introduce a new operation for directly comparing two SPECIFICATIONS.

我们引入一个用于直接比较两种 SPECIFICATION 的新操作:

boolean subsumes(Specification other);

A more stringent SPEC subsumes a less stringent one. It could take its place without any previous requirement being neglected.

更严格的 SPECIFICATION 包容不太严格的 SPECIFICATION。用更严格的 SPECIFICATION 来取代不严格的 SPECIFICATION 不会遗漏掉先前的任何需求,如图 10-15 所示。

The SPECIFICATION for a gasoline container has been tightened.

In the language of SPECIFICATION, we would say that the new SPECIFICATION subsumes the old SPECIFICATION, because any candidate that would satisfy the new SPEC would also satisfy the old.

在 SPECIFICATION 语言中,我们说新的 SPECIFICATION 包容旧的 SPECIFICATION,因为任何满足新 SPECIFICATION 的对象都将满足旧 SPECIFICATION。

If each of these SPECIFICATIONS is viewed as a predicate, subsumption is equivalent to logical implication. Using conventional notation, A → B means that statement A implies statement B, so that if A is true, B is also true.

如果把每个 SPECIFICATION 看成一个谓词,那么包容就等于逻辑蕴涵(logicalimplication)。使用传统的符号,A→B 表示声明 A 蕴涵声明 B,因此,如果 A 为真,则 B 也为真。

Let’s apply this logic to our container-matching needs. When a SPECIFICATION is being changed, we would like to know if the proposed new SPEC meets all the conditions of the old one.

让我们把这个逻辑应用于我们的容器匹配需求。当一个 SPECIFICATION 被修改时,我们想知道新 SPECIFICATION 是否满足旧 SPECIFICATION 的所有条件。

New Spec → Old Spec

That is, if the new spec is true, then the old spec is also true. Proving a logical implication in a general way is very difficult, but special cases can be easy. For example, particular parameterized SPECS can define their own subsumption rule.

也就是说,如果新规格为真,那么旧规格一定也为真。要证明一般情况下的逻辑蕴涵是很难的,但特殊情况就很容易证明。例如,参数化的 SPECIFICATION 可以定义它们自己的包容规则。

public class MinimumAgeSpecification {
   int threshold;

   public boolean isSatisfiedBy(Person candidate) {
      return candidate.getAge() >= threshold;
   }

   public boolean subsumes(MinimumAgeSpecification other) {
      return threshold >= other.getThreshold();
   }
}

A JUnit test might contain this:

JUnit 测试可能包含以下代码:

drivingAge = new MinimumAgeSpecification(16);
votingAge = new MinimumAgeSpecification(18);
assertTrue(votingAge.subsumes(drivingAge));

Another practical special case, one suited to address the Container Specification problem, is a SPECIFICATION interface combining subsumption with the single logical operator AND.

还有一个有用的特例适用于解决 Container Specification 问题,它用 SPECIFICATION 接口把包容与逻辑操作 AND 结合起来。

public interface Specification {
   boolean isSatisfiedBy(Object candidate);
   Specification and(Specification other);
   boolean subsumes(Specification other);
}

Proving implication with only the AND operator is simple:

证明只有一个 AND 操作符的涵盖是简单的:

A AND B → A

or, in a more complicated case:

或者在更复杂的情况中,

A AND B AND C → A AND B

So if the Composite Specification is able to collect all the leaf SPECIFICATIONS that are “ANDed” together, then all we have to do is check that the subsuming SPECIFICATION has all the leaves that the subsumed one has, and maybe some extra ones as well—its leaves are a superset of the other SPEC’s set of leaves.

这样,如果 Composite Specification 能够把所有由“AND”连接起来的叶节点(leaf)SPECIFICATION 收集到一起,那么我们要做的事情只是检查包容规格(subsuming SPECIFICATION)是否含有被包容规格的所有叶节点(而且它可能还包含更多的叶节点)——它的叶节点集合是另一个 SPECIFICATION 的叶节点集合的超集。

public boolean subsumes(Specification other) {
  if (other instanceof CompositeSpecification) {
     Collection otherLeaves =
        (CompositeSpecification) other.leafSpecifications();
     Iterator it = otherLeaves.iterator();
     while (it.hasNext()) {
        if (!leafSpecifications().contains(it.next()))
           return false;
     }
   } else {
      if (!leafSpecifications().contains(other))
         return false;
   }
   return true;
}

This interaction could be enhanced to compare carefully chosen parameterized leaf SPECIFICATIONS and some other complications. Unfortunately, when OR and NOT are included, these proofs become much more involved. In most situations it is best to avoid such complexity by making a choice, either forgoing some of the operators or forgoing subsumption. If both are needed, consider carefully if the benefit is great enough to justify the difficulty.

我们还可以增强这种交互,对仔细选择的参数化的叶节点 SPECIFICATION 进行比较或者进行其他一些复杂的比较。遗憾的是,当把 OR 和 NOT 也包括进来时,这些证明会变得更复杂。在大多数情况下,最好避免出现这样的复杂性:要么选择放弃一些运算符,要么不使用包容。如果这二者同时需要,那么要慎重考虑这样做的价值是否多过它所带来的麻烦。

10.9 ANGLES OF ATTACK 切入问题的角度

This chapter has presented a raft of techniques to clarify the intent of code, to make the consequences of using it transparent, and to decouple model elements. Even so, this kind of design is difficult. You can’t just look at an enormous system and say, “Let’s make this supple.” You have to choose targets. Here are a couple of broad approaches, followed by an extended example showing how the patterns are fit together and used to take on a bigger design.

本章展示了一系列技术,它们用于澄清代码意图,使得使用代码的影响变得显而易见,并且解除模型元素的耦合。尽管有这些技术,但要想实现这样的设计还是很难的。我们不能只是看着一个庞大的系统说:“让我们把它设计得灵活点吧。”我们必须选择具体的目标。下面介绍几种主要方法,然后给出一个扩展的示例,它展示了如何把这些模式结合起来使用,并用于处理更大的设计。

10.9.1 Carve Off Subdomains 分割子领域

You just can’t tackle the whole design at once. Pick away at it. Some aspects of the system will suggest approaches to you, and they can be factored out and worked over. You may see a part of the model that can be viewed as specialized math; separate that. Your application enforces complex rules restricting state changes; pull this out into a separate model or simple framework that lets you declare the rules. With each such step, not only is the new module clean, but also the part left behind is smaller and clearer. Part of what’s left is written in a declarative style, a declaration in terms of the special math or validation framework, or whatever form the subdomain takes.

我们无法一下子就能处理好整个设计,而需要一步一步地进行。我们从系统的某些方面可以看出适合用哪种方法处理,那么就把它们提取出来加以处理。如果模型的某个部分可以被看作是专门的数学,那么可以把这部分分离出来。如果应用程序实施了某些用来限制状态改变的复杂规则,那么可以把这部分提取到一个单独的模型中,或者提取到一个允许声明规则的简单框架中。随着这些步骤的进行,不仅新模型更整洁了,而且剩下的部分也更小、更清晰了。在剩下的模型中,有的部分是用声明式的风格来编写的——这些可能是根据专门数学或验证框架编写的声明,或者是子领域所采用的任何形式。

It is more useful to make a big impact on one area, making a part of the design really supple, than to spread your efforts thin. Chapter 15 discusses in more depth how to choose and manage subdomains.

重点突击某个部分,使设计的一个部分真正变得灵活起来,这比分散精力泛泛地处理整个系统要有用得多。第 15 章将更深入地讨论如何选择和管理子领域。

10.9.2 Draw on Established Formalisms, When You Can 尽可能利用已有的形式

Creating a tight conceptual framework from scratch is something you can’t do every day. Sometimes you discover and refine one of these over the course of the life of a project. But you can often use and adapt conceptual systems that are long established in your domain or others, some of which have been refined and distilled over centuries. Many business applications involve accounting, for example. Accounting defines a well-developed set of ENTITIES and rules that make for an easy adaptation to a deep model and a supple design.

我们不能把从头创建一个严密的概念框架当作一项日常的工作来做。在项目的生命周期中,我们有时会发现并精炼出这样一个框架。但更常见的情况是,可以对你的领域或其他领域中那些建立已久的概念系统加以修改和利用,其中有些系统已经被精化和提炼达几个世纪之久。例如,很多商业应用程序涉及会计学。会计学定义了一组成熟的 ENTITY 和规则,我们很容易对这些 ENTITY 和规则进行调整,得到一个深层的模型和柔性设计。

There are many such formalized conceptual frameworks, but my personal favorite is math. It is surprising how useful it can be to pull out some twist on basic arithmetic. Many domains include math somewhere. Look for it. Dig it out. Specialized math is clean, combinable by clear rules, and people find it easy to understand. One example from my past is “Shares Math,” which will end this chapter.

有很多这样的正式概念框架,而我个人最喜欢的框架是数学。数学的强大功能令人惊奇,它可以用基本数学概念把一些复杂的问题提取出来。很多领域都涉及数学,我们要寻找这样的部分,并把它挖掘出来。专门的数学很整齐,可以通过清晰的规则进行组合,并很容易理解。下面我要举一个例子,用它来结束本章,它来自我过去的经历——它就是“股份数学”(SharesMath)。

Example: Integrating the Patterns: Shares Math

示例把各种模式结合起来使用:股份数学

Chapter 8 told the story of a model breakthrough on a project to build a syndicated loan system. Now this example will go into detail, focusing on just one feature of a design comparable to the one on that project.

第 8 章讲述了在银团贷款系统项目上发生的一次模型突破的故事。现在我们将更详细地讨论这个例子,这里我们只集中讨论设计的一个特性,并与原来项目上的特性进行比较。

One requirement of that application was that when the borrower makes a principal payment, the money is, by default, prorated according to the lenders’ shares in the loan.

该应用程序的一个需求是,当借款者偿付本金时,默认是根据放贷方的股份来分配这笔钱。

Initial Design for Payment Distribution

最初的付款分配设计

As we refactor it, this code will get easier to understand, so don’t get stuck on this version.

随着我们对它进行重构,这段代码会变得越来越容易理解,因此不必过度深究这个版本。

public class Loan {
   private Map shares;

   //Accessors, constructors, and very simple methods are excluded

   public Map distributePrincipalPayment(double paymentAmount) {
      Map paymentShares = new HashMap();
      Map loanShares = getShares();
      double total = getAmount();
      Iterator it = loanShares.keySet().iterator();
      while(it.hasNext()) {
         Object owner = it.next();
         double initialLoanShareAmount = getShareAmount(owner);
         double paymentShareAmount =
            initialLoanShareAmount / total * paymentAmount;
         Share paymentShare =
            new Share(owner, paymentShareAmount);
         paymentShares.put(owner, paymentShare);

         double newLoanShareAmount =
            initialLoanShareAmount - paymentShareAmount;
         Share newLoanShare =
            new Share(owner, newLoanShareAmount);
         loanShares.put(owner, newLoanShare);
      }
      return paymentShares;
   }

   public double getAmount() {
      Map loanShares = getShares();
      double total = 0.0;
      Iterator it = loanShares.keySet().iterator();
      while(it.hasNext()) {
         Share loanShare = (Share) loanShares.get(it.next());
         total = total + loanShare.getAmount();
      }
      return total;
   }
}

Separating Commands and SIDE-EFFECT-FREE FUNCTIONS

把命令和 SIDE-EFFECT-FREE FUNCTION 分开

This design already has INTENTION-REVEALING INTERFACES. But the distributePaymentPrincipal() method does a dangerous thing: It calculates the shares for distribution and also modifies the Loan. Let’s refactor to separate the query from the modifier.

这个设计已经有了 INTENTION-REVEALING INTERFACE。但 distributePaymentPrincipal() 方法做了一件很危险的事情。它计算要分配的股份,并且还修改了 Loan。我们通过重构把查询从修改操作中分离出来。

public void applyPrincipalPaymentShares(Map paymentShares) {
   Map loanShares = getShares();
   Iterator it = paymentShares.keySet().iterator();
   while(it.hasNext()) {
      Object lender = it.next();
      Share paymentShare = (Share) paymentShares.get(lender);
      Share loanShare = (Share) loanShares.get(lender);
      double newLoanShareAmount = loanShare.getAmount() -
         paymentShare.getAmount();
      Share newLoanShare = new Share(lender, newLoanShareAmount);
      loanShares.put(lender, newLoanShare);
   }
}

public Map calculatePrincipalPaymentShares(double paymentAmount) {
   Map paymentShares = new HashMap();
   Map loanShares = getShares();
   double total = getAmount();
   Iterator it = loanShares.keySet().iterator();
   while(it.hasNext()) {
      Object lender = it.next();
      Share loanShare = (Share) loanShares.get(lender);
      double paymentShareAmount =
         loanShare.getAmount() / total * paymentAmount;
      Share paymentShare = new Share(lender, paymentShareAmount);
      paymentShares.put(lender, paymentShare);
   }
   return paymentShares;
}

Client code now looks like this:

客户代码现在如下:

Map distribution =
   aLoan.calculatePrincipalPaymentShares(paymentAmount);
aLoan.applyPrincipalPaymentShares(distribution);

Not too bad. The FUNCTIONS have encapsulated a lot of complexity behind INTENTION-REVEALING INTERFACES. But the code does begin to multiply some when we add applyDrawdown(), calculateFeePaymentShares(), and so on. Each extension complicates the code and weighs it down. This might be a point where the granularity is too coarse. The conventional approach would be to break the calculation methods down into subroutines. That could well be a good step along the way, but we ultimately want to see the underlying conceptual boundaries and deepen the model. The elements of a design with such a CONCEPT-CONTOURING grain could be combined to produce the needed variations.

这段代码不算太差。方法把大量的复杂性封装在 INTENTION-REVEALING INTERFACE 背后。但当我们添加 applyDrawdown(), calculateFeePaymentShares() 等一些函数之后,代码开始大量增加。每次扩充都使代码变得更复杂,速度也不断减慢。这可能是由于粒度过大造成的。传统的解决方法是把计算方法分解为子例程。这可能是一种不错的解决办法,但我们希望最终看到底层的概念边界,并深化模型。当设计元素具有这种 CONCEPT-CONTOURING 的粒度时,就可以把这些元素进行组合,得到所需的变体。

Making an Implicit Concept Explicit

把隐式概念变为显式概念

There are enough pointers now to start probing for that new model. The Share objects are passive in this implementation, and they are being manipulated in complex, low-level ways. This is because most of the rules and calculations about shares don’t apply to single shares, but to groups of them. There is a missing concept: shares are related to each other as parts making up a whole. Making this concept explicit will let us express those rules and calculations more succinctly.

现在我们有足够的条件来探索新模型了。在这个实现中,Share 对象是被动的,它们是用一些复杂、底层的方式来操纵的。这是因为大部分与股份有关的规则和计算并不适用于单独的股份,而是用于成组的股份。有一个概念被漏掉了:各个股份在构成整体时互相之间是有关联的。如果能把这个概念显式地表达出来,就能更简洁地表示这些规则和计算。

The Share Pie represents the total distribution of a specific Loan. It is an ENTITY whose identity is local within the AGGREGATE of the Loan. The actual distribution calculations can be delegated to the Share Pie.

Share Pie 表示了一个特定的 Loan 的总体分布。它是一个 ENTITY,其标识位于 Loan AGGREGATE 的内部。实际的分布计算可以被委托给 Share Pie。

public class Loan {
   private SharePie shares;

   //Accessors, constructors, and straightforward methods
   //are omitted

   public Map calculatePrincipalPaymentDistribution(
                                         double paymentAmount) {
      return getShares().prorated(paymentAmount);
   }
   public void applyPrincipalPayment(Map paymentShares) {
      shares.decrease(paymentShares);
   }
}

The Loan is simplified, and the Share calculations are centralized in a VALUE OBJECT focused on that responsibility. Still, the calculations haven’t really become more versatile or easier to use.

这样 Loan 就被简化了,而且 Share 计算也被集中到了一个 VALUE OBJECT 中(这个 VALUE OBJECT 只负责这个计算)。但是,这个计算并没有真正变得通用和易用。

Share Pie Becomes a VALUE OBJECT: Cascade of Insights

在进一步理解之后,把 Share Pie 变成一个 VALUE OBJECT

Often, the hands-on experience of implementing a new design will trigger a new insight into the model itself. In this case, the tight coupling of the Loan and Share Pie seems to be obscuring the relationship of the Share Pie and the Shares. What would happen if we made Share Pie a VALUE OBJECT?

通常,在实现一个新设计的过程中,所获得的经验会引导我们对模型本身形成新的认识。在这个例子中,Loan 和 Share Pie 的紧密耦合使 Share Pie 与 Share 之间的关系变得模糊不清。如果我们把 Share Pie 变成一个 VALUE OBJECT,会产生什么变化呢?

This would mean that increase(Map) and decrease(Map) would not be allowed, because the Share Pie would have to be immutable. To change the Share Pie’s value, the whole Pie would have to be replaced. So you could have operations such as addShares(Map) that would return a whole new, larger Share Pie.

这意味着不能再使用 increase(Map)decrease(Map) 了,因为 Share Pie 必须是不变的。要更改 Share Pie 的值,必须整个替换。因此需要使用 addShares(Map) 这样的方法来返回一个全新的、更大的 Share Pie。

Let’s go all the way to CLOSURE OF OPERATIONS. Instead of “increasing” a Share Pie or adding Shares to it, just add two Share Pies together: the result is the new, larger Share Pie.

让我们再进一步把它变成 CLOSURE OF OPERATION。我们不采用“增加”Share Pie 或向它添加 Share,而只是把两个 Share Pie 加起来,结果是一个新的、更大的 Share Pie。

We can partially close the prorate() operation over Share Pie just by changing the return type. Renaming it to prorated() emphasizes its lack of side effects. “Shares Math” starts to take shape, initially with four operations.

我们可以先把 Share Pie 上的 prorate() 操作变成半个闭合操作,这只需要修改返回类型即可。我们把它重命名为 prorated() ,以便强调它没有副作用。“股份数学”开始成型了,最初它有 4 个操作。

We can make some well-defined ASSERTIONS about our new VALUE OBJECTS, the Share Pies. Each method means something.

我们可以为新的 VALUE OBJECT(Share Pie)创建一些定义明确的 ASSERTION。每个方法都有各自的意义。

public class SharePie {
   private Map shares = new HashMap();

   //Accessors and other straightforward methods are omitted


   public double getAmount() {
      double total = 0.0;
      Iterator it = shares.keySet().iterator();
      while(it.hasNext()) {                              The whole is equal to the
         Share loanShare = getShare(it.next());          sum of its parts.
         total = total + loanShare.getAmount();
      }
      return total;
}

public SharePie minus(SharePie otherShares) {
   SharePie result = new SharePie();
   Set owners = new HashSet();
   owners.addAll(getOwners());
   owners.addAll(otherShares.getOwners());               The difference between
   Iterator it = owners.iterator();                      two Pies is the difference
   while(it.hasNext()) {                                 between each owner's
      Object owner = it.next();                          share.
      double resultShareAmount = getShareAmount(owner) –
            otherShares.getShareAmount(owner);
      result.add(owner, resultShareAmount);
   }
   return result;
}

public SharePie plus(SharePie otherShares) {             The combination of two
   //Similar to implementation of minus()                Pies is the combination of
}                                                        each owner's share.

public SharePie prorated(double amountToProrate) {
   SharePie proration = new SharePie();
   double basis = getAmount();                           An amount can be divided
   Iterator it = shares.keySet().iterator();             proportionately
   while(it.hasNext()) {                                 among all shareholders.
      Object owner = it.next();
      Share share = getShare(owner);
         double proratedShareAmount =
            share.getAmount() / basis * amountToProrate;
         proration.add(owner, proratedShareAmount);
      }
      return proration;
   }

}

The Suppleness of the New Design

新设计的柔性

At this point, the methods in the all-important Loan class could be as simple as this:

现在,最重要的 Loan 类中的方法已经很简单了,如下:

public class Loan {
   private SharePie shares;

   //Accessors, constructors, and straightforward methods
   //are omitted

   public SharePie calculatePrincipalPaymentDistribution(
                                        double paymentAmount) {
      return shares.prorated(paymentAmount);
   }

   public void applyPrincipalPayment(SharePie paymentShares) {
      setShares(shares.minus(paymentShares));
   }

Each of these short methods states its meaning. Applying a principal payment means that you subtract the payment from the loan, share by share. Distributing a principal payment is done by dividing the amount pro rata among the shareholders. The design of the Share Pie has allowed us to use a declarative style in the Loan code, producing code that begins to read like a conceptual definition of the business transaction, rather than a calculation.

这些简短的方法中的每一个都表达了其自己的含义。本金偿付表示从贷款中按照股份减去偿付额。对已偿付的本金进行分配是指在股份持有者之间按比例分配。Share Pie 的设计使我们能够在 Loan 代码中使用声明式风格,所编写的代码读起来像是业务交易的概念定义,而不像是一种计算。

Other transaction types (too complicated to list before) can be declared easily now. For example, loan drawdowns are divided among lenders based on their shares of the Facility. The new draw-down is added to the outstanding Loan. In our new domain language:

要查看每个贷方的原定贷款额与实际贷款额之差,只需计算该贷方在未偿 Loan 总额中的理论分配值,然后用 Loan 的实际股份减去这个值即可。

public class Facility {
   private SharePie shares;
   . . .
   public SharePie calculateDrawdownDefaultDistribution(
                                         double drawdownAmount) {
      return shares.prorated(drawdownAmount);
   }
}

public class Loan {
   . . .
   public void applyDrawdown(SharePie drawdownShares) {
      setShares(shares.plus(drawdownShares));
   }
}

To see the deviation of each lender from its agreed contribution, take the theoretical distribution of the outstanding Loan amount and subtract it from the Loan’s actual shares:

Share Pie 设计的一些特性使这种组合变得很容易,也提高了代码的表达能力。

SharePie originalAgreement =
   aFacility.getShares().prorated(aLoan.getAmount());
SharePie actual = aLoan.getShares();
SharePie deviation = actual.minus(originalAgreement);

Certain characteristics of the Share Pie design make for this easy recombination and communication in the code.

  • Complex logic is encapsulated in specialized VALUE OBJECTS with SIDE-EFFECT-FREE FUNCTIONS. Most complex logic has been encapsulated in these immutable objects. Because Share Pies are VALUE OBJECTS, the math operations can create new instances, which we can use freely to replace outdated instances.
  • 复杂的逻辑通过 SIDE-EFFECT-FREE FUNCTION 被封装到了专门的 VALUE OBJECT 中。大部分复杂逻辑都已经被封装到这些不变的对象中。由于 Share Pie 是 VALUE OBJECT,因此数学运算可以创建新实例,我们可以用这些新实例来替换旧实例。

None of the Share Pie methods causes any change to any existing object. This allows us to use plus(), minus(), and prorated() freely in intermediate calculations, combining them, expecting them to do what their names suggest, and nothing more. It also allows us to build analytical features based on the same methods. (Before, they could be called only when an actual distribution was made, because the data would change after each call.)

Share Pie 的所有方法都不会修改任何现有对象。这使我们在中间计算中能够自由地使用 plus()minus()prorated(),并通过组合它们来实现预期效果,同时又不会产生其他副作用。我们还可以根据这些方法来创建分析功能(以前,只有在执行实际计算的时候才能调用这些方法,因为在每次调用之后数据就改变了)。

  • State-modifying operations are simple and characterized with ASSERTIONS. The high-level abstractions of Shares Math allow invariants of transactions to be written concisely in a declarative style. For example, the deviation is the actual pie minus the Loan amount prorated based on the Facility’s Share Pie.
  • Model concepts are decoupled; operations entangle a minimum of other types. Some methods on Share Pie exhibit CLOSURE OF OPERATIONS (the methods to add or subtract are closed under Share Pies). Others take simple amounts as arguments or return values; they are not closed, but they add little to the conceptual load. The Share Pie interacts closely with only one other class, Share. As a result, the Share Pie is self-contained, easily understood, easily tested, and easily combined to form declarative transactions. These properties were inherited from the math formalism.
  • Familiar formalism makes the protocol easy to grasp. A wholly original protocol for manipulating shares could have been devised based on financial terminology. In principle, it could have been made supple. But it would have had two disadvantages. First, it would have to be invented, a difficult and uncertain task. Second, it would have to be learned by each person who dealt with it. People who see Shares Math recognize a system they already know, and because the design has been kept carefully consistent with the rules of arithmetic, those people are not misled.

  • 修改状态的操作很简单,而且是用 ASSERTION 来描述的。利用“股份数学”的高层抽象,我们可以用声明式的风格来精确地编写交易的固定规则。例如,差值是实际股份减去根据 Facility 的 Share Pie 按比例分配的 Loan 额。
  • 模型概念解除了耦合,操作只涉及最少的其他类型。Share Pie 上的一些方法显示出它们是 CLOSURE OF OPERATION(加、减方法是 Share Pie 之下的闭合操作)。其他操作以简单的总额作为参数或返回值,它们虽然不是闭合操作,但只增加了极少的概念负担。SharePie 只与一个其他的类——Share 有密切交互。这样,Share Pie 就非常直截了当,易于理解和测试,也很容易通过组合来产生声明式的交易。这些特性都是从数学形式中继承得来的。
  • 熟悉的形式使我们更容易掌握协议(protocol)。最初用于操作股份的协议本来也是可以用财务术语来设计的,而且从原则上讲,这样的设计也能很灵活。但它有两个缺点。首先,我们必须从头开始设计它,这是一项困难且没有把握完成的任务。其次,每个处理它的人都必须先学会它。而我们现在这种设计的好处是,看到股份数学的人会发现他们对这个早已十分熟悉了,而且由于设计与算术规则保持严格一致,因此人们不会被误导。

Pulling out the part of the problem that corresponded to the formalism of math, we arrived at a supple design for Shares that further distills the core Loan and Facility methods. (See Chapter 15 for discussion of the CORE DOMAIN.)

把与数学形式有关的那部分问题提取出来之后,我们得到了一个柔性的 Share 设计,这使得我们可以进一步精炼核心的 Loan 和 Facility 方法(参见第 15 章有关 CORE DOMAIN 的讨论)。

Supple design has a profound effect on the ability of software to cope with change and complexity. As the examples in this chapter have shown, it often hinges on quite detailed modeling and design decisions. The impact can go beyond a specific modeling and design problem. Chapter 15 will discuss the strategic value of supple design as one of several tools for distilling a domain model to make large and complex projects more tractable.

柔性设计可以极大地提升软件处理变更和复杂性的能力。正如本章的例子所示,柔性设计在很大程度上取决于详细的建模和设计决策。柔性设计的影响可能远远超越某个特定的建模和设计问题。第 15 章将讨论柔性设计的战略价值,我们将把它作为一种工具,用来精炼领域模型,以便使大型和复杂的项目更易于掌握。