架构原则

小窍门

此内容摘自电子书、使用 ASP.NET Core 和 Azure 构建新式 Web 应用程序,可在 .NET Docs 上获取或作为可脱机阅读的免费可下载 PDF。

架构现代 Web 应用程序:使用 ASP.NET Core 和 Azure 的电子书封面缩略图。

“如果建筑商像程序员编写程序一样建造建筑的话,那么第一只出现的啄木鸟就会毁灭文明。”
- 杰拉尔德·温伯格

你应该考虑到可维护性来构建和设计软件解决方案。 本部分概述的原则有助于指导你做出体系结构决策,从而产生干净、可维护的应用程序。 通常,这些原则将指导你从不紧密耦合到应用程序其他部分的离散组件中生成应用程序,而是通过显式接口或消息系统进行通信。

常见设计原则

分离关注点

开发时的指导原则是 分离关注点。 此原则断言,应根据软件执行的工作类型来分隔软件。 例如,请考虑一个应用程序,其中包含用于标识要向用户显示的值得注意的项目的逻辑,以及以特定方式设置此类项的格式,使它们更加明显。 负责选择要设置格式的项目的行为应与负责设置项目格式的行为分开,因为这些行为是单独关注的问题,它们只是彼此巧合相关的。

在体系结构上,可以通过将核心业务行为与基础结构和用户界面逻辑分开来构建应用程序,以遵循此原则。 理想情况下,业务规则和逻辑应驻留在单独的项目中,而不应依赖于应用程序中的其他项目。 这种分离有助于确保业务模型易于测试和改进,而无需紧密耦合到低级别实现细节(如果基础结构问题依赖于业务层中定义的抽象),也很有帮助。 关注点分离是应用程序体系结构中使用层背后的一个关键考虑因素。

封装

应用程序的不同部分应使用 封装 将它们与应用程序的其他部分隔离开来。 只要不违反外部协定,应用程序组件和层就应该能够调整其内部实现,而不会中断其协作者。 正确使用封装有助于在应用程序设计中实现松散耦合和模块化,因为只要维护相同的接口,对象和包就可以替换为替代实现。

在类中,封装是通过限制对类的内部状态的外部访问来实现的。 如果外部行为者想要操控对象的状态,则应通过定义完善的函数(或属性设置器)来实现,而不是直接访问对象的私有状态。 同样,应用程序组件和应用程序本身应公开定义完善的接口供其协作者使用,而不是允许直接修改其状态。 只要维护公共契约,这种方法就可以让应用程序的内部设计更自由地随时间演变,而无需担心会因此影响到合作伙伴。

可变全局状态与封装是对立的。 从一个函数中的可变全局状态获取的值不能保证在另一个函数中值保持不变(甚至在同一函数的后续部分)。 了解对可变的全局状态的担忧是 C# 等编程语言支持不同的作用域规则的原因之一,这些规则从语句到方法再到类都被广泛应用。 值得注意的是,那些依赖中央数据库进行应用程序内部和之间集成的数据驱动型架构本身选择依赖数据库所表示的可变全局状态。 域驱动设计和整洁架构中的一个关键考虑因素是如何封装对数据的访问,以及如何确保应用程序状态不会因直接访问其持久性格式而失效。

依赖关系反转

应用程序中的依赖方向应该指向抽象层次,而非具体的实现细节。 大多数应用程序都编写为编译时依赖项流向运行时执行方向,从而生成直接依赖项关系图。 也就是说,如果类 A 调用类 B 和类 B 调用类 C 的方法,则在编译时类 A 将依赖于类 B,类 B 将依赖于类 C,如图 4-1 所示。

直接依赖项图

图 4-1. 直接依赖项关系图。

应用依赖项反转原则允许 A 对 B 实现的抽象调用方法,使 A 可以在运行时调用 B,但 B 依赖于编译时由 A 控制的接口(因此, 反转 典型的编译时依赖项)。 在运行时,程序执行流保持不变,但引入接口意味着可以轻松插入这些接口的不同实现。

倒排依赖项图

图 4-2. 倒排依赖项关系图。

依赖项反转 是构建松散耦合应用程序的关键部分,因为可以编写实现详细信息来依赖和实现更高级别的抽象,而不是以另一种方式实现。 因此,生成的应用程序更具可测试性、模块化性和可维护性。 通过遵循依赖项反转原则,可以实现 依赖项注入 的做法。

显式依赖项

方法和类应显式要求他们需要的任何协作对象才能正常运行。 它称为 显式依赖项原则。 通过类构造函数,类可以标识其实现有效状态和正常工作所需的内容。 如果定义可以构造和调用的类,但只有在某些全局组件或基础结构组件已到位时才能正常运行,则这些类对其客户端不 诚实 。 构造函数合约告诉客户端,它只需要特定的内容(如果类使用的是无参数构造函数,可能不需要任何东西),但是在运行时,结果表明对象实际上需要其他东西。

遵循明确依赖原则时,类和方法会清楚地向客户端表明它们需要什么才能正常运行。 遵循原则使代码更具自我记录,并且编码协定更加用户友好,因为只要用户提供方法或构造函数参数形式的必需内容,他们正在使用的对象在运行时的行为就会正确。

单一责任

单一责任原则适用于面向对象的设计,但也可以被视为类似于关注点分离的体系结构原则。 它指出,对象应只有一个职责,并且它们应该只有一个变更的理由。 具体而言,对象应更改的唯一情况是,必须更新其执行一项责任的方式。 遵循此原则有助于生成更松散耦合和模块化的系统,因为许多新行为可以作为新类实现,而不是将额外的责任添加到现有类。 添加新类总是比更改现有类更安全,因为还没有代码依赖于新类。

在单体应用程序中,我们可以在高层次上将单一职责原则应用于应用程序的各层。 呈现责任应保留在 UI 项目中,而数据访问责任应保留在基础结构项目中。 业务逻辑应保留在应用程序核心项目中,可在其中轻松进行测试,并且可以独立于其他职责进行发展。

当此原则应用于应用程序体系结构并将其带到其逻辑终结点时,将获得微服务。 给定的微服务应承担单个责任。 如果需要扩展系统的行为,通常最好添加其他微服务,而不是向现有微服务添加责任。

详细了解微服务体系结构

不要自我重复 (DRY)

应用程序应避免在多个位置指定与特定概念相关的行为,因为这种做法是错误的频繁来源。 在某些时候,要求的变化需要更改此行为。 可能至少有一个行为实例无法更新,并且系统的行为不一致。

而不是复制逻辑,而是将其封装在编程构造中。 将此构造设置为对该行为的唯一控制,并让任何需要该行为的应用程序部分使用新的构造。

注释

避免将仅巧合重复的行为绑定在一起。 例如,仅仅因为两个不同的常量具有相同的值,这并不意味着你只应有一个常量(从概念上讲它们指的是不同的内容)。 重复始终优于与错误的抽象耦合。

持久性无感知

持久性无知 (PI)是指需要持久保存的类型,但其代码不受持久性技术选择影响。 .NET 中的此类类型有时称为普通旧 CLR 对象(POCO),因为它们不需要从特定基类继承或实现特定接口。 持久性无知是有价值的,因为它允许以多种方式保留相同的业务模型,从而为应用程序提供额外的灵活性。 持久性选择可能会随着时间推移而变化,从一种数据库技术更改为另一种,或者除了使用关系数据库之外,还需要其他形式的持久性(例如,除了关系数据库外,使用 Redis 缓存或 Azure Cosmos DB)。

违反此原则的一些示例包括:

  • 必要的基类。

  • 必需的接口实现。

  • 负责保存其自身的类(例如活动记录模式)。

  • 所需的无参数构造函数。

  • 需要 virtual 关键字的属性。

  • 特定于持久性的必需特性。

类具有上述任何特征或行为的要求增加了要持久化的类型与持久性技术选择之间的耦合,使得将来采用新的数据访问策略更加困难。

有界上下文

边界上下文 是 Domain-Driven 设计中的中心模式。 它们通过将其分解为单独的概念模块,为大型应用程序或组织提供一种解决复杂性的方法。 然后,每个概念模块表示一个上下文,该上下文与其他上下文(因此,边界)分离,并且可以独立发展。 理想情况下,每个边界上下文都应可以自由地为其中的概念选择自己的名称,并且应该具有对其自己的持久性存储的独占访问权限。

至少,单个 Web 应用程序应努力成为自己的边界上下文,并为其业务模型使用自己的持久性存储,而不是与其他应用程序共享数据库。 边界上下文之间的通信通过编程接口进行,而不是通过共享数据库进行,从而允许业务逻辑和事件在响应所发生的更改时发生。 有界上下文会紧密映射到微服务,后者在理想情况下也作为其自己的单独有界上下文实现。

其他资源