体系结构原则Architectural principles

“如果建筑师按照程序员编写程序的方式建造建筑物,那么第一只到来的啄木鸟(找 Bug)就将摧毁文明。”"If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization."
- Gerald Weinberg- Gerald Weinberg

构建和设计软件解决方案时应考虑到可维护性。You should architect and design software solutions with maintainability in mind. 本部分概述的原则可帮助指导你作出体系结构决策,生成简洁、可维护的应用程序。The principles outlined in this section can help guide you toward architectural decisions that will result in clean, maintainable applications. 一般而言,在这些原则的指导下构建的应用程序各部分间可通过显式接口或消息传送系统进行通信,并非松散耦合的离散组件。Generally, these principles will guide you toward building applications out of discrete components that are not tightly coupled to other parts of your application, but rather communicate through explicit interfaces or messaging systems.

通用设计原则Common design principles

分离关注点Separation of concerns

分离关注点是开发时的指导原则。A guiding principle when developing is Separation of Concerns. 此原则主张应根据软件执行的工作类型将软件分离。This principle asserts that software should be separated based on the kinds of work it performs. 例如,假设应用程序中包含两个逻辑,其中一个逻辑标识要显示给用户的注意事项,另一个以特定方式设置这些注意事项的格式,使其更加显眼。For instance, consider an application that includes logic for identifying noteworthy items to display to the user, and which formats such items in a particular way to make them more noticeable. 负责选择为哪些事项设置格式的行为应与负责设置格式的行为区分开,因为这两种行为只是碰巧彼此相关联的独立关注点。The behavior responsible for choosing which items to format should be kept separate from the behavior responsible for formatting the items, since these behaviors are separate concerns that are only coincidentally related to one another.

从体系结构上来说,按此原则有逻辑地构建应用程序应将核心业务行为与基础结构及用户界面逻辑区分开。Architecturally, applications can be logically built to follow this principle by separating core business behavior from infrastructure and user-interface logic. 理想情况下,业务规则和逻辑应单独位于一个项目中,且该项目不依赖于应用程序中的其他项目。Ideally, business rules and logic should reside in a separate project, which should not depend on other projects in the application. 此区分操作可帮助确保该业务模型易于测试,且可在不与低级别实现详细信息紧密耦合的情况下逐步改进。This separation helps ensure that the business model is easy to test and can evolve without being tightly coupled to low-level implementation details. 在应用程序体系结构的使用层背后,关注点分离是核心设计思想。Separation of concerns is a key consideration behind the use of layers in application architectures.

封装Encapsulation

应用程序的不同部分应通过封装与应用程序中的其他部分隔离开。Different parts of an application should use encapsulation to insulate them from other parts of the application. 只要不违反外部协定,应用程序组件和层应能在不中断其协作者的情况下调整其内部实现。Application components and layers should be able to adjust their internal implementation without breaking their collaborators as long as external contracts are not violated. 正确使用封装有助于在应用程序设计中实现松散耦合及模块化,因为只要维持相同的接口,就可以用替代实现来替代对象和包。Proper use of encapsulation helps achieve loose coupling and modularity in application designs, since objects and packages can be replaced with alternative implementations so long as the same interface is maintained.

在类中实现封装的方式是限制对该类的内部状态的外部访问权限。In classes, encapsulation is achieved by limiting outside access to the class's internal state. 如果外部参与者想操作对象的状态,则应通过明确定义的函数(或属性 setter)来进行操作,而非直接访问该对象的私有状态。If an outside actor wants to manipulate the state of the object, it should do so through a well-defined function (or property setter), rather than having direct access to the private state of the object. 同样,应用程序组件和应用程序本身应公开明确定义的接口供协作者使用,而非让协作者直接修改其状态。Likewise, application components and applications themselves should expose well-defined interfaces for their collaborators to use, rather than allowing their state to be modified directly. 这样一来,只要公共协定得到维护,你就可以不断改进应用程序的内部设计,而无需担心会中断协作者。This frees the application's internal design to evolve over time without worrying that doing so will break collaborators, so long as the public contracts are maintained.

依赖关系反转Dependency inversion

应用程序中的依赖关系方向应该是抽象的方向,而不是实现详细信息的方向。The direction of dependency within the application should be in the direction of abstraction, not implementation details. 大部分应用程序都是这样编写的:编译时依赖关系顺着运行时执行的方向流动,从而生成一个直接依赖项关系图。Most applications are written such that compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph. 也就是说,如果模块 A 调用模块 B 中的函数,而模块 B 又调用模块 C 中的函数,则编译时 A 取决于 B,而 B 又取决于 C,如图 4-1 中所示。That is, if module A calls a function in module B, which calls a function in module C, then at compile time A will depend on B, which will depend on C, as shown in Figure 4-1.

直接依赖项关系图

图 4-1Figure 4-1. 直接依赖项关系图。Direct dependency graph.

应用依赖关系反转原则后,A 可以调用 B 实现的抽象上的方法,让 A 可以在运行时调用 B,而 B 又在编译时依赖于 A 控制的接口(因此,典型的编译时依赖项发生反转)。Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at runtime, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). 运行时,程序执行的流程保持不变,但接口引入意味着可以轻松插入这些接口的不同实现。At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.

反转依赖项关系图

图 4-2Figure 4-2. 反转依赖项关系图。Inverted dependency graph.

依赖项反转是生成松散耦合应用程序的关键一环,因为可以将实现详细信息编写为依赖并实现更高级别的抽象,而不是相反。Dependency inversion is a key part of building loosely coupled applications, since implementation details can be written to depend on and implement higher-level abstractions, rather than the other way around. 因此,生成的应用程序的可测试性、模块化程度以及可维护性更高。The resulting applications are more testable, modular, and maintainable as a result. 遵循依赖关系反转原则可实现依赖关系注入。The practice of dependency injection is made possible by following the dependency inversion principle.

显式依赖关系Explicit dependencies

方法和类应显式要求正常工作所需的任何协作对象。Methods and classes should explicitly require any collaborating objects they need in order to function correctly. 通过类构造函数,类可以标识其实现有效状态和正常工作所需的内容。Class constructors provide an opportunity for classes to identify the things they need in order to be in a valid state and to function properly. 如果定义的类可供构造和调用,但仅在具备特定全局组件或基础结构组件时正常工作,则这些类对其客户端而言就不诚实。If you define classes that can be constructed and called, but that will only function properly if certain global or infrastructure components are in place, these classes are being dishonest with their clients. 构造函数协定将告知客户端,它只需要指定的内容(如果类只使用无参数构造函数,则可能不需要任何内容),但随后在运行时,结果发现对象确实需要某些其他内容。The constructor contract is telling the client that it only needs the things specified (possibly nothing if the class is just using a parameterless constructor), but then at runtime it turns out the object really did need something else.

若遵循显式依赖关系原则,类和方法就会诚实地告知客户端其需要哪些内容才能工作。By following the explicit dependencies principle, your classes and methods are being honest with their clients about what they need in order to function. 遵循此原则可以让代码更好地自我记录,并让代码协定更有利于用户,因为用户相信只要他们以方法或构造函数参数的形式提供所需的内容,他们使用的对象在运行时就能正常工作。Following the principle makes your code more self-documenting and your coding contracts more user-friendly, since users will come to trust that as long as they provide what's required in the form of method or constructor parameters, the objects they're working with will behave correctly at run time.

单一责任Single responsibility

单一责任原则适用于面向对象的设计,但也可被视为类似于分离关注点的体系结构原则。The single responsibility principle applies to object-oriented design, but can also be considered as an architectural principle similar to separation of concerns. 它指出对象只应有一个责任,并且只能因为一个原因更改对象。It states that objects should have only one responsibility and that they should have only one reason to change. 具体而言,只在必须更新对象执行其唯一责任的方式时才应更改对象。Specifically, the only situation in which the object should change is if the manner in which it performs its one responsibility must be updated. 遵循这一原则有助于生成更松散耦合和模块化的系统,因为许多类型的新行为可以作为新类实现,而不是通过向现有类添加其他责任。Following this principle helps to produce more loosely coupled and modular systems, since many kinds of new behavior can be implemented as new classes, rather than by adding additional responsibility to existing classes. 添加新类始终比更改现有类安全,因为还没有任何代码依赖于新类。Adding new classes is always safer than changing existing classes, since no code yet depends on the new classes.

在整体应用程序中,可以在高级别将单一责任原则应用于应用程序中的层。In a monolithic application, we can apply the single responsibility principle at a high level to the layers in the application. 显示责任应位于 UI 项目中,而数据访问责任应位于基础结构项目中。Presentation responsibility should remain in the UI project, while data access responsibility should be kept within an infrastructure project. 业务逻辑应位于应用程序核心项目中,该项目易于测试,并且可以独立于其他责任进行逐步改进。Business logic should be kept in the application core project, where it can be easily tested and can evolve independently from other responsibilities.

将此原则应用到应用程序体系结构及其逻辑终结点时,你将获得微服务。When this principle is applied to application architecture and taken to its logical endpoint, you get microservices. 给定的微服务应具有单一责任。A given microservice should have a single responsibility. 一般而言,如果需要扩展系统的行为,最好通过添加其他微服务来实现,而不要向现有微服务添加责任。If you need to extend the behavior of a system, it's usually better to do it by adding additional microservices, rather than by adding responsibility to an existing one.

详细了解微服务体系结构Learn more about microservices architecture

不要自我重复 (DRY)Don't repeat yourself (DRY)

应用程序应避免在多个位置指定与特定概念相关的行为,因为这种做法经常会导致出错。The application should avoid specifying behavior related to a particular concept in multiple places as this practice is a frequent source of errors. 有时,如果要求发生变化,将要求更改此行为。At some point, a change in requirements will require changing this behavior. 至少有一个行为实例可能无法更新,系统行为可能不一致。It's likely that at least one instance of the behavior will fail to be updated, and the system will behave inconsistently.

请将逻辑封装在编程构造中,而不要重复该逻辑。Rather than duplicating logic, encapsulate it in a programming construct. 让此构造成为针对此行为的单一权限,并让应用程序中需要此行为的任何其他部分都使用新的构造。Make this construct the single authority over this behavior, and have any other part of the application that requires this behavior use the new construct.

备注

避免将恰巧重复的行为绑定在一起。Avoid binding together behavior that is only coincidentally repetitive. 例如,只因为两个不同的常数具有相同的值,如果从概念上讲两个常数是指不同的内容,这并不意味着只应使用一个常数。For example, just because two different constants both have the same value, that doesn't mean you should have only one constant, if conceptually they're referring to different things.

持久性无感知Persistence ignorance

持久性无感知 (PI) 是指需要保持不变的类型,但其代码不受所选择的持久性技术的影响。Persistence ignorance (PI) refers to types that need to be persisted, but whose code is unaffected by the choice of persistence technology. .NET 中的这种类型有时被称为普通旧 CLR 对象 (POCO),因为这种类型无需继承特定的基类或实现特定的接口。Such types in .NET are sometimes referred to as Plain Old CLR Objects (POCOs), because they do not need to inherit from a particular base class or implement a particular interface. 持久性无感知非常有用,因为它可以让相同的业务模型以多种方式保持不变,让应用程序更加灵活。Persistence ignorance is valuable because it allows the same business model to be persisted in multiple ways, offering additional flexibility to the application. 持久性选择可能会随着时间的推移而发生变化,从一种数据库技术变为另一种数据库技术,或除应用程序一开始具备的持久性形式之外还需要其他形式的持久性(例如,除相关数据库之外还需使用 Redis 缓存或 Azure Cosmos DB)。Persistence choices might change over time, from one database technology to another, or additional forms of persistence might be required in addition to whatever the application started with (for example, using a Redis cache or Azure Cosmos DB in addition to a relational database).

违反此原则的一些示例包括:Some examples of violations of this principle include:

  • 必需的基类。A required base class.

  • 必需的接口实现。A required interface implementation.

  • 负责保存其自身的类(例如活动记录模式)。Classes responsible for saving themselves (such as the Active Record pattern).

  • 所需的无参数构造函数。Required parameterless constructor.

  • 需要 virtual 关键字的属性。Properties requiring virtual keyword.

  • 特定于持久性的必需特性。Persistence-specific required attributes.

要求类具有上述任何特性或行为会增加要保持不变的类型和持久性技术的选择之间的耦合,从而增加将来采用新的数据访问策略的难度。The requirement that classes have any of the above features or behaviors adds coupling between the types to be persisted and the choice of persistence technology, making it more difficult to adopt new data access strategies in the future.

有界上下文Bounded contexts

有界上下文是领域驱动设计中的中心模式。Bounded contexts are a central pattern in Domain-Driven Design. 它们可以将大型应用程序或组织分解为独立的概念模块,通过这种方式来解决复杂性问题。They provide a way of tackling complexity in large applications or organizations by breaking it up into separate conceptual modules. 每个概念模块表示各自独立的上下文(因此有界),并且可以独立改进。Each conceptual module then represents a context that is separated from other contexts (hence, bounded), and can evolve independently. 理想情况下,每个有界上下文都应该能够为其中的概念自由选择它自己的名称,并对其自己的持久性存储具有独占访问权限。Each bounded context should ideally be free to choose its own names for concepts within it, and should have exclusive access to its own persistence store.

至少,各 Web 应用程序应努力成为自己的有界上下文,为其业务模型提供自己的持久性存储,而不是与其他应用程序共享数据库。At a minimum, individual web applications should strive to be their own bounded context, with their own persistence store for their business model, rather than sharing a database with other applications. 有界上下文之间的通信通过编程接口进行,而不是通过共享数据库进行,这样可以引发业务逻辑和事件来响应发生的更改。Communication between bounded contexts occurs through programmatic interfaces, rather than through a shared database, which allows for business logic and events to take place in response to changes that take place. 有界上下文会紧密映射到微服务,后者在理想情况下也作为其自己的单独有界上下文实现。Bounded contexts map closely to microservices, which also are ideally implemented as their own individual bounded contexts.

其他资源Additional resources