Skip to content

第 11 章 Systems 系统

by Dr. Kevin Dean Wampler

"Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build, and test."

"复杂性是致命的。它吸走开发者的精力,使产品难以规划、构建和测试。"

—Ray Ozzie, CTO, Microsoft Corporation

——Ray Ozzie,微软公司首席技术官

11.1 HOW WOULD YOU BUILD A CITY? 你会如何建造一座城市?

Could you manage all the details yourself? Probably not. Even managing an existing city is too much for one person. Yet, cities work (most of the time). They work because cities have teams of people who manage particular parts of the city, the water systems, power systems, traffic, law enforcement, building codes, and so forth. Some of those people are responsible for the big picture, while others focus on the details.

你能自己管理所有细节吗?可能不能。即使管理一个现有的城市也超出了一个人的能力。然而,城市运作着(大部分时间)。它们运作是因为城市有团队管理城市的特定部分:供水系统、电力系统、交通、执法、建筑规范等等。其中一些人负责全局,而另一些人专注于细节。

Cities also work because they have evolved appropriate levels of abstraction and modularity that make it possible for individuals and the "components" they manage to work effectively, even without understanding the big picture.

城市之所以运作,还因为它们已经演化出了适当的抽象层次和模块化,使得个人和他们管理的"组件"能够有效地工作,即使不理解全局。

Although software teams are often organized like that too, the systems they work on often don't have the same separation of concerns and levels of abstraction. Clean code helps us achieve this at the lower levels of abstraction. In this chapter let us consider how to stay clean at higher levels of abstraction, the system level.

尽管软件团队通常也以这种方式组织,但他们工作的系统往往没有相同层次的关注点分离和抽象层次。整洁代码帮助我们在较低的抽象层次上实现这一点。在本章中,让我们考虑如何在更高的抽象层次——系统层次——保持整洁。

11.2 SEPARATE CONSTRUCTING A SYSTEM FROM USING IT 将系统的构造与使用分离

First, consider that construction is a very different process from use. As I write this, there is a new hotel under construction that I see out my window in Chicago. Today it is a bare concrete box with a construction crane and elevator bolted to the outside. The busy people there all wear hard hats and work clothes. In a year or so the hotel will be finished. The crane and elevator will be gone. The building will be clean, encased in glass window walls and attractive paint. The people working and staying there will look a lot different too.

首先,考虑构造是一个与使用非常不同的过程。在我写这篇文章时,从我在芝加哥的窗户望出去,有一家新酒店正在建设中。今天它是一个裸露的混凝土盒子,外面安装着施工起重机和电梯。那里忙碌的人都戴着安全帽,穿着工作服。大约一年后,酒店将会完工。起重机和电梯将会消失。建筑将会变得整洁,被玻璃幕墙和漂亮的油漆包裹。在那里工作和住宿的人也会看起来很不一样。

Software systems should separate the startup process, when the application objects are constructed and the dependencies are "wired" together, from the runtime logic that takes over after startup.

软件系统应该将启动过程——应用程序对象被构造并依赖被"连接"在一起——与启动后接管的运行时逻辑分离。

The startup process is a concern that any application must address. It is the first concern that we will examine in this chapter. The separation of concerns is one of the oldest and most important design techniques in our craft.

启动过程是任何应用程序必须解决的关注点。这是我们在本章中要审视的第一个关注点。关注点分离是我们这门手艺中最古老、最重要的设计技术之一。

Unfortunately, most applications don't separate this concern. The code for the startup process is ad hoc and it is mixed in with the runtime logic. Here is a typical example:

不幸的是,大多数应用程序没有分离这个关注点。启动过程的代码是临时性的,并且与运行时逻辑混合在一起。这是一个典型的例子:

java
   public Service getService() {
     if (service == null)
       service = new MyServiceImpl(…); // Good enough default for most cases?
     return service;
   }

This is the LAZY INITIALIZATION/EVALUATION idiom, and it has several merits. We don't incur the overhead of construction unless we actually use the object, and our startup times can be faster as a result. We also ensure that null is never returned.

这是延迟初始化/求值(LAZY INITIALIZATION/EVALUATION)惯用法,它有几个优点。除非我们实际使用对象,否则不会产生构造开销,因此我们的启动时间可以更快。我们也确保永远不会返回 null。

However, we now have a hard-coded dependency on MyServiceImpl and everything its constructor requires (which I have elided). We can't compile without resolving these dependencies, even if we never actually use an object of this type at runtime!

然而,我们现在有了对 MyServiceImpl 及其构造函数所需的一切(我已省略)的硬编码依赖。不解决这些依赖我们就无法编译,即使我们在运行时从未实际使用过这种类型的对象!

Testing can be a problem. If MyServiceImpl is a heavyweight object, we will need to make sure that an appropriate TEST DOUBLE1 or MOCK OBJECT gets assigned to the service field before this method is called during unit testing. Because we have construction logic mixed in with normal runtime processing, we should test all execution paths (for example, the null test and its block). Having both of these responsibilities means that the method is doing more than one thing, so we are breaking the Single Responsibility Principle in a small way.

测试可能是个问题。如果 MyServiceImpl 是一个重量级对象,我们需要确保在单元测试期间调用此方法之前,将适当的测试替身(TEST DOUBLE)[1]或模拟对象(MOCK OBJECT)分配给 service 字段。因为我们将构造逻辑与正常的运行时处理混合在一起,我们应该测试所有执行路径(例如,null 测试及其代码块)。拥有这两个职责意味着该方法做了不止一件事,因此我们在小范围内违反了单一职责原则。

  1. [Mezzaros07].

[1] [Mezzaros07]。

Perhaps worst of all, we do not know whether MyServiceImpl is the right object in all cases. I implied as much in the comment. Why does the class with this method have to know the global context? Can we ever really know the right object to use here? Is it even possible for one type to be right for all possible contexts?

也许最糟糕的是,我们不知道 MyServiceImpl 在所有情况下是否是正确的对象。我在注释中暗示了这一点。为什么拥有这个方法的类必须知道全局上下文?我们真的能知道在这里使用的正确对象吗?一种类型是否可能适用于所有可能的上下文?

One occurrence of LAZY-INITIALIZATION isn't a serious problem, of course. However, there are normally many instances of little setup idioms like this in applications. Hence, the global setup strategy (if there is one) is scattered across the application, with little modularity and often significant duplication.

当然,一次延迟初始化并不是严重的问题。然而,应用程序中通常有许多这样的小设置惯用法实例。因此,全局设置策略(如果有的话)分散在整个应用程序中,模块化程度低,且通常有大量重复。

If we are diligent about building well-formed and robust systems, we should never let little, convenient idioms lead to modularity breakdown. The startup process of object construction and wiring is no exception. We should modularize this process separately from the normal runtime logic and we should make sure that we have a global, consistent strategy for resolving our major dependencies.

如果我们努力建立良好形式和健壮的系统,我们就不应该让小的、方便的惯用法导致模块化崩溃。对象构造和连接的启动过程也不例外。我们应该将这个过程与正常的运行时逻辑分开模块化,并且应该确保我们有一个全局的、一致的策略来解决我们的主要依赖。

11.2.1 Separation of Main 分离 Main

One way to separate construction from use is simply to move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately. (See Figure 11-1.)

将构造与使用分离的一种方法是简单地将构造的所有方面移动到 main 或 main 调用的模块中,并假设所有对象都已被适当地构造和连接来设计系统的其余部分。(见图 11-1。)

The flow of control is easy to follow. The main function builds the objects necessary for the system, then passes them to the application, which simply uses them. Notice the direction of the dependency arrows crossing the barrier between main and the application. They all go one direction, pointing away from main. This means that the application has no knowledge of main or of the construction process. It simply expects that everything has been built properly.

控制流很容易跟踪。main 函数构建系统所需的对象,然后将它们传递给应用程序,应用程序只是使用它们。注意跨越 main 和应用程序之间屏障的依赖箭头的方向。它们都指向一个方向,远离 main。这意味着应用程序不知道 main 或构造过程。它只是期望一切都被正确构建了。

11.2.2 Factories 工厂

Sometimes, of course, we need to make the application responsible for when an object gets created. For example, in an order processing system the application must create the

当然,有时我们需要让应用程序负责对象何时被创建。例如,在订单处理系统中,应用程序必须创建

Figure 11-1 Separating construction in main()

图 11-1 在 main() 中分离构造

LineItem instances to add to an Order. In this case we can use the ABSTRACT FACTORY2 pattern to give the application control of when to build the LineItems, but keep the details of that construction separate from the application code. (See Figure 11-2.)

LineItem 实例以添加到 Order 中。在这种情况下,我们可以使用抽象工厂(ABSTRACT FACTORY)[2]模式来让应用程序控制何时构建 LineItem,但将构造的细节与应用程序代码分开。(见图 11-2。)

  1. [GOF].

[2] [GOF]。

Figure 11-2 Separation construction with factory

图 11-2 用工厂分离构造

Again notice that all the dependencies point from main toward the OrderProcessing application. This means that the application is decoupled from the details of how to build a LineItem. That capability is held in the LineItemFactoryImplementation, which is on the main side of the line. And yet the application is in complete control of when the LineItem instances get built and can even provide application-specific constructor arguments.

再次注意,所有依赖都从 main 指向 OrderProcessing 应用程序。这意味着应用程序与如何构建 LineItem 的细节解耦。该能力位于 LineItemFactoryImplementation 中,它在线的 main 一侧。然而,应用程序完全控制 LineItem 实例何时被构建,甚至可以提供特定于应用程序的构造参数。

11.2.3 Dependency Injection 依赖注入

A powerful mechanism for separating construction from use is Dependency Injection (DI), the application of Inversion of Control (IoC) to dependency management.3 Inversion of Control moves secondary responsibilities from an object to other objects that are dedicated to the purpose, thereby supporting the Single Responsibility Principle. In the context of dependency management, an object should not take responsibility for instantiating dependencies itself. Instead, it should pass this responsibility to another "authoritative" mechanism, thereby inverting the control. Because setup is a global concern, this authoritative mechanism will usually be either the "main" routine or a special-purpose container.

将构造与使用分离的一个强大机制是依赖注入(Dependency Injection, DI),它是控制反转(Inversion of Control, IoC)在依赖管理中的应用。[3] 控制反转将次要职责从对象转移到专门用于该目的的其他对象,从而支持单一职责原则。在依赖管理的上下文中,对象不应该承担实例化依赖本身的责任。相反,它应该将这个责任传递给另一个"权威"机制,从而反转控制。因为设置是一个全局关注点,这个权威机制通常是"main"例程或专用容器。

  1. See, for example, [Fowler].

[3] 参见,例如,[Fowler]。

JNDI lookups are a "partial" implementation of DI, where an object asks a directory server to provide a "service" matching a particular name.

JNDI 查找是 DI 的"部分"实现,其中对象请求目录服务器提供匹配特定名称的"服务"。

java
   MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));

The invoking object doesn't control what kind of object is actually returned (as long it implements the appropriate interface, of course), but the invoking object still actively resolves the dependency.

调用对象不控制实际返回什么类型的对象(当然,只要它实现了适当的接口),但调用对象仍然主动解析依赖。

True Dependency Injection goes one step further. The class takes no direct steps to resolve its dependencies; it is completely passive. Instead, it provides setter methods or constructor arguments (or both) that are used to inject the dependencies. During the construction process, the DI container instantiates the required objects (usually on demand) and uses the constructor arguments or setter methods provided to wire together the dependencies. Which dependent objects are actually used is specified through a configuration file or programmatically in a special-purpose construction module.

真正的依赖注入更进一步。类不采取任何直接步骤来解析其依赖;它是完全被动的。相反,它提供 setter 方法或构造函数参数(或两者)来注入依赖。在构造过程中,DI 容器实例化所需的对象(通常是按需),并使用提供的构造函数参数或 setter 方法将依赖连接在一起。实际使用哪些依赖对象通过配置文件或在专用构造模块中以编程方式指定。

The Spring Framework provides the best known DI container for Java.4 You define which objects to wire together in an XML configuration file, then you ask for particular objects by name in Java code. We will look at an example shortly.

Spring Framework 为 Java 提供了最知名的 DI 容器。[4] 你在 XML 配置文件中定义要连接在一起的对象,然后在 Java 代码中按名称请求特定对象。我们很快将看一个例子。

  1. See [Spring]. There is also a Spring.NET framework.

[4] 参见 [Spring]。还有一个 Spring.NET 框架。

But what about the virtues of LAZY-INITIALIZATION? This idiom is still sometimes useful with DI. First, most DI containers won't construct an object until needed. Second, many of these containers provide mechanisms for invoking factories or for constructing proxies, which could be used for LAZY-EVALUATION and similar optimizations.5

但延迟初始化的优点呢?这个惯用法在 DI 中有时仍然有用。首先,大多数 DI 容器在需要之前不会构造对象。其次,许多容器提供了调用工厂或构造代理的机制,可用于延迟求值和类似的优化。[5]

  1. Don't forget that lazy instantiation/evaluation is just an optimization and perhaps premature!

[5] 不要忘记延迟实例化/求值只是一种优化,而且可能是过早的优化!

11.3 SCALING UP 扩展

Cities grow from towns, which grow from settlements. At first the roads are narrow and practically nonexistent, then they are paved, then widened over time. Small buildings and empty plots are filled with larger buildings, some of which will eventually be replaced with skyscrapers.

城市从城镇发展而来,城镇从定居点发展而来。起初道路狭窄,几乎不存在,然后被铺平,然后随时间加宽。小型建筑和空地被更大的建筑填满,其中一些最终将被摩天大楼取代。

At first there are no services like power, water, sewage, and the Internet (gasp!). These services are also added as the population and building densities increase.

起初没有电力、供水、污水和互联网(天哪!)等服务。随着人口和建筑密度的增加,这些服务也会被添加。

This growth is not without pain. How many times have you driven, bumper to bumper through a road "improvement" project and asked yourself, "Why didn't they build it wide enough the first time!?"

这种增长并非没有痛苦。有多少次你在道路"改善"项目中堵得水泄不通,问自己:"为什么他们第一次不建得够宽!?"

But it couldn't have happened any other way. Who can justify the expense of a six-lane highway through the middle of a small town that anticipates growth? Who would want such a road through their town?

但这不可能以其他方式发生。谁能证明一条六车道高速公路穿过预期增长的小镇中心的费用是合理的?谁会想要这样一条路穿过他们的城镇?

It is a myth that we can get systems "right the first time." Instead, we should implement only today's stories, then refactor and expand the system to implement new stories tomorrow. This is the essence of iterative and incremental agility. Test-driven development, refactoring, and the clean code they produce make this work at the code level.

我们能"第一次就做对"系统是一个神话。相反,我们应该只实现今天的故事,然后重构和扩展系统以实现明天的新故事。这是迭代和增量敏捷的精髓。测试驱动开发、重构和它们产生的整洁代码在代码层面使这成为可能。

But what about at the system level? Doesn't the system architecture require preplanning? Certainly, it can't grow incrementally from simple to complex, can it?

但在系统层面呢?系统架构不需要预先规划吗?当然,它不能从简单增量增长到复杂,对吧?

Software systems are unique compared to physical systems. Their architectures can grow incrementally, if we maintain the proper separation of concerns.

软件系统与物理系统相比是独特的。如果我们保持适当的关注点分离,它们的架构可以增量增长。

The ephemeral nature of software systems makes this possible, as we will see. Let us first consider a counterexample of an architecture that doesn't separate concerns adequately.

软件系统的短暂性使之成为可能,正如我们将看到的。让我们首先考虑一个没有充分分离关注点的架构的反例。

The original EJB1 and EJB2 architectures did not separate concerns appropriately and thereby imposed unnecessary barriers to organic growth. Consider an Entity Bean for a persistent Bank class. An entity bean is an in-memory representation of relational data, in other words, a table row.

原始的 EJB1 和 EJB2 架构没有适当地分离关注点,从而对有机增长施加了不必要的障碍。考虑一个用于持久化 Bank 类的 Entity Bean。Entity Bean 是关系数据的内存表示,换句话说,就是表中的一行。

First, you had to define a local (in process) or remote (separate JVM) interface, which clients would use. Listing 11-1 shows a possible local interface:

首先,你必须定义一个本地(进程内)或远程(独立 JVM)接口,客户端将使用它。代码清单 11-1 展示了一个可能的本地接口:

Listing 11-1 An EJB2 local interface for a Bank EJB

代码清单 11-1 Bank EJB 的 EJB2 本地接口

java
   package com.example.banking;
   import java.util.Collections;
   import javax.ejb.*;
   
   public interface BankLocal extends java.ejb.EJBLocalObject {
     String getStreetAddr1() throws EJBException;
     String getStreetAddr2() throws EJBException;
     String getCity() throws EJBException;
     String getState() throws EJBException;
     String getZipCode() throws EJBException;
     void setStreetAddr1(String street1) throws EJBException;
     void setStreetAddr2(String street2) throws EJBException;
     void setCity(String city) throws EJBException;
     void setState(String state) throws EJBException;
     void setZipCode(String zip) throws EJBException;
     Collection getAccounts() throws EJBException;
     void setAccounts(Collection accounts) throws EJBException;
     void addAccount(AccountDTO accountDTO) throws EJBException;
   }

I have shown several attributes for the Bank's address and a collection of accounts that the bank owns, each of which would have its data handled by a separate Account EJB. Listing 11-2 shows the corresponding implementation class for the Bank bean.

我展示了 Bank 地址的几个属性和银行拥有的账户集合,每个账户的数据将由单独的 Account EJB 处理。代码清单 11-2 展示了 Bank bean 的相应实现类。

Listing 11-2 The corresponding EJB2 Entity Bean Implementation

代码清单 11-2 相应的 EJB2 Entity Bean 实现

java
   package com.example.banking;
   import java.util.Collections;
   import javax.ejb.*;
   
   public abstract class Bank implements javax.ejb.EntityBean {
     // Business logic…
     public abstract String getStreetAddr1();
     public abstract String getStreetAddr2();
     public abstract String getCity();
     public abstract String getState();
     public abstract String getZipCode();
     public abstract void setStreetAddr1(String street1);
     public abstract void setStreetAddr2(String street2);
     public abstract void setCity(String city);
     public abstract void setState(String state);
     public abstract void setZipCode(String zip);
     public abstract Collection getAccounts();
     public abstract void setAccounts(Collection accounts);
     public void addAccount(AccountDTO accountDTO) {
       InitialContext context = new InitialContext();
       AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
       AccountLocal account = accountHome.create(accountDTO);
       Collection accounts = getAccounts();
       accounts.add(account);
     }
     // EJB container logic
     public abstract void setId(Integer id);
     public abstract Integer getId();
     public Integer ejbCreate(Integer id) { … }
     public void ejbPostCreate(Integer id) { … }
     // The rest had to be implemented but were usually empty:
     public void setEntityContext(EntityContext ctx) {} 
     public void unsetEntityContext() {}
     public void ejbActivate() {}
     public void ejbPassivate() {}
     public void ejbLoad() {}
     public void ejbStore() {}
     public void ejbRemove() {}
   }

I haven't shown the corresponding LocalHome interface, essentially a factory used to create objects, nor any of the possible Bank finder (query) methods you might add.

我没有展示相应的 LocalHome 接口——本质上是用于创建对象的工厂——也没有展示你可能添加的任何可能的 Bank 查找器(查询)方法。

Finally, you had to write one or more XML deployment descriptors that specify the object-relational mapping details to a persistence store, the desired transactional behavior, security constraints, and so on.

最后,你必须编写一个或多个 XML 部署描述符,指定到持久化存储的对象-关系映射细节、期望的事务行为、安全约束等。

The business logic is tightly coupled to the EJB2 application "container." You must subclass container types and you must provide many lifecycle methods that are required by the container.

业务逻辑与 EJB2 应用程序"容器"紧密耦合。你必须子类化容器类型,并且必须提供容器所需的许多生命周期方法。

Because of this coupling to the heavyweight container, isolated unit testing is difficult. It is necessary to mock out the container, which is hard, or waste a lot of time deploying EJBs and tests to a real server. Reuse outside of the EJB2 architecture is effectively impossible, due to the tight coupling.

由于与重量级容器的耦合,隔离的单元测试很困难。需要模拟容器,这很难,或者浪费大量时间将 EJB 和测试部署到真实服务器上。由于紧密耦合,在 EJB2 架构之外复用实际上是不可能的。

Finally, even object-oriented programming is undermined. One bean cannot inherit from another bean. Notice the logic for adding a new account. It is common in EJB2 beans to define "data transfer objects" (DTOs) that are essentially "structs" with no behavior. This usually leads to redundant types holding essentially the same data, and it requires boilerplate code to copy data from one object to another.

最后,甚至面向对象编程也被破坏了。一个 bean 不能从另一个 bean 继承。注意添加新账户的逻辑。在 EJB2 bean 中,定义"数据传输对象"(DTO)是常见的,它们本质上是没有行为的"结构体"。这通常导致持有基本相同数据的冗余类型,并且需要样板代码将数据从一个对象复制到另一个对象。

11.3.1 Cross-Cutting Concerns 横切关注点

The EJB2 architecture comes close to true separation of concerns in some areas. For example, the desired transactional, security, and some of the persistence behaviors are declared in the deployment descriptors, independently of the source code.

EJB2 架构在某些领域接近真正的关注点分离。例如,期望的事务、安全和一些持久化行为在部署描述符中声明,独立于源代码。

Note that concerns like persistence tend to cut across the natural object boundaries of a domain. You want to persist all your objects using generally the same strategy, for example, using a particular DBMS6 versus flat files, following certain naming conventions for tables and columns, using consistent transactional semantics, and so on.

注意,像持久化这样的关注点倾向于跨越领域的自然对象边界。你希望使用大致相同的策略来持久化所有对象,例如,使用特定的数据库管理系统[6]而不是平面文件,遵循表和列的特定命名约定,使用一致的事务语义等。

  1. Database management system.

[6] 数据库管理系统。

In principle, you can reason about your persistence strategy in a modular, encapsulated way. Yet, in practice, you have to spread essentially the same code that implements the persistence strategy across many objects. We use the term cross-cutting concerns for concerns like these. Again, the persistence framework might be modular and our domain logic, in isolation, might be modular. The problem is the fine-grained intersection of these domains.

原则上,你可以以模块化、封装的方式推理你的持久化策略。然而,在实践中,你必须将实现持久化策略的基本相同的代码分散到许多对象中。我们用术语"横切关注点"来描述这类关注点。同样,持久化框架可能是模块化的,我们的领域逻辑在隔离状态下也可能是模块化的。问题是这些领域的细粒度交叉。

In fact, the way the EJB architecture handled persistence, security, and transactions, "anticipated" aspect-oriented programming (AOP),7 which is a general-purpose approach to restoring modularity for cross-cutting concerns.

事实上,EJB 架构处理持久化、安全和事务的方式"预见"了面向切面编程(AOP)[7],这是一种恢复横切关注点模块化的通用方法。

  1. See [AOSD] for general information on aspects and [AspectJ] and [Colyer] for AspectJ-specific information.

[7] 参见 [AOSD] 了解切面的一般信息,参见 [AspectJ] 和 [Colyer] 了解 AspectJ 特定信息。

In AOP, modular constructs called aspects specify which points in the system should have their behavior modified in some consistent way to support a particular concern. This specification is done using a succinct declarative or programmatic mechanism.

在 AOP 中,称为切面(aspect)的模块化构造指定系统中的哪些点应该以某种一致的方式修改其行为以支持特定的关注点。这种规范使用简洁的声明式或编程机制完成。

Using persistence as an example, you would declare which objects and attributes (or patterns thereof) should be persisted and then delegate the persistence tasks to your persistence framework. The behavior modifications are made noninvasively8 to the target code by the AOP framework. Let us look at three aspects or aspect-like mechanisms in Java.

以持久化为例,你会声明哪些对象和属性(或其模式)应该被持久化,然后将持久化任务委托给你的持久化框架。行为修改由 AOP 框架以非侵入方式[8]对目标代码进行。让我们看看 Java 中的三种切面或类切面机制。

  1. Meaning no manual editing of the target source code is required.

[8] 意味着不需要手动编辑目标源代码。

11.4 JAVA PROXIES Java 代理

Java proxies are suitable for simple situations, such as wrapping method calls in individual objects or classes. However, the dynamic proxies provided in the JDK only work with interfaces. To proxy classes, you have to use a byte-code manipulation library, such as CGLIB, ASM, or Javassist.9

Java 代理适用于简单情况,例如在单个对象或类中包装方法调用。然而,JDK 中提供的动态代理仅适用于接口。要代理类,你必须使用字节码操作库,如 CGLIB、ASM 或 Javassist。[9]

  1. See [CGLIB], [ASM], and [Javassist].

[9] 参见 [CGLIB]、[ASM] 和 [Javassist]。

Listing 11-3 shows the skeleton for a JDK proxy to provide persistence support for our Bank application, covering only the methods for getting and setting the list of accounts.

代码清单 11-3 展示了为我们的 Bank 应用程序提供持久化支持的 JDK 代理的骨架,仅涵盖获取和设置账户列表的方法。

Listing 11-3 JDK Proxy Example

代码清单 11-3 JDK 代理示例

java
   // Bank.java (suppressing package names…)
   import java.utils.*;
   
   // The abstraction of a bank.
   public interface Bank {
     Collection<Account> getAccounts();
     void setAccounts(Collection<Account> accounts);
   }
   // BankImpl.java
   import java.utils.*;
 
   // The "Plain Old Java Object" (POJO) implementing the abstraction.
   public class BankImpl implements Bank {
     private List<Account> accounts;
 
     public Collection<Account> getAccounts() { 
       return accounts; 
     }
     public void setAccounts(Collection<Account> accounts) { 
       this.accounts = new ArrayList<Account>(); 
       for (Account account: accounts) {
         this.accounts.add(account);
       }
     }
   }
   // BankProxyHandler.java
   import java.lang.reflect.*;
   import java.util.*;
   // "InvocationHandler" required by the proxy API.
   public class BankProxyHandler implements InvocationHandler {
     private Bank bank;
     
     public BankHandler (Bank bank) {
       this.bank = bank;
     }
     // Method defined in InvocationHandler
     public Object invoke(Object proxy, Method method, Object[] args) 
         throws Throwable {
     String methodName = method.getName();
     if (methodName.equals("getAccounts")) {
       bank.setAccounts(getAccountsFromDatabase());
       return bank.getAccounts();
     } else if (methodName.equals("setAccounts")) {
       bank.setAccounts((Collection<Account>) args[0]);
       setAccountsToDatabase(bank.getAccounts());
       return null;
     } else {

     }
   }
   // Lots of details here:
   protected Collection<Account> getAccountsFromDatabase() { … }
   protected void setAccountsToDatabase(Collection<Account> accounts) { … }
   }
 
   // Somewhere else…
 
   Bank bank = (Bank) Proxy.newProxyInstance(
     Bank.class.getClassLoader(), 
     new Class[] { Bank.class },
     new BankProxyHandler(new BankImpl()));

We defined an interface Bank, which will be wrapped by the proxy, and a Plain-Old Java Object (POJO), BankImpl, that implements the business logic. (We will revisit POJOs shortly.)

我们定义了一个接口 Bank,它将被代理包装,以及一个普通 Java 对象(POJO)BankImpl,它实现了业务逻辑。(我们很快将重新讨论 POJO。)

The Proxy API requires an InvocationHandler object that it calls to implement any Bank method calls made to the proxy. Our BankProxyHandler uses the Java reflection API to map the generic method invocations to the corresponding methods in BankImpl, and so on.

Proxy API 需要一个 InvocationHandler 对象,它被调用来实现对代理的任何 Bank 方法调用。我们的 BankProxyHandler 使用 Java 反射 API 将通用方法调用映射到 BankImpl 中的相应方法,等等。

There is a lot of code here and it is relatively complicated, even for this simple case.10 Using one of the byte-manipulation libraries is similarly challenging. This code "volume"

这里有很多代码,即使对于这个简单的情况也相对复杂。[10] 使用其中一个字节码操作库同样具有挑战性。代码"量"

  1. For more detailed examples of the Proxy API and examples of its use, see, for example, [Goetz].

[10] 关于 Proxy API 的更详细示例和使用示例,参见,例如,[Goetz]。

and complexity are two of the drawbacks of proxies. They make it hard to create clean code! Also, proxies don't provide a mechanism for specifying system-wide execution "points" of interest, which is needed for a true AOP solution.11

和复杂性是代理的两个缺点。它们使创建整洁代码变得困难!此外,代理没有提供指定系统级执行"点"的机制,这是真正的 AOP 解决方案所需要的。[11]

  1. AOP is sometimes confused with techniques used to implement it, such as method interception and "wrapping" through proxies. The real value of an AOP system is the ability to specify systemic behaviors in a concise and modular way.

[11] AOP 有时与用于实现它的技术混淆,如方法拦截和通过代理进行"包装"。AOP 系统的真正价值在于能够以简洁和模块化的方式指定系统行为。

11.5 PURE JAVA AOP FRAMEWORKS 纯 Java AOP 框架

Fortunately, most of the proxy boilerplate can be handled automatically by tools. Proxies are used internally in several Java frameworks, for example, Spring AOP and JBoss AOP, to implement aspects in pure Java.12 In Spring, you write your business logic as Plain-Old Java Objects. POJOs are purely focused on their domain. They have no dependencies on enterprise frameworks (or any other domains). Hence, they are conceptually simpler and easier to test drive. The relative simplicity makes it easier to ensure that you are implementing the corresponding user stories correctly and to maintain and evolve the code for future stories.

幸运的是,大多数代理样板代码可以由工具自动处理。代理在几个 Java 框架内部使用,例如 Spring AOP 和 JBoss AOP,以纯 Java 实现切面。[12] 在 Spring 中,你将业务逻辑编写为普通 Java 对象(POJO)。POJO 纯粹专注于它们的领域。它们不依赖于企业框架(或任何其他领域)。因此,它们在概念上更简单,更容易测试驱动。相对的简单性使你更容易确保正确实现相应的用户故事,并为未来的故事维护和演进代码。

  1. See [Spring] and [JBoss]. "Pure Java" means without the use of AspectJ.

[12] 参见 [Spring] 和 [JBoss]。"纯 Java"意味着不使用 AspectJ。

You incorporate the required application infrastructure, including cross-cutting concerns like persistence, transactions, security, caching, failover, and so on, using declarative configuration files or APIs. In many cases, you are actually specifying Spring or JBoss library aspects, where the framework handles the mechanics of using Java proxies or byte-code libraries transparently to the user. These declarations drive the dependency injection (DI) container, which instantiates the major objects and wires them together on demand.

你使用声明式配置文件或 API 来整合所需的应用程序基础设施,包括横切关注点,如持久化、事务、安全、缓存、故障转移等。在许多情况下,你实际上是在指定 Spring 或 JBoss 库切面,框架对用户透明地处理使用 Java 代理或字节码库的机制。这些声明驱动依赖注入(DI)容器,它按需实例化主要对象并将它们连接在一起。

Listing 11-4 shows a typical fragment of a Spring V2.5 configuration file, app.xml13:

代码清单 11-4 展示了 Spring V2.5 配置文件 app.xml 的典型片段:[13]

  1. Adapted from http://www.theserverside.com/tt/articles/article.tss?l=IntrotoSpring25.

[13] 改编自 http://www.theserverside.com/tt/articles/article.tss?l=IntrotoSpring25。

Listing 11-4 Spring 2.X configuration file

代码清单 11-4 Spring 2.X 配置文件

xml
   <beans>

     <bean id="appDataSource"
     class="org.apache.commons.dbcp.BasicDataSource"
     destroy-method="close"
     p:driverClassName="com.mysql.jdbc.Driver"
     p:url="jdbc:mysql://localhost:3306/mydb"
     p:username="me"/>
 
     <bean id="bankDataAccessObject"
     class="com.example.banking.persistence.BankDataAccessObject"
     p:dataSource-ref="appDataSource"/>
 
     <bean id="bank"
   class="com.example.banking.model.Bank"
   p:dataAccessObject-ref="bankDataAccessObject"/>

 </beans>

Each "bean" is like one part of a nested "Russian doll," with a domain object for a Bank proxied (wrapped) by a data accessor object (DAO), which is itself proxied by a JDBC driver data source. (See Figure 11-3.)

每个"bean"就像嵌套的"俄罗斯娃娃"的一部分,一个 Bank 的领域对象被数据访问对象(DAO)代理(包装),而 DAO 本身又被 JDBC 驱动数据源代理。(见图 11-3。)

Figure 11-3 The "Russian doll" of decorators

图 11-3 装饰器的"俄罗斯娃娃"

The client believes it is invoking getAccounts() on a Bank object, but it is actually talking to the outermost of a set of nested DECORATOR14 objects that extend the basic behavior of the Bank POJO. We could add other decorators for transactions, caching, and so forth.

客户端认为它是在 Bank 对象上调用 getAccounts(),但实际上它是在与一组嵌套的装饰器(DECORATOR)[14]对象中最外层的一个对话,这些对象扩展了 Bank POJO 的基本行为。我们可以为事务、缓存等添加其他装饰器。

  1. [GOF].

[14] [GOF]。

In the application, a few lines are needed to ask the DI container for the top-level objects in the system, as specified in the XML file.

在应用程序中,需要几行代码来向 DI 容器请求系统中的顶层对象,如 XML 文件中所指定的。

java
   XmlBeanFactory bf =
     new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
   Bank bank = (Bank) bf.getBean("bank");

Because so few lines of Spring-specific Java code are required, the application is almost completely decoupled from Spring, eliminating all the tight-coupling problems of systems like EJB2.

因为需要的 Spring 特定 Java 代码如此之少,应用程序几乎与 Spring 完全解耦,消除了像 EJB2 这样的系统的所有紧密耦合问题。

Although XML can be verbose and hard to read,15 the "policy" specified in these configuration files is simpler than the complicated proxy and aspect logic that is hidden from view and created automatically. This type of architecture is so compelling that frameworks like Spring led to a complete overhaul of the EJB standard for version 3. EJB3

尽管 XML 可能冗长且难以阅读,[15] 但这些配置文件中指定的"策略"比隐藏在视图之外并自动创建的复杂代理和切面逻辑更简单。这种类型的架构如此引人注目,以至于像 Spring 这样的框架导致了 EJB 标准第 3 版的彻底改革。EJB3

  1. The example can be simplified using mechanisms that exploit convention over configuration and Java 5 annotations to reduce the amount of explicit "wiring" logic required.

[15] 可以使用利用约定优于配置和 Java 5 注解的机制来简化示例,以减少所需的显式"连接"逻辑量。

largely follows the Spring model of declaratively supporting cross-cutting concerns using XML configuration files and/or Java 5 annotations.

大致遵循 Spring 模型,使用 XML 配置文件和/或 Java 5 注解来声明式地支持横切关注点。

Listing 11-5 shows our Bank object rewritten in EJB316.

代码清单 11-5 展示了用 EJB3[16] 重写的 Bank 对象。

  1. Adapted from http://www.onjava.com/pub/a/onjava/2006/05/17/standardizing-with-ejb3-java-persistence-api.html

[16] 改编自 http://www.onjava.com/pub/a/onjava/2006/05/17/standardizing-with-ejb3-java-persistence-api.html

Listing 11-5 An EBJ3 Bank EJB

代码清单 11-5 EJB3 Bank EJB

java
   package com.example.banking.model;
   import javax.persistence.*;
   import java.util.ArrayList;
   import java.util.Collection;
 
   @Entity
   @Table(name = "BANKS")
   public class Bank implements java.io.Serializable {
      @Id @GeneratedValue(strategy=GenerationType.AUTO)
      private int id;
 
      @Embeddable // An object "inlined" in Bank's DB row
      public class Address {
         protected String streetAddr1;
         protected String streetAddr2;
         protected String city;
         protected String state;
         protected String zipCode;
      }
      @Embedded
      private Address address;
 
      @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
                 mappedBy="bank")
      private Collection<Account> accounts = new ArrayList<Account>();
 
      public int getId() {
         return id;
      }
 
      public void setId(int id) {
         this.id = id;
      }
 
      public void addAccount(Account account) {
         account.setBank(this);
         accounts.add(account);
      }
      public Collection<Account> getAccounts() {
         return accounts;
      }
   public void setAccounts(Collection<Account> accounts) {
      this.accounts = accounts;
   }
 }

This code is much cleaner than the original EJB2 code. Some of the entity details are still here, contained in the annotations. However, because none of that information is outside of the annotations, the code is clean, clear, and hence easy to test drive, maintain, and so on.

这段代码比原始的 EJB2 代码整洁得多。一些实体细节仍然在这里,包含在注解中。然而,因为这些信息都不在注解之外,代码是整洁的、清晰的,因此容易测试驱动、维护等。

Some or all of the persistence information in the annotations can be moved to XML deployment descriptors, if desired, leaving a truly pure POJO. If the persistence mapping details won't change frequently, many teams may choose to keep the annotations, but with far fewer harmful drawbacks compared to the EJB2 invasiveness.

如果需要,注解中的部分或全部持久化信息可以移动到 XML 部署描述符中,留下真正的纯 POJO。如果持久化映射细节不会经常更改,许多团队可能选择保留注解,但与 EJB2 的侵入性相比,有害的缺点要少得多。

11.6 ASPECTJ ASPECTS AspectJ 切面

Finally, the most full-featured tool for separating concerns through aspects is the AspectJ language,17 an extension of Java that provides "first-class" support for aspects as modularity constructs. The pure Java approaches provided by Spring AOP and JBoss AOP are sufficient for 80–90 percent of the cases where aspects are most useful. However, AspectJ provides a very rich and powerful tool set for separating concerns. The drawback of AspectJ is the need to adopt several new tools and to learn new language constructs and usage idioms.

最后,通过切面分离关注点的最全功能工具是 AspectJ 语言,[17] 它是 Java 的扩展,为切面作为模块化构造提供"一等"支持。Spring AOP 和 JBoss AOP 提供的纯 Java 方法对于切面最有用的 80-90% 的情况已经足够。然而,AspectJ 提供了非常丰富和强大的工具集来分离关注点。AspectJ 的缺点是需要采用几个新工具并学习新的语言构造和使用惯用法。

  1. See [AspectJ] and [Colyer].

[17] 参见 [AspectJ] 和 [Colyer]。

The adoption issues have been partially mitigated by a recently introduced "annotation form" of AspectJ, where Java 5 annotations are used to define aspects using pure Java code. Also, the Spring Framework has a number of features that make incorporation of annotation-based aspects much easier for a team with limited AspectJ experience.

采用问题已经部分缓解,最近引入了 AspectJ 的"注解形式",使用 Java 5 注解以纯 Java 代码定义切面。此外,Spring Framework 有许多功能,使得对于 AspectJ 经验有限的团队来说,整合基于注解的切面变得更容易。

A full discussion of AspectJ is beyond the scope of this book. See [AspectJ], [Colyer], and [Spring] for more information.

关于 AspectJ 的完整讨论超出了本书的范围。参见 [AspectJ]、[Colyer] 和 [Spring] 了解更多信息。

11.7 TEST DRIVE THE SYSTEM ARCHITECTURE 测试驱动系统架构

The power of separating concerns through aspect-like approaches can't be overstated. If you can write your application's domain logic using POJOs, decoupled from any architecture concerns at the code level, then it is possible to truly test drive your architecture. You can evolve it from simple to sophisticated, as needed, by adopting new technologies on demand. It is not necessary to do a Big Design Up Front18 (BDUF). In fact, BDUF is even harmful because it inhibits adapting to change, due to the psychological resistance to discarding prior effort and because of the way architecture choices influence subsequent thinking about the design.

通过类切面方法分离关注点的力量怎么强调都不为过。如果你能使用 POJO 编写应用程序的领域逻辑,在代码层面与任何架构关注点解耦,那么就有可能真正测试驱动你的架构。你可以根据需要,按需采用新技术,将其从简单演进到复杂。没有必要进行大设计前期[18](BDUF)。事实上,BDUF 甚至是有害的,因为它抑制适应变化,这是由于心理上对放弃先前努力的抵抗,以及架构选择影响后续设计思考的方式。

  1. Not to be confused with the good practice of up-front design, BDUF is the practice of designing everything up front before implementing anything at all.

[18] 不要与前期设计的良好实践混淆,BDUF 是在实现任何东西之前就预先设计一切的做法。

Building architects have to do BDUF because it is not feasible to make radical architectural changes to a large physical structure once construction is well underway.19 Although software has its own physics,20 it is economically feasible to make radical change, if the structure of the software separates its concerns effectively.

建筑师必须进行 BDUF,因为一旦施工正在进行,对大型物理结构进行根本性的架构变更是不可行的。[19] 尽管软件有自己的物理特性,[20] 但如果软件的结构有效地分离了关注点,进行根本性变更是经济可行的。

  1. There is still a significant amount of iterative exploration and discussion of details, even after construction starts.

[19] 即使在施工开始后,仍然有大量的迭代探索和细节讨论。

  1. The term software physics was first used by [Kolence].

[20] 术语"软件物理学"最早由 [Kolence] 使用。

This means we can start a software project with a "naively simple" but nicely decoupled architecture, delivering working user stories quickly, then adding more infrastructure as we scale up. Some of the world's largest Web sites have achieved very high availability and performance, using sophisticated data caching, security, virtualization, and so forth, all done efficiently and flexibly because the minimally coupled designs are appropriately simple at each level of abstraction and scope.

这意味着我们可以从一个"天真简单"但良好解耦的架构开始一个软件项目,快速交付可用的用户故事,然后在扩展时添加更多基础设施。世界上一些最大的网站已经实现了非常高的可用性和性能,使用复杂的数据缓存、安全、虚拟化等,这一切都因为最小耦合的设计在每个抽象层次和范围内都是适当简单的,从而高效且灵活地完成。

Of course, this does not mean that we go into a project "rudderless." We have some expectations of the general scope, goals, and schedule for the project, as well as the general structure of the resulting system. However, we must maintain the ability to change course in response to evolving circumstances.

当然,这并不意味着我们"无舵"地进入一个项目。我们对项目的大致范围、目标和时间表,以及最终系统的大致结构有一些期望。然而,我们必须保持根据不断变化的情况改变方向的能力。

The early EJB architecture is but one of many well-known APIs that are over-engineered and that compromise separation of concerns. Even well-designed APIs can be overkill when they aren't really needed. A good API should largely disappear from view most of the time, so the team expends the majority of its creative efforts focused on the user stories being implemented. If not, then the architectural constraints will inhibit the efficient delivery of optimal value to the customer.

早期的 EJB 架构只是众多过度工程化且损害关注点分离的知名 API 之一。即使设计良好的 API,当它们不是真正需要时也可能是过度的。一个好的 API 在大部分时间应该基本上从视野中消失,这样团队就可以将大部分创造性精力集中在正在实现的用户故事上。如果不是这样,那么架构约束将抑制向客户高效交付最优价值。

To recap this long discussion,

回顾这个长篇讨论,

An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java (or other) Objects. The different domains are integrated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code.

一个最优的系统架构由模块化的关注点领域组成,每个领域都用普通 Java(或其他)对象实现。不同的领域通过最小侵入性的切面或类切面工具集成在一起。这种架构可以像代码一样被测试驱动。

11.8 OPTIMIZE DECISION MAKING 优化决策

Modularity and separation of concerns make decentralized management and decision making possible. In a sufficiently large system, whether it is a city or a software project, no one person can make all the decisions.

模块化和关注点分离使得分散管理和决策成为可能。在一个足够大的系统中,无论是城市还是软件项目,没有一个人能做出所有决定。

We all know it is best to give responsibilities to the most qualified persons. We often forget that it is also best to postpone decisions until the last possible moment. This isn't lazy or irresponsible; it lets us make informed choices with the best possible information. A premature decision is a decision made with suboptimal knowledge. We will have that much less customer feedback, mental reflection on the project, and experience with our implementation choices if we decide too soon.

我们都知道最好将职责交给最有资格的人。我们常常忘记,最好也将决定推迟到最后一刻。这不是懒惰或不负责任;它让我们用尽可能好的信息做出明智的选择。过早的决定是在次优知识下做出的决定。如果我们决定得太早,我们将拥有更少的客户反馈、对项目的精神反思和实现选择的经验。

The agility provided by a POJO system with modularized concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced.

POJO 系统提供的敏捷性,加上模块化的关注点,使我们能够基于最新的知识做出最优的、即时的决策。这些决策的复杂性也降低了。

11.9 USE STANDARDS WISELY, WHEN THEY ADD DEMONSTRABLE VALUE 明智地使用标准,当它们能带来可证明的价值时

Building construction is a marvel to watch because of the pace at which new buildings are built (even in the dead of winter) and because of the extraordinary designs that are possible with today's technology. Construction is a mature industry with highly optimized parts, methods, and standards that have evolved under pressure for centuries.

建筑施工是一个奇观,因为新建筑的建造速度(即使在严冬)以及当今技术所能实现的非凡设计。建筑是一个成熟的行业,拥有高度优化的部件、方法和标准,在几个世纪的压力下演化而来。

Many teams used the EJB2 architecture because it was a standard, even when lighter-weight and more straightforward designs would have been sufficient. I have seen teams become obsessed with various strongly hyped standards and lose focus on implementing value for their customers.

许多团队使用 EJB2 架构是因为它是一个标准,即使更轻量级和更直接的设计就已经足够。我见过团队沉迷于各种被大力炒作的标准,而失去了为客户实现价值的焦点。

Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.

标准使复用思想和组件、招聘有相关经验的人、封装好思想以及将组件连接在一起变得更容易。然而,创建标准的过程有时对行业来说太长了,一些标准与它们旨在服务的采用者的真正需求脱节了。

11.10 SYSTEMS NEED DOMAIN-SPECIFIC LANGUAGES 系统需要领域特定语言

Building construction, like most domains, has developed a rich language with a vocabulary, idioms, and patterns21 that convey essential information clearly and concisely. In software, there has been renewed interest recently in creating Domain-Specific Languages (DSLs),22 which are separate, small scripting languages or APIs in standard languages that permit code to be written so that it reads like a structured form of prose that a domain expert might write.

建筑施工,像大多数领域一样,发展了一种丰富的语言,具有词汇、惯用法和模式[21],能够清晰简洁地传达基本信息。在软件领域,最近对创建领域特定语言(DSL)[22]重新产生了兴趣,它们是独立的、小型的脚本语言或标准语言中的 API,允许编写的代码读起来像领域专家可能编写的结构化散文。

  1. The work of [Alexander] has been particularly influential on the software community.

[21] [Alexander] 的工作对软件社区影响特别大。

  1. See, for example, [DSL]. [JMock] is a good example of a Java API that creates a DSL.

[22] 参见,例如,[DSL]。[JMock] 是创建 DSL 的 Java API 的一个好例子。

A good DSL minimizes the "communication gap" between a domain concept and the code that implements it, just as agile practices optimize the communications within a team and with the project's stakeholders. If you are implementing domain logic in the same language that a domain expert uses, there is less risk that you will incorrectly translate the domain into the implementation.

一个好的 DSL 最小化了领域概念与实现它的代码之间的"沟通差距",就像敏捷实践优化了团队内部以及与项目利益相关者之间的沟通一样。如果你使用领域专家使用的同一种语言来实现领域逻辑,那么你错误地将领域翻译到实现中的风险就更小。

DSLs, when used effectively, raise the abstraction level above code idioms and design patterns. They allow the developer to reveal the intent of the code at the appropriate level of abstraction.

DSL 在有效使用时,将抽象层次提升到代码惯用法和设计模式之上。它们允许开发者在适当的抽象层次上揭示代码的意图。

Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJOs, from high-level policy to low-level details.

领域特定语言允许应用程序中所有抽象层次和所有领域都用 POJO 表示,从高层策略到底层细节。

11.11 CONCLUSION 结论

Systems must be clean too. An invasive architecture overwhelms the domain logic and impacts agility. When the domain logic is obscured, quality suffers because bugs find it easier to hide and stories become harder to implement. If agility is compromised, productivity suffers and the benefits of TDD are lost.

系统也必须整洁。侵入性架构压倒了领域逻辑并影响了敏捷性。当领域逻辑被遮蔽时,质量会下降,因为 bug 更容易隐藏,故事更难实现。如果敏捷性受到损害,生产力会下降,TDD 的好处也会丧失。

At all levels of abstraction, the intent should be clear. This will only happen if you write POJOs and you use aspect-like mechanisms to incorporate other implementation concerns noninvasively.

在所有抽象层次上,意图都应该是清晰的。只有当你编写 POJO 并使用类切面机制以非侵入方式整合其他实现关注点时,这才会发生。

Whether you are designing systems or individual modules, never forget to use the simplest thing that can possibly work.

无论你是在设计系统还是单个模块,永远不要忘记使用可能有效的最简单的东西。