18
4

苍鹭与少年,宫崎骏的坚持

0
归档:2024年4月分类:点滴生活

看完了《你想活出怎样的人生》,从内容上来看,这部电影的核心仍然是人文主义关怀、关于战后创伤与现代新国家的重建,当然也带有上世纪知识分子对过去挥之不去的烙印。吉卜力的动画片其实更多是拍给大人们看的,宫崎骏一直没有放弃自己坚持的理念:和平主义(反思战争和反对一切战争)、女性主义(主角大都是独立勇敢自由的女孩)和人与自然平等共存。

这次宫崎骏老爷爷因为身体原因不能去奥斯卡领奖,看来这部动画大概率真的会是他的封笔之作。老爷爷从始至终都坚持完全手绘制作动画的模式,这种方式成本极高,但是制作出来的动画相当逼真。这种创作动画的手法,民国时期的万氏兄弟曾经引领亚洲,其实日本的动画最开始是受中国影响,日本动画大师手冢治虫曾说他正是看了万氏兄弟的《铁扇公主》动画片后放弃学医,决定从事动画创作,手冢治虫算是宫崎骏的上一辈。只是后来中国突降人祸,形成了几十年的文化断层,日本一直沿着这条路坚持走到了今天,获得了巨大成功和全世界的尊重。

大约在二十年前,我们一起第一次看吉卜力的动画片《千与千寻》,这是他们工作室第一部获得奥斯卡最佳动画的作品,后来我们看完了吉卜力出品的大部分动画,并收藏了喜欢的蓝光,家里还有一本收集每部动画手绘底稿复印件的大部头书。前段时间我们两次东渡扶桑,特地去了一趟他们的美术馆,除了沉浸于吉卜力的梦幻世界,还看到了龙猫巴士爬行的电线杆原型、千寻神隐的神社痕迹和动画片背景里的各种现实场景。

日本是一个传统又时尚,保守又先进的多元化现代文明国家,只有你亲自去一趟才能深刻体会这种感觉,我在那里看到了本地朴质的文化与奢华的现代物质文化并肩存在,也看到了基督教、佛教和神道教各自蓬勃发展,更看到了古代中国的文化与现代文明交织成一个五彩斑斓的世界,太多的不想说,我真的认为:现在的日本(也许还有印度)是真正的神奇的东方文明古国。

14
3

The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.

依赖反转原则(DIP)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而用具体实现。

In a statically typed language, like Java, this means that the use, import, and include statements should refer only to source modules containing interfaces, abstract classes, or some other kind of abstract declaration. Nothing concrete should be depended on.

也就是说,在 Java 这类静态类型的编程语言中,在使用 use、import、include 这些语句时应该只引用那些包含接口、抽象类或者其他抽象类型声明的源文件,不应该引用任何具体实现。

The same rule applies for dynamically typed languages, like Ruby and Python. Source code dependencies should not refer to concrete modules. However, in these languages it is a bit harder to define what a concrete module is. In particular, it is any module in which the functions being called are implemented.

同样的,在 Ruby、Python 这类动态类型的编程语言中,我们也不应该在源代码 层次上引用包含具体实现的模块。当然,在这类语言中,事实上很难清晰界定某个模块是否属于“具体实现”。

Clearly, treating this idea as a rule is unrealistic, because software systems must depend on many concrete facilities. For example, the String class in Java is concrete, and it would be unrealistic to try to force it to be abstract. The source code dependency on the concrete java.lang.string cannot, and should not, be avoided.

显而易见,把这条设计原则当成金科玉律来加以严格执行是不现实的,因为软件系统在实际构造中不可避免地需要依赖到一些具体实现。例如,Java 中的 String 类就是这样一个具体实现,我们将其强迫转化为抽象类是不现实的,而在源代码层次上也无法避免对 java.lang.String 的依赖,并且也不应该尝试去避免。

By comparison, the String class is very stable. Changes to that class are very rare and tightly controlled. Programmers and architects do not have to worry about frequent and capricious changes to String.

但 String 类本身是非常稳定的,因为这个类被修改的情况是非常罕见的,而且可修改的内容也受到严格的控制,所以程序员和软件架构师完全不必担心 String 类上会发生经常性的或意料之外的修改。

For these reasons, we tend to ignore the stable background of operating system and platform facilities when it comes to DIP. We tolerate those concrete dependencies because we know we can rely on them not to change.

同理,在应用 DIP 时,我们也不必考虑稳定的操作系统或者平台设施,因为这些系统接口很少会有变动。

It is the volatile concrete elements of our system that we want to avoid depending on. Those are the modules that we are actively developing, and that are undergoing frequent change.

我们主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。

STABLE ABSTRACTIONS 稳定的抽象层
Every change to an abstract interface corresponds to a change to its concrete implementations. Conversely, changes to concrete implementations do not always, or even usually, require changes to the interfaces that they implement. Therefore interfaces are less volatile than implementations.

我们每次修改抽象接口的时候,一定也会去修改对应的具体实现。但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。所以我们可以认为接口比实现更稳定。

Indeed, good software designers and architects work hard to reduce the volatility of interfaces. They try to find ways to add functionality to implementations without making changes to the interfaces. This is Software Design 101.

的确,优秀的软件设计师和架构师会花费很大精力来设计接口,以减少未来对其进行改动。毕竟争取在不修改接口的情况下为软件增加新的功能是软件设计的基础常识。

The implication, then, is that stable software architectures are those that avoid depending on volatile concretions, and that favor the use of stable abstract interfaces. This implication boils down to a set of very specific coding practices:

也就是说,如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。下面,我们将该设计原则归结为以下几条具体的编码守则:

Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed. It also puts severe constraints on the creation of objects and generally enforces the use of Abstract Factories.
Don’t derive from volatile concrete classes. This is a corollary to the previous rule, but it bears special mention. In statically typed languages, inheritance is the strongest, and most rigid, of all the source code relationships; consequently, it should be used with great care. In dynamically typed languages, inheritance is less of a problem, but it is still a dependency—and caution is always the wisest choice.
Don’t override concrete functions. Concrete functions often require source code dependencies. When you override those functions, you do not eliminate those dependencies—indeed, you inherit them. To manage those dependencies, you should make the function abstract and create multiple implementations.
Never mention the name of anything concrete and volatile. This is really just a restatement of the principle itself.
应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。这条守则适用于所有编程语言,无论静态类型语言还是动态类型语言。同时,对象的创建过程也应该受到严格限制,对此,我们通常会选择用抽象工厂(abstract factory)这个设计模式。
不要在具体实现类上创建衍生类。上一条守则虽然也隐含了这层意思,但它还是值得被单独拿出来做一次详细声明。在静态类型的编程语言中,继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。即使是在稍微便于修改的动态类型语言中,这条守则也应该被认真考虑。
不要覆盖(override)包含具体实现的函数。调用包含具体实现的函数通常 就意味着引入了源代码级别的依赖。即使覆盖了这些函数,我们也无法消除这其中的依赖——这些函数继承了那些依赖关系。在这里,控制依赖关系的唯一办法,就是创建一个抽象函数,然后再为该函数提供多种具体实现。
应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。这基本上是 DIP 原则的另外一个表达方式。
FACTORIES 工厂模式
To comply with these rules, the creation of volatile concrete objects requires special handling. This caution is warranted because, in virtually all languages, the creation of an object requires a source code dependency on the concrete definition of that object.

如果想要遵守上述编码守则,我们就必须要对那些易变对象的创建过程做一些特殊处理,这样的谨慎是很有必要的,因为基本在所有的编程语言中,创建对象的操作都免不了需要在源代码层次上依赖对象的具体实现。

In most object-oriented languages, such as Java, we would use an Abstract Factory to manage this undesirable dependency.

在大部分面向对象编程语言中,人们都会选择用抽象工厂模式来解决这个源代码依赖的问题。

The diagram in Figure 11.1 shows the structure. The Application uses the ConcreteImpl through the Service interface. However, the Application must somehow create instances of the ConcreteImpl. To achieve this without creating a source code dependency on the ConcreteImpl, the Application calls the makeSvc method of the ServiceFactory interface. This method is implemented by the ServiceFactoryImpl class, which derives from ServiceFactory. That implementation instantiates the ConcreteImpl and returns it as a Service.

下面,我们通过图 11.1 来描述一下该设计模式的结构。如你所见 Application 类是通过 Service 接口来使用 ConcreteImpl 类的。然而,Application 类还是必须要构造 ConcreteImpl 类实例。于是,为了避免在源代码层次上引入对 ConcreteImpl 类具体实现的依赖,我们现在让 Application 类去调用 ServiceFactory 接口的 makeSvc 方法。这个方法就由 ServiceFactoryImpl 类来具体提供,它是 ServiceFactory 的一个衍生类。该方法的具体实现就是初始化一个 ConcreteImpl 类的实例,并且将其以 Service 类型返回。

Use of the Abstract Factory pattern to manage the dependency

The curved line in Figure 11.1 is an architectural boundary. It separates the abstract from the concrete. All source code dependencies cross that curved line pointing in the same direction, toward the abstract side.

图 11.1 中间的那条曲线代表了软件架构中的抽象层与具体实现层的边界。在这里,所有跨越这条边界源代码级别的依赖关系都应该是单向的,即具体实现层依赖抽象层。

The curved line divides the system into two components: one abstract and the other concrete. The abstract component contains all the high-level business rules of the application. The concrete component contains all the implementation details that those business rules manipulate.

这条曲线将整个系统划分为两部分组件:抽象接口与其具体实现。抽象接口组件中包含了应用的所有高阶业务规则,而具体实现组件中则包括了所有这些业务规则所需要做的具体操作及其相关的细节信息。

Note that the flow of control crosses the curved line in the opposite direction of the source code dependencies. The source code dependencies are inverted against the flow of control—which is why we refer to this principle as Dependency Inversion.

请注意,这里的控制流跨越架构边界的方向与源代码依赖关系跨越该边界的方向正好相反,源代码依赖方向永远是控制流方向的反转——这就是 DIP 被称为依赖反转原则的原因。

CONCRETE COMPONENTS 具体实现组件
The concrete component in Figure 11.1 contains a single dependency, so it violates the DIP. This is typical. DIP violations cannot be entirely removed, but they can be gathered into a small number of concrete components and kept separate from the rest of the system.

在图 11.1 中,具体实现组件的内部仅有一条依赖关系,这条关系其实是违反 DIP 的。这种情况很常见,我们在软件系统中并不可能完全消除违反 DIP 的情见通常只需要把它们集中于少部分的具体实现组件中,将其与系统的其他部分隔离即可。

Most systems will contain at least one such concrete component—often called main because it contains the main1 function. In the case illustrated in Figure 11.1, the main function would instantiate the ServiceFactoryImpl and place that instance in a global variable of type ServiceFactory. The Application would then access the factory through that global variable.

绝大部分系统中都至少存在一个具体实现组件 我们一般称之为 main 组化 因为它们通常是 main 函数所在之处。在 图 11.1 中,函数应该负责创建 ServiceFactoryImpl 实例,并将其赋值给类型为 ServiceFactory 的全局变量,以便让 Application 类通过这个全局变量来进行相关调用。

CONCLUSION 本章小结
As we move forward in this book and cover higher-level architectural principles, the DIP will show up again and again. It will be the most visible organizing principle in our architecture diagrams. The curved line in Figure 11.1 will become the architectural boundaries in later chapters. The way the dependencies cross that curved line in one direction, and toward more abstract entities, will become a new rule that we will call the Dependency Rule.

随着本书内容的进一步深入,以及我们对高级系统架构理论的进一步讨论,DIP 出现的频率将会越来越高。在系统架构图中,DIP 通常是最显而易见的组织原此我们在后续章节中会把图 11.1 中的那条曲线称为架构边界,而跨越边界的、朝向抽象层的单向依赖关系则会成为一个设计守则——依赖守则。

13
3

The Interface Segregation Principle (ISP) derives its name from the diagram shown in Figure 10.1.

“接口隔离原则(ISP)”这个名字来自图 10.1 所示的这种软件结构。

The Interface Segregation Principle

In the situation illustrated in Figure 10.1, there are several users who use the operations of the OPS class. Let’s assume that User1 uses only op1, User2 uses only op2, and User3 uses only op3.

在图 10.1 所描绘的应用中,有多个用户需要操作 OPS 类。现在,我们假设这里的 User1 只需要使用 op1,User2 只需要使用 op2,User3 只需要使用 op3。

Now imagine that OPS is a class written in a language like Java. Clearly, in that case, the source code of User1 will inadvertently depend on op2 and op3, even though it doesn’t call them. This dependence means that a change to the source code of op2 in OPS will force User1 to be recompiled and redeployed, even though nothing that it cared about has actually changed.

在这种情况下,如果 OPS 类是用 Java 编程语言编写的,那么很明显,User1 虽然不需要调用 op2、op3,但在源代码层次上也与它们形成依赖关系。这种依赖意味着我们对 OPS 代码中 op2 所做的任何修改,即使不会影响到 User1 的功能,也会导致它需要被重新编译和部署。

This problem can be resolved by segregating the operations into interfaces as shown in Figure 10.2.

Again, if we imagine that this is implemented in a statically typed language like Java, then the source code of User1 will depend on U1Ops, and op1, but will not depend on OPS. Thus a change to OPS that User1 does not care about will not cause User1 to be recompiled and redeployed.

这个问题可以通过将不同的操作隔离成接口来解决,具体如图 10.2 所示。同样,我们也假设这个例子是用 Java 这种静态类型语言来实现的,那么现在 User1 的源代码会依赖于 U1Ops 和 op1,但不会依赖于 OPS。这样一来,我们之后对 OPS 做的修改只要不影响到 User1 的功能,就不需要重新编译和部署 User1 了。

Segregated operations

ISP AND LANGUAGE ISP 与编程语言
Clearly, the previously given description depends critically on language type. Statically typed languages like Java force programmers to create declarations that users must import, or use, or otherwise include. It is these included declarations in source code that create the source code dependencies that force recompilation and redeployment.

很明显,上述例子很大程度上也依赖于我们所釆用的编程语言:对于 Java 这样的静态类型语言来说,它们需要程序员显式地 import、use 或者 include 其实现功能所需要的源代码。而正是这些语句带来了源代码之间的依赖关系,这也就导致了某些模块需要被重新编译和重新部署。

In dynamically typed languages like Ruby and Python, such declarations don’t exist in source code. Instead, they are inferred at runtime. Thus there are no source code dependencies to force recompilation and redeployment. This is the primary reason that dynamically typed languages create systems that are more flexible and less tightly coupled than statically typed languages.

而对于 Ruby 和 Python 这样的动态类型语言来说,源代码中就不存在这样的声明,它们所用对象的类型会在运行时被推演出来,所以也就不存在强制重新编译重新部署的必要性。这就是动态类型语言要比静态类型语言更灵活、耦合度更松的原因。

This fact could lead you to conclude that the ISP is a language issue, rather than an architecture issue.

当然,如果仅仅就这样说的话,读者可能会误以为 ISP 只是一个与编程语言的选择紧密相关的设计原则,而非软件架构问题,这就错了。

ISP AND ARCHITECTURE ISP 与软件架构
If you take a step back and look at the root motivations of the ISP, you can see a deeper concern lurking there. In general, it is harmful to depend on modules that contain more than you need. This is obviously true for source code dependencies that can force unnecessary recompilation and redeployment—but it is also true at a much higher, architectural level.

回顾一下 ISP 最初的成因:在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源代码层次来说,这样的依赖关系会导致不必要的重新编译和重新部署,对更高层次的软件架构设计来说,问题也是类似的。

Consider, for example, an architect working on a system, S. He wants to include a certain framework, F, into the system. Now suppose that the authors of F have bound it to a particular database, D. So S depends on F. which depends on D (Figure 10.3).

例如,我们假设某位软件架构师在设计系统 S 时,想要在该系统中引入某个框架 F。这时候,假设框架 F 的作者又将其捆绑在一个特定的数据库 D 上,那么就形成了 S 依赖于 F,F 又依赖于 D 的关系(参见图 10.3)。

A problematic architecture

Now suppose that D contains features that F does not use and, therefore, that S does not care about. Changes to those features within D may well force the redeployment of F and, therefore, the redeployment of S. Even worse, a failure of one of the features within D may cause failures in F and S.

在这种情况下,如果 D 中包含了 F 不需要的功能,那么这些功能同样也会是 S 不需要的。而我们对 D 中的这些功能的修改将会导致 F 需要被重新部署,后者又会导致 S 的重新部署。更糟糕的是,D 中一个无关功能的错误也可能会导致 F 和 S 运行出错。

CONCLUSION 本章小结
The lesson here is that depending on something that carries baggage that you don’t need can cause you troubles that you didn’t expect.

本章所讨论的设计原则告诉我们:任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

We’ll explore this idea in more detail when we discuss the Common Reuse Principle in Chapter 13, “Component Cohesion.”

我们将会在第 13 章“组件聚合”中讨论共同复用原则的时候再来深入探讨更多相关的细节。

12
3

In 1988, Barbara Liskov wrote the following as a way of defining subtypes.

1988 年,Barbara Liskov 在描述如何定义子类型时写下了这样一段话:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.1

这里需要的是一种可替换性:如果对于每个类型是 S 的对象 o1 都存在一个类型为 T 的对象 o2,能使操作 T 类型的程序 P 在用 o2 替换 o1 时行为保持不变,我们就可以将 S 称为 T 的子类型。

To understand this idea, which is known as the Liskov Substitution Principle (LSP), let’s look at some examples.

为了让读者理解这段话中所体现的设计理念,也就是里氏替换原则(LSP),我们可以来看几个例子。

GUIDING THE USE OF INHERITANCE 继承的使用指导
Imagine that we have a class named License, as shown in Figure 9.1. This class has a method named calcFee(), which is called by the Billing application. There are two “subtypes” of License: PersonalLicense and BusinessLicense. They use different algorithms to calculate the license fee.

假设我们有一个 License 类,其结构如图 9.1 所示。该类中有一个名为 calcFee() 的方法,该方法将由 Billing 应用程序来调用。而 License 类有两个“子类型”:PersonalLicense 与 BusinessLicense,这两个类会用不同的算法来计算授权费用。

License, and its derivatives, conform to LSP

This design conforms to the LSP because the behavior of the Billing application does not depend, in any way, on which of the two subtypes it uses. Both of the subtypes are substitutable for the License type.

上述设计是符合 LSP 原则的,因为 Billing 应用程序的行为并不依赖于其使用的任何一个衍生类。也就是说,这两个衍生类的对象都是可以用来替换 License 类对象的。

THE SQUARE/RECTANGLE PROBLEM 正方形/长方形问题
The canonical example of a violation of the LSP is the famed (or infamous, depending on your perspective) square/rectangle problem (Figure 9.2).

正方形/长方形问题是个著名(或者说臭名远扬)的违反 LSP 的设计案例(这个问题的结构如图 9.2 所示)。

The infamous square/rectangle problem

In this example, Square is not a proper subtype of Rectangle because the height and width of the Rectangle are independently mutable; in contrast, the height and width of the Square must change together. Since the User believes it is communicating with a Rectangle, it could easily get confused. The following code shows why:

在这个案例中,Square 类并不是 Rectangle 类的子类型,因为 Rectangle 类的高和宽可以分别修改,而 Square 类的高和宽则必须一同修改。由于 User 类 始终认为自己在操作 Rectangle 类,因此会带来一些混淆。例如在下面的代码中:

Rectangle r = …
r.setW(5);
r.setH(2);
assert(r.area() == 10);
If the … code produced a Square, then the assertion would fail.

很显然,如果上述代码在…处返回的是 Square 类,则最后的这个 assert 是不会成立的。

The only way to defend against this kind of LSP violation is to add mechanisms to the User (such as an if statement) that detects whether the Rectangle is, in fact, a Square. Since the behavior of the User depends on the types it uses, those types are not substitutable.

如果想要防范这种违反 LSP 的行为,唯一的办法就是在 user 类中增加用于区分 Rectangle 和 Square 的检测逻辑(例如增加 if 语句)。但这样一来,user 为的行为又将依赖于它所使用的类,这两个类就不能互相替换了。

LSP AND ARCHITECTURE LSP 与软件架构
In the early years of the object-oriented revolution, we thought of the LSP as a way to guide the use of inheritance, as shown in the previous sections. However, over the years the LSP has morphed into a broader principle of software design that pertains to interfaces and implementations.

在面向对象这场编程革命兴起的早期,我们的普遍认知正如上文所说,认为 LSP 只不过是指导如何使用继承关系的一种方法,然而随着时间的推移,LSP 逐渐演变成了一种更广泛的、指导接口与其实现方式的设计原则。

The interfaces in question can be of many forms. We might have a Java-style interface, implemented by several classes. Or we might have several Ruby classes that share the same method signatures. Or we might have a set of services that all respond to the same REST interface.

这里提到的接口可以有多种形式——可以是 Java 风格的接口,具有多个实现类;也可以像 Ruby 一样,几个类共用一样的方法签名,甚至可以是几个服务响应同一个 REST 接口。

In all of these situations, and more, the LSP is applicable because there are users who depend on well-defined interfaces, and on the substitutability of the implementations of those interfaces.

LSP 适用于上述所有的应用场景,因为这些场景中的用户都依赖于一种接口,并且都期待实现该接口的类之间能具有可替换性。

The best way to understand the LSP from an architectural viewpoint is to look at what happens to the architecture of a system when the principle is violated.

想要从软件架构的角度来理解 LSP 的意义,最好的办法还是来看几个反面案例。

EXAMPLE LSP VIOLATION 违反 LSP 的案例
Assume that we are building an aggregator for many taxi dispatch services. Customers use our website to find the most appropriate taxi to use, regardless of taxi company. Once the customer makes a decision, our system dispatches the chosen taxi by using a restful service.

假设我们现在正在构建一个提供出租车调度服务的系统。在该系统中,用户可以通过访问我们的网站,从多个出租车公司内寻找最适合自己的出租车。当用户选定车子时,该系统会通过调用 restful 服务接口来调度这辆车。

Now assume that the URI for the restful dispatch service is part of the information contained in the driver database. Once our system has chosen a driver appropriate for the customer, it gets that URI from the driver record and then uses it to dispatch the driver.

接下来,我们再假设该 restful 调度服务接口的 URI 被存储在司机数据库中。一旦该系统选中了最合适的出租车司机,它就会从司机数据库的记录中读取相应的 URI 信息,并通过调用这个 URI 来调度汽车。

Suppose Driver Bob has a dispatch URI that looks like this:

也就是说,如果司机 Bob 的记录中包含如下调度 URI:

purplecab.com/driver/Bob
Our system will append the dispatch information onto this URI and send it with a PUT, as follows:

那么,我们的系统就会将调度信息附加在这个 URI 上,并发送这样一个 PUT 请求:

purplecab.com/driver/Bob
/pickupAddress/24 Maple St.
/pickupTime/153
/destination/ORD
Clearly, this means that all the dispatch services, for all the different companies, must conform to the same REST interface. They must treat the pickupAddress, pickupTime, and destination fields identically.

很显然,这意味着所存参与该调度服务的公司都必须遵守同样的 REST 接口,它们必须用同样的方式处理 pickupAddress、pickupTime 和 destination 字段。

Now suppose the Acme taxi company hired some programmers who didn’t read the spec very carefully. They abbreviated the destination field to just dest. Acme is the largest taxi company in our area, and Acme’s CEO’s ex-wife is our CEO’s new wife, and … Well, you get the picture. What would happen to the architecture of our system?

接下来,我们再假设 Acme 出租车公司现在招聘的程序员由于没有仔细阅读上述接口定义,结果将 destination 字段缩写成了 dest。而 Acme 又是本地最大的出租车公司,另外,Acme CEO 的前妻不巧还是我们 CEO 的新欢……你懂的!这这会对系统的架构造成什么影响呢?

Obviously, we would need to add a special case. The dispatch request for any Acme driver would have to be constructed using a different set of rules from all the other drivers.

显然,我们需要为系统增加一类特殊用例,以应对 Acme 司机的调度请求。这必须要用另外一套规则来构建。

The simplest way to accomplish this goal would be to add an if statement to the module that constructed the dispatch command:

最简单的做法当然是增加一条 if 语句:

if (driver.getDispatchUri().startsWith("acme.com"))…
But, of course, no architect worth his or her salt would allow such a construction to exist in the system. Putting the word “acme” into the code itself creates an opportunity for all kinds of horrible and mysterious errors, not to mention security breaches.

然而很明显,任何一个称职的软件架构师都不会允许这样一条语句出现在自己的系统中。因为直接将“acme”这样的字串写入代码会留下各种各样神奇又可怕的错误隐患,甚至会导致安全问题。

For example, what if Acme became even more successful and bought the Purple Taxi company. What if the merged company maintained the separate brands and the separate websites, but unified all of the original companies’ systems? Would we have to add another if statement for “purple”?

例如,Acme 也许会变得更加成功,最终收购了 Purple 出租车公司。然后,它们在保留了各自名字的同时却统一了彼此的计算机系统。在这种情况下,系统中难道还要再增加一条“purple”的特例吗?

Our architect would have to insulate the system from bugs like this by creating some kind of dispatch command creation module that was driven by a configuration database keyed by the dispatch URI. The configuration data might look something like this:

软件架构师应该创建一个调度请求创建组件,并让该组件使用一个配置数据库来保存 URI 组装格式,这样的方式可以保护系统不受外界因素变化的影响。例如其配置信息可以如下:

URI Dispatch Format
Acme.com /pickupAddress/%s/pickupTime/%s/dest/%s
. /pickupAddress/%s/pickupTime/%s/destination/%s
And so our architect has had to add a significant and complex mechanism to deal with the fact that the interfaces of the restful services are not all substitutable.

但这样一来,软件架构师就需要通过增加一个复杂的组件来应对并不完全能实现互相替换的 restful 服务接口。

CONCLUSION 本章小结
The LSP can, and should, be extended to the level of architecture. A simple violation of substitutability, can cause a system’s architecture to be polluted with a significant amount of extra mechanisms.

LSP 可以且应该被应用于软件架构层面,因为一旦违背了可替换也该系统架构就不得不为此增添大量复杂的应对机制。

11
3

The Open-Closed Principle (OCP) was coined in 1988 by Bertrand Meyer.1 It says:

开闭原则(OCP)是 Bertrand Meyer 在 1988 年提出的,该设计原则认为:

A software artifact should be open for extension but closed for modification.

设计良好的计算机软件应该易于扩展,同时抗拒修改。

In other words, the behavior of a software artifact ought to be extendible, without having to modify that artifact.

换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。

This, of course, is the most fundamental reason that we study software architecture. Clearly, if simple extensions to the requirements force massive changes to the software, then the architects of that software system have engaged in a spectacular failure.

其实这也是我们研究软件架构的根本目的。如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的。

Most students of software design recognize the OCP as a principle that guides them in the design of classes and modules. But the principle takes on even greater significance when we consider the level of architectural components.

尽管大部分软件设计师都已经认可了 OCP 是设计类与模块时的重要原则,但是在软件架构层面,这项原则的意义则更为重大。

A thought experiment will make this clear.

下面,让我们用一个思想实验来做一些说明。

A THOUGHT EXPERIMENT 思想实验
Imagine, for a moment, that we have a system that displays a financial summary on a web page. The data on the page is scrollable, and negative numbers are rendered in red.

假设我们现在要设计一个在 Web 页面上展示财务数据的系统,页面上的数据要可以滚动显示,其中负值应显示为红色。

Now imagine that the stakeholders ask that this same information be turned into a report to be printed on a black-and-white printer. The report should be properly paginated, with appropriate page headers, page footers, and column labels. Negative numbers should be surrounded by parentheses.

接下来,该系统的所有者又要求同样的数据需要形成一个报表,该报表要能用黑白打印机打印,并且其报表格式要得到合理分页,每页都要包含页头、页尾及栏目名。同时,负值应该以括号表示。

Clearly, some new code must be written. But how much old code will have to change?

显然,我们需要增加一些代码来完成这个要求。但在这里我们更关注的问题是,满足新的要求需要更改多少旧代码。

A good software architecture would reduce the amount of changed code to the barest minimum. Ideally, zero.

一个好的软件架构设计师会努力将旧代码的修改需求量降至最小,甚至为 0。

How? By properly separating the things that change for different reasons (the Single Responsibility Principle), and then organizing the dependencies between those things properly (the Dependency Inversion Principle).

但该如何实现这一点呢?我们可以先将满足不同需求的代码分组(即 SRP),然后再来调整这些分组之间的依赖关系(即 DIP)。

By applying the SRP, we might come up with the data-flow view shown in Figure 8.1. Some analysis procedure inspects the financial data and produces reportable data, which is then formatted appropriately by the two reporter processes.

利用 SRP,我们可以按图 8.1 中所展示的方式来处理数据流。即先用一段分析程序处理原始的财务数据,以形成报表的数据结构,最后再用两个不同的报表生成器来产生报表。

Applying the SRP

The essential insight here is that generating the report involves two separate responsibilities: the calculation of the reported data, and the presentation of that data into a web- and printer-friendly form.

这里的核心就是将应用生成报表的过程拆成两个不同的操作。即先计算出报表数据,再生成具体的展示报表(分别以网页及纸质的形式展示)。

Having made this separation, we need to organize the source code dependencies to ensure that changes to one of those responsibilities do not cause changes in the other. Also, the new organization should ensure that the behavior can be extended without undo modification.

接下来,我们就该修改其源代码之间的依赖关系了。这样做的目的是保证其中一个操作被修改之后不会影响到另外一个操作。同时,我们所构建的新的组织形式应该保证该程序后续在行为上的扩展都无须修改现有代码。

We accomplish this by partitioning the processes into classes, and separating those classes into components, as shown by the double lines in the diagram in Figure 8.2. In this figure, the component at the upper left is the Controller. At the upper right, we have the Interactor. At the lower right, there is the Database. Finally, at the lower left, there are four components that represent the Presenters and the Views.

在具体实现上,我们会将整个程序进程划分成一系列的类,然后再将这些类分割成不同的组件。下面,我们用图 8.2 中的那些双线框来具体描述一下整个实现。在这个图中,左上角的组件是 Controller,右上角是 Interactor,右下角是 Database,左下角则有四个组件分别用于代表不同的 Presenter 和 View。

Partitioning the processes into classes and separating the classes into components

Classes marked with are interfaces; those marked with are data structures. Open arrowheads are using relationships. Closed arrowheads are implements or inheritance relationships.

在图 8.2 中,用 标记的类代表接口,用 标记的则代表数据结构;开放箭头指代的是使用关系,闭合箭头则指代了实现与继承关系。

The first thing to notice is that all the dependencies are source code dependencies. An arrow pointing from class A to class B means that the source code of class A mentions the name of class B, but class B mentions nothing about class A. Thus, in Figure 8.2, FinancialDataMapper knows about FinancialDataGateway through an implements relationship, but FinancialDataGateway knows nothing at all about FinancialDataMapper.

首先,我们在图 8.2 中看到的所有依赖关系都是其源代码中存在的依赖关系。这里,从类 A 指向类 B 的箭头意味着 A 的源代码中涉及了 B,但 是 B 的源代码中并不涉及 A。因此在图 8.2 中,FinancialDataMapper 在实现接口时需要知道 FinancialDataGateway 的实现,而 FinancialDataGateway 则完全知道 FinancialDataMapper 的实现。

The next thing to notice is that each double line is crossed in one direction only. This means that all component relationships are unidirectional, as shown in the component graph in Figure 8.3. These arrows point toward the components that we want to protect from change.

其次,这里很重要的一点是这些双线框的边界都是单向跨越的。也就是说,上图中所有组件之间的关系都是单向依赖的,如图 8.3 所示,图中的箭头都指向那些我们不想经常更改的组件。

The component relationships are unidirectional

Let me say that again: If component A should be protected from changes in component B, then component B should depend on component A.

让我们再来复述一下这里的设计原则:如果 A 组件不想被 B 组件上发生的修改所影响,那么就应该让 B 组件依赖于 A 组件。

We want to protect the Controller from changes in the Presenters. We want to protect the Presenters from changes in the Views. We want to protect the Interactor from changes in—well, anything.

所以现在的情况是,我们不想让发生在 Presenter 上的修改影响到 Controller,也不想让发生在 View 上的修改影响到 Presenter。而最关键的是,我们不想让任何修改影响到 Interactor。

The Interactor is in the position that best conforms to the OCP. Changes to the Database, or the Controller, or the Presenters, or the Views, will have no impact on the Interactor.

其中,Interactor 组件是整个系统中最符合 OCP 的。发生在 Database、Controller、Presenter 甚至 View 上的修改都不会影响到 Interactor。

Why should the Interactor hold such a privileged position? Because it contains the business rules. The Interactor contains the highest-level policies of the application. All the other components are dealing with peripheral concerns. The Interactor deals with the central concern.

为什么 Interactor 会被放在这么重要的位置上呢?因为它是该程序的业务逻辑所在之处,Interactor 中包含了其最高层次的应用策略。其他组件都只是负责处理周边的辅助逻辑,只有 Interactor 才是核心组件。

Even though the Controller is peripheral to the Interactor, it is nevertheless central to the Presenters and Views. And while the Presenters might be peripheral to the Controller, they are central to the Views.

虽然 Controller 组件只是 Interactor 的附属品,但它却是 Presenter 和 View 所服务的核心。同样的,虽然 Presenter 组件是 Controller 的附属品,但它却是 View 所服务的核心。

Notice how this creates a hierarchy of protection based on the notion of “level.” Interactors are the highest-level concept, so they are the most protected. Views are among the lowest-level concepts, so they are the least protected. Presenters are higher level than Views, but lower level than the Controller or the Interactor.

另外需要注意的是,这里利用“层级”这个概念创造了一系列不同的保护层级。譬如,Interactor 是最高层的抽象,所以它被保护得最严密,而 Presenter 比 View 时层级高,但比 Controller 和 Interactor 的层级低。

This is how the OCP works at the architectural level. Architects separate functionality based on how, why, and when it changes, and then organize that separated functionality into a hierarchy of components. Higher-level components in that hierarchy are protected from the changes made to lower-level components.

以上就是我们在软件架构层次上对 OCP 这一设计原则的应用。软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。

DIRECTIONAL CONTROL 依赖方向的控制
If you recoiled in horror from the class design shown earlier, look again. Much of the complexity in that diagram was intended to make sure that the dependencies between the components pointed in the correct direction.

如果刚刚的类设计把你吓着了,别害怕!你刚刚在图表中所看到的复杂度是我们想要对组件之间的依赖方向进行控制而产生的。

For example, the FinancialDataGateway interface between the FinancialReportGenerator and the FinancialDataMapper exists to invert the dependency that would otherwise have pointed from the Interactor component to the Database component. The same is true of the FinancialReportPresenter interface, and the two View interfaces.

例如,FinanciaIReportGenerator 和 FinancialDataMapper 之间的 Financial Da taGateway 接口是为了反转 Interactor 与 Database 之间的依赖关系而产生的。同样的,FinancialReportPresenter 接口与两个 View 接口之间也类似于这种情况。

INFORMATION HIDING 信息隐藏
The FinancialReportRequester interface serves a different purpose. It is there to protect the FinancialReportController from knowing too much about the internals of the Interactor. If that interface were not there, then the Controller would have transitive dependencies on the FinancialEntities.

当然,FinancialReportRequester 接口的作用则完全不同,它的作用是保护 FinancialReportController 不过度依赖于 Interactor 的内部细节。如果没有这个接口,则 Controller 将会传递性地依赖于 FinancialEntities。

Transitive dependencies are a violation of the general principle that software entities should not depend on things they don’t directly use. We’ll encounter that principle again when we talk about the Interface Segregation Principle and the Common Reuse Principle.

这种传递性依赖违反了“软件系统不应该依赖其不直接使用的组件”这一基本原则。之后,我们会在讨论接口隔离原则和共同复用原则的时候再次提到这一点。

So, even though our first priority is to protect the Interactor from changes to the Controller, we also want to protect the Controller from changes to the Interactor by hiding the internals of the Interactor.

所以,虽然我们的首要目的是为了让 Interactor 屏蔽掉发生在 Controller 上的修改,但也需要通过隐藏 Interactor 内部细节的方法来让其屏蔽掉来自 Controller 的依赖。

CONCLUSION 本章小结
The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change. This goal is accomplished by partitioning the system into components, and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.

OCP 是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

10
3

Of all the SOLID principles, the Single Responsibility Principle (SRP) might be the least well understood. That’s likely because it has a particularly inappropriate name. It is too easy for programmers to hear the name and then assume that it means that every module should do just one thing.

SRP 是 SOLID 五大设计原则中最容易被误解的一个。也许是名字的原因,很多程序员根据 SRP 这个名字想当然地认为这个原则就是指:每个模块都应该只做一件事。

Make no mistake, there is a principle like that. A function should do one, and only one, thing. We use that principle when we are refactoring large functions into smaller functions; we use it at the lowest levels. But it is not one of the SOLID principles—it is not the SRP.

没错,后者的确也是一个设计原则,即确保一个函数只完成一个功能。我们在将大型函数重构成小函数时经常会用到这个原则,但这只是一个面向底层实现细节的设计原则,并不是 SRP 的全部。

Historically, the SRP has been described this way:

在历史上,我们曾经这样描述 SRP 这一设计原则:

A module should have one, and only one, reason to change.

任何一个软件模块都应该有且仅有一个被修改的原因。

Software systems are changed to satisfy users and stakeholders; those users and stakeholders are the “reason to change” that the principle is talking about. Indeed, we can rephrase the principle to say this:

在现实环境中,软件系统为了满足用户和所有者的要求,必然要经常做出这样那样的修改。而该系统的用户或者所有者就是该设计原则中所指的“被修改的原因”。所以,我们也可以这样描述 SRP:

A module should be responsible to one, and only one, user or stakeholder.

任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。

Unfortunately, the words “user” and “stakeholder” aren’t really the right words to use here. There will likely be more than one user or stakeholder who wants the system changed in the same way. Instead, we’re really referring to a group—one or more people who require that change. We’ll refer to that group as an actor.

不过,这里的“用户”和 “系统利益相关者”在用词上也并不完全准确,它们很有可能指的是一个或多个用户和利益相关者,只要这些人希望对系统进行的变更是相似的,就可以归为一类——一个或多个有共同需求的人。在这里,我们将其称为行为者(actor)。

Thus the final version of the SRP is:

所以,对于 SRP 的最终描述就变成了:

A module should be responsible to one, and only one, actor.

任何一个软件模块都应该只对某一类行为者负责。

Now, what do we mean by the word “module”? The simplest definition is just a source file. Most of the time that definition works fine. Some languages and development environments, though, don’t use source files to contain their code. In those cases a module is just a cohesive set of functions and data structures.

那么,上文中提到的“软件模块”究竟又是在指什么呢?大部分情况下,其最简单的定义就是指一个源代码文件。然而,有些编程语言和编程环境并不是用源代码文件来存储程序的。在这些情况下,“软件模块”指的就是一组紧密相关的函数和数据结构。

That word “cohesive” implies the SRP. Cohesion is the force that binds together the code responsible to a single actor.

在这里,“相关”这个词实际上就隐含了 SRP 这一原则。代码与数据就是靠着与某一类行为者的相关性被组合在一起的。

Perhaps the best way to understand this principle is by looking at the symptoms of violating it.

或许,理解这个设计原则最好的办法就是计大家来看一些反面案例。

SYMPTOM 1: ACCIDENTAL DUPLICATION 反面案例 1:重复的假象
My favorite example is the Employee class from a payroll application. It has three methods: calculatePay(), reportHours(), and save() (Figure 7.1).

这是我最喜欢举的一个例子:某个工资管理程序中的 Employee 类有三个函数 calculatePay()、reportHours() 和 save()(见图 7.1)。

The Employee class

This class violates the SRP because those three methods are responsible to three very different actors.

如你所见,这个类的三个函数分别对应的是三类非常不同的行为者,违反了 SRP 设计原则。

The calculatePay() method is specified by the accounting department, which reports to the CFO.
The reportHours() method is specified and used by the human resources department, which reports to the COO.
The save() method is specified by the database administrators (DBAs), who report to the CTO.
calculatePay() 函数是由财务部门制定的,他们负责向 CFO 汇报。
reportHours() 函数是由人力资源部门制定并使用的,他们负责向 COO 汇报。
save() 函数是由 DBA 制定的,他们负责向 CTO 汇报。
By putting the source code for these three methods into a single Employee class, the developers have coupled each of these actors to the others. This coupling can cause the actions of the CFO’s team to affect something that the COO’s team depends on.

这三个函数被放在同一个源代码文件,即同一个 Employee 类中,程序员这样 做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致 CFO 团队的命令影响到 C 00 团队所依赖的功能。

For example, suppose that the calculatePay() function and the reportHours() function share a common algorithm for calculating non-overtime hours. Suppose also that the developers, who are careful not to duplicate code, put that algorithm into a function named regularHours() (Figure 7.2).

例如,calculatePay() 函数和 reportHours() 函数使用同样的逻辑来计算正常工作时数。程序员为了避免重复编码,通常会将该算法单独实现为一个名为 regularHours() 的函数(见图 7.2)。

Shared algorithm

Now suppose that the CFO’s team decides that the way non-overtime hours are calculated needs to be tweaked. In contrast, the COO’s team in HR does not want that particular tweak because they use non-overtime hours for a different purpose.

接下来,假设 CFO 团队需要修改正常工作时数的计算方法,而 COO 带领的 HR 团队不需要这个修改,因为他们对数据的用法是不同的。

A developer is tasked to make the change, and sees the convenient regularHours() function called by the calculatePay() method. Unfortunately, that developer does not notice that the function is also called by the reportHours() function.

这时候,负责这项修改的程序员会注意到 calculatePay() 函数调用了 regularHours() 函数,但可能不会注意到该函数会同时被 reportHours() 调用。

The developer makes the required change and carefully tests it. The CFO’s team validates that the new function works as desired, and the system is deployed.

于是,该程序员就这样按照要求进行了修改,同时 CFO 团队的成员验证了新算法工作正常。这项修改最终被成功部署上线了。

Of course, the COO’s team doesn’t know that this is happening. The HR personnel continue to use the reports generated by the reportHours() function—but now they contain incorrect numbers. Eventually the problem is discovered, and the COO is livid because the bad data has cost his budget millions of dollars.

但是,COO 团队显然完全不知道这些事情的发生,HR 仍然在使用 reportHours() 产生的报表,随后就会发现他们的数据出错了!最终这个问题让 COO 十分愤怒,因为这些错误的数据给公司造成了几百万美元的损失。

We’ve all seen things like this happen. These problems occur because we put code that different actors depend on into close proximity. The SRP says to separate the code that different actors depend on.

与此类似的事情我们肯定多多少少都经历过。这类问题发生的根源就是因为我们将不同行为者所依赖的代码强凑到了一起。对此,SRP 强调这类代码一定要被分开。

SYMPTOM 2: MERGES 反面案例 2:代码合井
It’s not hard to imagine that merges will be common in source files that contain many different methods. This situation is especially likely if those methods are responsible to different actors.

一个拥有很多函数的源代码文件必然会经历很多次代码合并,该文件中的这些函数分别服务不同行为者的情况就更常见了。

For example, suppose that the CTO’s team of DBAs decides that there should be a simple schema change to the Employee table of the database. Suppose also that the COO’s team of HR clerks decides that they need a change in the format of the hours report.

例如,CTO 团队的 DBA 决定要对 Employee 数据库表结构进行简单修改。与此同时,COO 团队的 HR 需要修改工作时数报表的格式。

Two different developers, possibly from two different teams, check out the Employee class and begin to make changes. Unfortunately their changes collide. The result is a merge.

这样一来,就很可能出现两个来自不同团队的程序员分别对 Employee 类进行 修改的情况。不出意外的话,他们各自的修改一定会互相冲突,这就必须要进行代码合并。

I probably don’t need to tell you that merges are risky affairs. Our tools are pretty good nowadays, but no tool can deal with every merge case. In the end, there is always risk.

In our example, the merge puts both the CTO and the COO at risk. It’s not inconceivable that the CFO could be affected as well.

在这个例子中,这次代码合并不仅有可能让 CTO 和 COO 要求的功能出错,甚至连 CFO 原本正常的功能也可能收到影响。

There are many other symptoms that we could investigate, but they all involve multiple people changing the same source file for different reasons.

事实上,这样的案例还有很多,我们就不一一列举了。它们的一个共同点是,多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。

Once again, the way to avoid this problem is to separate code that supports different actors.

而避免这种问题产生的方法就是将服务不同行为者的代码进行切分。

SOLUTIONS 解决方案
There are many different solutions to this problem. Each moves the functions into different classes.

我们有很多不同的方法可以用来解决上面的问题,每一种方法都需要将相关的函数划分成不同的类。

Perhaps the most obvious way to solve the problem is to separate the data from the functions. The three classes share access to EmployeeData, which is a simple data structure with no methods (Figure 7.3). Each class holds only the source code necessary for its particular function. The three classes are not allowed to know about each other. Thus any accidental duplication is avoided.

其中,最简单直接的办法是将数据与函数分离,设计三个类共同使用一个不包括函数的、十分简单的 EmployeeData 类(见图 7.3),每个类只包含与之相关的函数代码,互相不可见,这样就不存在互相依赖的情况了。

The three classes do not know about each other

The downside of this solution is that the developers now have three classes that they have to instantiate and track. A common solution to this dilemma is to use the Facade pattern (Figure 7.4).

这种解决方案的坏处在于:程序员现在需要在程序里处理三个类。另一种解决办法是使用 Facade 设计模式(见图 7.4)。

The Facade pattern

The EmployeeFacade contains very little code. It is responsible for instantiating and delegating to the classes with the functions.

这样一来,EmployeeFacade 类所需要的代码量就很少了,它仅仅包含了初始化和调用三个具体实现类的函数。

Some developers prefer to keep the most important business rules closer to the data. This can be done by keeping the most important method in the original Employee class and then using that class as a Facade for the lesser functions (Figure 7.5).

当然,也有些程序员更倾向于把最重要的业务逻辑与数据放在一起,那么我们也可以选择将最重要的函数保留在 Employee 类中,同时用这个类来调用其他没那么重要的函数(见图 7.5)。

The most important method is kept in the original Employee class and used as a Facade for the lesser functions

You might object to these solutions on the basis that every class would contain just one function. This is hardly the case. The number of functions required to calculate pay, generate a report, or save the data is likely to be large in each case. Each of those classes would have many private methods in them.

读者也许会反对上面这些解决方案,因为看上去这里的每个类中都只有一个函数。事实上并非如此,因为无论是计算工资、生成报表还是保存数据都是一个很复杂的过程,每个类都可能包含了许多私有函数。

Each of the classes that contain such a family of methods is a scope. Outside of that scope, no one knows that the private members of the family exist.

总而言之,上面的每一个类都分别容纳了一组作用于相同作用域的函数,而在该作用域之外,它们各自的私有函数是互相不可见的。

CONCLUSION 本章小结
The Single Responsibility Principle is about functions and classes—but it reappears in a different form at two more levels. At the level of components, it becomes the Common Closure Principle. At the architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries. We’ll be studying all of these ideas in the chapters to come.

单一职责原则主要讨论的是函数和类之间的关系——但是它在两个讨论层面上会以不同的形式出现。在组件层面,我们可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它则是用于奠定架构边界的变更轴心(Axis of Change)。我们在接下来的章节中会深入学习这些原则。

09
3

Good software systems begin with clean code. On the one hand, if the bricks aren’t well made, the architecture of the building doesn’t matter much. On the other hand, you can make a substantial mess with well-made bricks. This is where the SOLID principles come in.

通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。毕竟,如果建筑所使用的砖头质量不佳,那么架构所能起到的作用也会很有限。反之亦然,如果建筑的架构设计不佳,那么其所用的砖头质量再好也没有用。这就是 SOLID 设计原则所要解决的问题。

The SOLID principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected. The use of the word “class” does not imply that these principles are applicable only to object-oriented software. A class is simply a coupled grouping of functions and data. Every software system has such groupings, whether they are called classes or not. The SOLID principles apply to those groupings.

SOLID 原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如将这些类链接起来成为程序。请注意,这里虽然用到了 “类”这个词,但是并不意味着我们将要讨论的这些设计原则仅仅适用于面向对象编程。这里的类仅仅代表一种数据和函数的分组,每个软件系统都会有自己的分类系统,不管它们各自是不是将其称为“类”,事实上都是 SOLID 原则的适用领域。

The goal of the principles is the creation of mid-level software structures that:

一般情况下,我们为软件构建中层结构的主要目标如下:

Tolerate change,
Are easy to understand, and
Are the basis of components that can be used in many software systems.
使软件可容忍被改动。
使软件更容易被理解。
构建可在多个软件系统中复用的组件。
The term “mid-level” refers to the fact that these principles are applied by programmers working at the module level. They are applied just above the level of the code and help to define the kinds of software structures used within modules and components.

我们在这里之所以会使用“中层”这个词,是因为这些设计原则主要适用于那些进行模块级编程的程序员。SOLID 原则应该直接紧贴于具体的代码逻辑之上,这些原则是用来帮助我们定义软件架构中的组件和模块的。

Just as it is possible to create a substantial mess with well-made bricks, so it is also possible to create a system-wide mess with well-designed mid-level components. For this reason, once we have covered the SOLID principles, we will move on to their counterparts in the component world, and then to the principles of high-level architecture.

当然了,正如用好砖也会盖歪楼一样,采用设计良好的中层组件并不能保证系统的整体架构运作良好。正因为如此,我们在讲完 SOLID 原则之后,还会再继续针对组件的设计原则进行更进一步的讨论,将其推进到高级软件架构部分。

The history of the SOLID principles is long. I began to assemble them in the late 1980s while debating software design principles with others on USENET (an early kind of Facebook). Over the years, the principles have shifted and changed. Some were deleted. Others were merged. Still others were added. The final grouping stabilized in the early 2000s, although I presented them in a different order.

SOLID 原则的历史已经很悠久了,早在 20 世纪 80 年代末期,我在 USENET 新闻组 (该新闻组在当时就相当于今天的 Facebook)上和其他人辩论软件设计理念的时候,该设计原则就已经开始逐渐成型了。随着时间的推移,其中有一些原则得到了修改,有一些则被抛弃了,还有一些被合并了,另外也增加了一些。它们的最终形态是在 2000 年左右形成的,只不过当时采用的是另外一个展现顺序。

In 2004 or thereabouts, Michael Feathers sent me an email saying that if I rearranged the principles, their first words would spell the word SOLID—and thus the SOLID principles were born.

2004 年前后,Michael Feathers 的一封电子邮件提醒我:如果重新排列这些设计原则,那么它们的首字母可以排列成 SOLID——这就是 SOLID 原则诞生的故事。

The chapters that follow describe each principle more thoroughly. Here is the executive summary:

在这一部分中,我们会逐章地详细讨论每个设计原则,下面先来做一个简单摘要。

SRP: The Single Responsibility Principle An active corollary to Conway’s law: The best structure for a software system is heavily influenced by the social structure of the organization that uses it so that each software module has one, and only one, reason to change.
OCP: The Open-Closed Principle Bertrand Meyer made this principle famous in the 1980s. The gist is that for software systems to be easy to change, they must be designed to allow the behavior of those systems to be changed by adding new code, rather than changing existing code.
LSP: The Liskov Substitution Principle Barbara Liskov’s famous definition of subtypes, from 1988. In short, this principle says that to build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.
ISP: The Interface Segregation Principle This principle advises software designers to avoid depending on things that they don’t use.
DIP: The Dependency Inversion Principle The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.
SRP:单一职责原则。 该设计原则是某于康威圧律(Conway's Law)的一个推论——一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。
OCP:开闭原则。 该设计原则是由 Bertrand Meyer 在 20 世纪 80 年代大力推广的,其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
LSP:里氏替换原则。 该设计原则是 Barbara Liskov 在 1988 年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
ISP:接口隔离原则。 这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖。
DIP:依赖反转原则。 该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
These principles have been described in detail in many different publications1 over the years. The chapters that follow will focus on the architectural implications of these principles instead of repeating those detailed discussions. If you are not already familiar with these principles, what follows is insufficient to understand them in detail and you would be well advised to study them in the footnoted documents.

这些年来,这些设计原则在很多不同的出版物中都有过详细描述。在接下来的章节中,我们将会主要关注这些原则在软件架构上的意义,而不再重复其细节信息。如果你对这些原则并不是特别了解,那么我建议你先通过脚注中的文档熟悉一下它们,否则接下来的章节可能有点难以理解。

08
3

In many ways, the concepts of functional programming predate programming itself. This paradigm is strongly based on the l-calculus invented by Alonzo Church in the 1930s.

函数式编程所依赖的原理,在很多方而其实是早于编程本身出现的。因为函数式编程这种范式强烈依赖于 Alonzo Church 在 20 世纪 30 年代发明的 λ 演算。

SQUARES OF INTEGERS 整数平方
To explain what functional programming is, it’s best to examine some examples. Let’s investigate a simple problem: printing the squares of the first 25 integers.

我们最好还是用一个例子来解释什么是函数式编程。请看下面的这个例子:这段代码想要输出前 25 个整数的平方值。

In a language like Java, we might write the following:

如果使用 Java 语言,代码如下:

public class Squint {
public static void main(String args[]) {
for (int i=0; i<25; i++)
System.out.println(i*i);
}
}
In a language like Clojure, which is a derivative of Lisp, and is functional, we might implement this same program as follows:

下面我们改用 Clojure 语言来写这个程序,Clojure 是 LISP 语言的一种衍生体,属于函数式编程语言。其代码如下:

(println (take 25 (map (fn [x] (* x x)) (range))))
If you don’t know Lisp, then this might look a little strange. So let me reformat it a bit and add some comments.

如果读者对 LISP 不熟悉,这段代码可能看起来很奇怪。没关系,让我们换一种格式,用注释来说明一下吧:

(println ;__ Print
(take 25 ;
the first 25
(map (fn [x] (* x x)) ;__ squares
(range)))) ;___ of Integers
It should be clear that println, take, map, and range are all functions. In Lisp, you call a function by putting it in parentheses. For example, (range) calls the range function.

很明显,这里的 println、take、map 和 range 都是函数。在 LISP 中,函数是通过括号来调用的,例如(range)表达式就是在调用 range 函数。

The expression (fn [x] (* x x)) is an anonymous function that calls the multiply function, passing its input argument in twice. In other words, it computes the square of its input.

而表达式 (fn [x] (* xx)) 则是一个匿名函数,该函数用同样的值作为参数调用了乘法函数。换句话说,该函数计算的是平方值。

Looking at the whole thing again, it’s best to start with the innermost function call.

现在让我们回过头再看一下这整句代码,从最内侧的函数调用开始:

The range function returns a never-ending list of integers starting with 0.
This list is passed into the map function, which calls the anonymous squaring function on each element, producing a new never-ending list of all the squares.
The list of squares is passed into the take function, which returns a new list with only the first 25 elements.
The println function prints its input, which is a list of the first 25 squares of integers.
range 函数会返回一个从 0 开始的整数无穷列表。
然后该列表会被传入 map 函数,并针对列表中的每个元素,调用求平方值的匿名函数,产生了一个无穷多的、包含平方值的列表。
接着再将这个列表传入 take 函数,后者会返回一个仅包含前 25 个元素的 新列表。
println 函数将它的参数输出,该参数就是上面这个包含了 25 个平方值的 列表。
If you find yourself terrified by the concept of never-ending lists, don’t worry. Only the first 25 elements of those never-ending lists are actually created. That’s because no element of a never-ending list is evaluated until it is accessed.

读者不用担心上面提到的无穷列表。因为这些列表中的元素只有在被访问时才会被创建,所以实际上只有前 25 个元素是真正被创建了的。

If you found all of that confusing, then you can look forward to a glorious time learning all about Clojure and functional programming. It is not my goal to teach you about these topics here.

如果上述内容还是让读者觉得云里雾里的话,可以自行学习一下 Clojure 和函数式编程,本书的目标并不是要教你学会这门语言,因此不再展开。

Instead, my goal here is to point out something very dramatic about the difference between the Clojure and Java programs. The Java program uses a mutable variable—a variable that changes state during the execution of the program. That variable is i—the loop control variable. No such mutable variable exists in the Clojure program. In the Clojure program, variables like x are initialized, but they are never modified.

相反,我们讨论它的主要目标是要突显出 Clojure 和 Java 这两种语言之间的巨大区别。在 Java 程序中,我们使用的是可变量,即变量 i,该变量的值会随着程序执行的过程而改变,故被称为循环控制变量。而 Clojure 程序中是不存在这种变量的,变量 x 一旦被初始化之后,就不会再被更改了。

This leads us to a surprising statement: Variables in functional languages do not vary.

这句话有点出人意料:函数式编程语言中的变量(Variable)是不可变(Vary)的。

IMMUTABILITY AND ARCHITECTURE 不可变性与软件架构
Why is this point important as an architectural consideration? Why would an architect be concerned with the mutability of variables? The answer is absurdly simple: All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables. You cannot have a race condition or a concurrent update problem if no variable is ever updated. You cannot have deadlocks without mutable locks.

为什么不可变性是软件架构设计需要考虑的重点呢?为什么软件架构帅要操心变量的可变性呢?答案显而易见:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。

In other words, all the problems that we face in concurrent applications—all the problems we face in applications that require multiple threads, and multiple processors—cannot happen if there are no mutable variables.

换句话说,一切并发应用遇到的问题,一切由于使用多线程、多处理器而引起的问题,如果没有可变变量的话都不对能发工。

As an architect, you should be very interested in issues of concurrency. You want to make sure that the systems you design will be robust in the presence of multiple threads and processors. The question you must be asking yourself, then, is whether immutability is practicable.

作为一个软件架构师,当然应该要对并发问题保持高度关注。我们需要确保自己设计的系统在多线程、多处理器环境中能稳定工作。所以在这里,我们实际应该要问的问题是:不可变性是否实际可行?

The answer to that question is affirmative, if you have infinite storage and infinite processor speed. Lacking those infinite resources, the answer is a bit more nuanced. Yes, immutability can be practicable, if certain compromises are made.

如果我们能忽略存储器与处理器在速度上的限制,那么答案是肯定的。否则的话,不可变性只有在一定情况下是可行的。

Let’s look at some of those compromises.

下面让我们来看一下它具体该如何做到可行。

SEGREGATION OF MUTABILITY 可变性的隔离
One of the most common compromises in regard to immutability is to segregate the application, or the services within the application, into mutable and immutable components. The immutable components perform their tasks in a purely functional way, without using any mutable variables. The immutable components communicate with one or more other components that are not purely functional, and allow for the state of variables to be mutated (Figure 6.1).

一种常见方式是将应用程序,或者是应用程序的内部服务进行切分,划分为可变的和不可变的两种组件。不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件通信的方式来修改变量状态(参见图 6.1)。

Mutating state and transactional memory

Since mutating state exposes those components to all the problems of concurrency, it is common practice to use some kind of transactional memory to protect the mutable variables from concurrent updates and race conditions.

由于状态的修改会导致一系列并发问题的产生,所以我们通常会采用某种事务型内存来保护可变变量,避免同步更新和竞争状态的发生。

Transactional memory simply treats variables in memory the same way a database treats records on disk.1 It protects those variables with a transaction- or retry-based scheme.

事务型内存基本上与数据库保护磁盘数据的方式 1 类似,通常釆用的是事务或者重试机制。

A simple example of this approach is Clojure’s atom facility:

下面我们可以用 Clojure 中的 atom 机制来写一个简单的例子:

(def counter (atom 0)) ; initialize counter to 0
(swap! counter inc) ; safely increment counter.
In this code, the counter variable is defined as an atom. In Clojure, an atom is a special kind of variable whose value is allowed to mutate under very disciplined conditions that are enforced by the swap! function.

在这段代码中,counter 变量被定义为 atom 类型。在 Clojure 中,atom 是一类特殊的变量,它被允许在 swap!函数定义的严格条件下进行更改。

The swap! function, shown in the preceding code, takes two arguments: the atom to be mutated, and a function that computes the new value to be stored in the atom. In our example code, the counter atom will be changed to the value computed by the inc function, which simply increments its argument.

至于 swap! 函数,如同上面代码所写,它需要两个参数:一个是被用来修改的 atom 类型实例,另一个是用来计算新值的函数。在上面的代码中,inc 函数会将参数加 1 并存入 counter 这个 atom 实例。

The strategy used by swap! is a traditional compare and swap algorithm. The value of counter is read and passed to inc. When inc returns, the value of counter is locked and compared to the value that was passed to inc. If the value is the same, then the value returned by inc is stored in counter and the lock is released. Otherwise, the lock is released, and the strategy is retried from the beginning.

在这里,swap!所采用的策略是传统的比较+替换算法。即先读取 counter 变量的值,再将其传入 inc 函数。然后当 inc 函数返回时,将原先用锁保护起来的 counter 值与传入 inc 时的值进行比较。如果两边的值一致,则将 inc 函数返回的值存入 counter,释放锁。否则,先释放锁,再从头进行重试。

The atom facility is adequate for simple applications. Unfortunately, it cannot completely safeguard against concurrent updates and deadlocks when multiple dependent variables come into play. In those instances, more elaborate facilities can be used.

当然,atom 这个机制只适用于上面这种简单的应用程序,它并不适用于解决由多个相关变量同时需要更改所引发的并发更新问题和死锁问题,要想解决这些问题,我们就需要用到更复杂的机制。

The point is that well-structured applications will be segregated into those components that do not mutate variables and those that do. This kind of segregation is supported by the use of appropriate disciplines to protect those mutated variables.

这里的要点是:一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。

Architects would be wise to push as much processing as possible into the immutable components, and to drive as much code as possible out of those components that must allow mutation.

软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。

EVENT SOURCING 事件溯源
The limits of storage and processing power have been rapidly receding from view. Nowadays it is common for processors to execute billions of instructions per second and to have billions of bytes of RAM. The more memory we have, and the faster our machines are, the less we need mutable state.

随着存储和处理能力的大幅进步,现在拥有每秒可以执行数十亿条指令的处理器,以及数十亿字节内存的计算机已经很常见了。而内存越大,处理速度越快,我们对可变状态的依赖就会越少。

As a simple example, imagine a banking application that maintains the account balances of its customers. It mutates those balances when deposit and withdrawal transactions are executed.

举个简单的例子,假设某个银行应用程序需要维护客户账户余额信息,当它放行存取款事务时,就要同时负责修改余额记录。

Now imagine that instead of storing the account balances, we store only the transactions. Whenever anyone wants to know the balance of an account, we simply add up all the transactions for that account, from the beginning of time. This scheme requires no mutable variables.

如果我们不保存具体账户余额,仅仅保存事务日志,那么当有人想查询账户余额时。我们就将全部交易记录取出,并且每次都得从最开始到当下进行累计。当然,这样的设计就不需要维护任何可变变量了。

Obviously, this approach sounds absurd. Over time, the number of transactions would grow without bound, and the processing power required to compute the totals would become intolerable. To make this scheme work forever, we would need infinite storage and infinite processing power.

但显而易见,这种实现是有些不合理的。因为随着时间的推移,事务的数目会无限制增长,每次处理总额所需要的处理能力很快就会变得不能接受。如果想使这种设计永远可行的话,我们将需要无限容量的存储,以及无限的处理能力。

But perhaps we don’t have to make the scheme work forever. And perhaps we have enough storage and enough processing power to make the scheme work for the reasonable lifetime of the application.

但是可能我们并不需要这个设计永远可行,而且可能在整个程序的生命周期内,我们有足够的存储和处理能力来满足它。

This is the idea behind event sourcing.2 Event sourcing is a strategy wherein we store the transactions, but not the state. When state is required, we simply apply all the transactions from the beginning of time.

这就是事件溯源,在这种体系下,我们只存储事务记录,不存储具体状态。当需要具体状态时,我们只要从头开始计算所有的事务即可。

Of course, we can take shortcuts. For example, we can compute and save the state every midnight. Then, when the state information is required, we need compute only the transactions since midnight.

Now consider the data storage required for this scheme: We would need a lot of it. Realistically, offline data storage has been growing so fast that we now consider trillions of bytes to be small—so we have a lot of it.

在存储方面,这种架构的确需要很大的存储容量。如今离线数据存储器的增长是非常快的,现在 1 TB 对我们来说也已经不算什么了。

More importantly, nothing ever gets deleted or updated from such a data store. As a consequence, our applications are not CRUD; they are just CR. Also, because neither updates nor deletions occur in the data store, there cannot be any concurrent update issues.

更重要的是,这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是 CRUD,而是 CR。因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。

If we have enough storage and enough processor power, we can make our applications entirely immutable—and, therefore, entirely functional.

如果我们有足够大的存储量和处理能力,应用程序就可以用完全不可变的、纯函数式的方式来编程。

If this still sounds absurd, it might help if you remembered that this is precisely the way your source code control system works.

如果读者还是觉得这听起来不太靠谱,可以想想我们现在用的源代码管理程序,它们正是用这种方式工作的!

CONCLUSION 本章小结
To summarize:

下面我们来总结一下:

Structured programming is discipline imposed upon direct transfer of control.
Object-oriented programming is discipline imposed upon indirect transfer of control.
Functional programming is discipline imposed upon variable assignment.
结构化编程是多对程序控制权的直接转移的限制。
面向对象编程是对程序控制权的间接转移的限制。
函数式编程是对程序中赋值操作的限制。
Each of these three paradigms has taken something away from us. Each restricts some aspect of the way we write code. None of them has added to our power or our capabilities.

这三个编程范式都对程序员提出了新的限制。每个范式都约束了某种编写代码的方式,没有一个编程范式是在增加新能力。

What we have learned over the last half-century is what not to do.

也就是说,我们过去 50 年学到的东西主要是——什么不应该做。

With that realization, we have to face an unwelcome fact: Software is not a rapidly advancing technology. The rules of software are the same today as they were in 1946, when Alan Turing wrote the very first code that would execute in an electronic computer. The tools have changed, and the hardware has changed, but the essence of software remains the same.

我们必须面对这种不友好的现实:软件构建并不是一个迅速前进的技术。今天构建软件的规则和 1946 年阿兰·图灵写下电子计算机的第一行代码时是一样的。尽管工具变化了,硬件变化了,但是软件编程的核心没有变。

Software—the stuff of computer programs—is composed of sequence, selection, iteration, and indirection. Nothing more. Nothing less.

总而言之,软件,或者说计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加,也缺一不可。

公告栏

欢迎大家来到我的博客,我是dodoro,希望我的博客能给你带来帮助。