跳转到内容

设计与架构

软件设计与架构 — 经典书籍综合解读

Section titled “软件设计与架构 — 经典书籍综合解读”

1. Software Architecture in Practice (4th Edition) — Len Bass, Paul Clements, Rick Kazman

Section titled “1. Software Architecture in Practice (4th Edition) — Len Bass, Paul Clements, Rick Kazman”

软件架构是对系统进行推理所需的一组结构,包括软件元素、元素间的关系,以及两者的属性。本书的核心论点是:架构是需求(尤其是质量属性)与实现之间的关键桥梁——它是最早期的设计决策,也是最难更改且对系统成败影响最大的决策。架构的本质并非画框图和连线,而是做出有原则的决策以实现可度量的质量属性。

  • 架构由结构组成——静态结构(模块)、动态结构(组件与连接器)和分配结构(映射到组织或物理元素)。
  • 每个系统都有架构,无论它是否被记录下来。系统的架构独立于对它的任何描述。
  • 架构是一种抽象——它有意省略了对系统推理无用的元素细节。
  • 架构影响周期(architecture influence cycle):架构受利益相关者、开发组织、技术环境和架构师经验的影响。反过来,架构也会影响这些因素。

本书最具特色的贡献是对质量属性(QAs)的系统化处理:

  • 质量属性是那些决定系统是否满足利益相关者需求(功能之外)的”-ilities”(可用性、可修改性、性能、安全性、可测试性、易用性等)。
  • 功能在很大程度上与架构正交——几乎任何架构都能实现任何功能。然而,质量属性与架构深度相关。
  • 质量属性必须通过质量属性场景(quality attribute scenarios)来具体化,包含六个部分:(1) 刺激源、(2) 刺激、(3) 环境、(4) 制品、(5) 响应、(6) 响应度量。例如,一个可用性场景:“在正常运行期间,系统收到一个未预期的外部消息;系统继续运行无宕机,消息在5秒内被记录。”
  • 质量属性之间经常冲突:提升性能可能损害可修改性;提升安全性可能损害易用性。架构本质上就是管理质量属性之间的权衡

战术是直接实现质量属性响应的基本设计决策,是构成架构模式的基础构件。

  • 可用性战术:检测故障(ping/echo、心跳、异常、投票),从故障中恢复(主动冗余、被动冗余、备用件、回滚、状态重同步),预防故障(移出服务、事务、预测模型)。
  • 可修改性战术:减小模块大小,提高内聚,减少耦合(封装、使用中间层、限制依赖、重构、抽象公共服务),延迟绑定时间(配置文件、多态、组件替换、遵守已定义的协议)。
  • 性能战术:控制资源需求(管理事件速率、限制执行时间、优先处理事件、减少开销、限定队列大小),管理资源(增加资源、引入并发、维护计算或数据的多个副本、调度资源)。
  • 安全性战术:检测攻击(入侵检测、服务拒绝检测、验证消息完整性、检测消息延迟),抵御攻击(认证、授权、维护数据保密性、维护数据完整性、限制暴露、限制访问),应对攻击(撤销访问、锁定计算机、通知相关方),从攻击中恢复(审计追踪、恢复)。
  • 可测试性战术:控制与观察(专用接口、录制/回放、本地化状态存储、抽象数据源、沙箱、可执行断言)。
  • 易用性战术:支持用户主动(取消、撤销、暂停/恢复、聚合),支持系统主动(维护任务模型、维护用户模型、维护系统模型)。

模式是跨系统反复出现的战术组合:

  • 模块模式:分层模式(严格分层 vs. 宽松分层)。
  • 组件与连接器模式:Broker(代理)、Model-View-Controller (MVC)、Pipe-and-Filter(管道与过滤器)、Client-Server(客户端-服务器)、Peer-to-Peer(对等)、Service-Oriented Architecture (SOA,面向服务架构)、Publish-Subscribe(发布-订阅)、Shared-Data(共享数据)。
  • 分配模式:Map-Reduce、Multi-tier(多层)。

每种模式的描述包含:它包含的元素、元素间的关系、它施加的约束,以及它的弱点/权衡。

ADD 是本书推荐的架构设计方法:

  1. 选择系统中要设计的元素(从整个系统开始)。
  2. 识别该元素的架构驱动因素——最显著影响架构的功能需求、质量属性场景和约束的组合。
  3. 生成设计方案——选择能解决驱动因素的模式、战术和部署模型。
  4. 盘点剩余需求,选择下一次迭代的输入。
  5. 重复步骤1-4,对每个需要进一步分解的元素执行。

ADD 是迭代式的:每一轮分解一个选定的元素,以驱动因素为输入。

Architecture Tradeoff Analysis Method (ATAM,架构权衡分析方法) 是一种结构化评估过程:

  1. 向利益相关者介绍 ATAM。
  2. 展示业务驱动因素。
  3. 展示架构。
  4. 识别架构方法(所使用的模式/战术)。
  5. 生成质量属性效用树:将质量目标层次化分解为具体场景,每个场景标注重要性和难度。
  6. 分析架构方法与场景的匹配度,识别敏感点(影响特定QA的架构决策)、权衡点(影响多个QA的决策),以及风险/非风险
  7. 与更广泛的利益相关者群体一起头脑风暴并优先排序场景。
  8. 使用新优先排序的场景重新分析。
  9. 展示结果。

ATAM 的关键输出是:一组优先排序的场景、一组风险、敏感点和权衡点。

  • 架构文档的存在是为了向利益相关者传达架构。不同的利益相关者需要不同的视图。
  • 视图(Views):视图是一组架构元素及其关系的表示。本书识别了三种视图类型(模块、组件与连接器、分配),以及每种类型中的特定视图风格。
  • 文档包应包括:视图、视图间的映射(因为一个视图中的元素可能对应另一个视图中的元素)、决策的理由,以及文档路线图。
  • “Views and Beyond”(视图与超越)方法:记录利益相关者需要的任何视图,并用跨视图文档补充说明视图间的关系。
  • 记录决策背后的理由,而不仅仅是决策本身。
  • Conway’s Law(康威定律):设计系统的组织受限于产生与其沟通结构相镜像的设计。本书既将其作为一种观察来讨论,也作为架构师可以利用或必须解决的因素。
  • 架构能力应成为组织资产。本书讨论了架构能力框架和架构师在组织中的角色。
  • Technical debt(技术债务):权宜之计对架构完整性的累积成本。本书讨论了如何识别、衡量和管理技术债务。
  • 敏捷中的架构:本书讨论了架构与敏捷方法如何共存——架构”跑道”(runway)的概念,即有意增量构建而非偶然涌现。
  • DevOps 与架构:部署、监控和运维质量属性(可部署性、可监控性)是一等公民的架构关注点。
  • A-7E 航电系统作为案例反复出现,展示了通过信息隐藏实现可修改性。
  • 万维网被作为可扩展性和可修改性的架构案例来分析。
  • 效用树(utility tree)是一种实用的可视化工具,迫使利益相关者从模糊的质量目标(“系统应该很快”)转向精确、可度量的场景。
  • 将架构仅视为高层框图,而不指定行为、质量属性和理由。
  • 混淆功能需求与质量属性需求——功能可以用任何架构实现,但质量属性不行。
  • 不显式评估架构,导致”靠希望驱动的开发”。
  • 忽视架构决策的政治和组织背景。
  • 为架构师本人而非为需要文档的受众编写架构文档。

2. Patterns of Enterprise Application Architecture — Martin Fowler

Section titled “2. Patterns of Enterprise Application Architecture — Martin Fowler”

企业应用的特征是需要持久化操作复杂数据,通常有大量用户并发访问和复杂的业务规则,同时需要通过多种接口与其他系统集成。没有单一的”最佳”架构——正确的选择取决于领域逻辑的复杂性和数据访问需求的特征。Fowler 的核心信息是:理解模式,理解有利于每种模式的力量,然后将模式与你的实际情况匹配。

Fowler 将企业应用架构组织为层:

  1. 表示层(Presentation layer)——处理用户界面和显示。
  2. 领域逻辑层(Domain logic layer)——核心业务规则。
  3. 数据源层(Data source layer)——与数据库、消息系统等通信。

基本规则:依赖应向下流动。上层可以依赖下层,但反之不行。每层应可替换而不影响其他层。

组织领域逻辑的三种主要模式:

  1. Transaction Script(事务脚本):将业务逻辑组织为每个业务事务一个过程。每个过程接收来自表示层的输入,进行处理(验证、计算、数据库操作),然后返回结果。对领域复杂度不高的应用简单有效。隐喻就是一个从头到尾协调单个事务的”脚本”。

  2. Domain Model(领域模型):一个包含数据和行为的领域对象模型。对象对应现实世界的实体,并包含管理它们的业务规则。丰富的领域模型使用继承、策略和其他面向对象模式。对复杂领域逻辑非常强大,但需要更多基础设施(尤其是 ORM 层)。Fowler 指出这是面向对象设计真正发挥威力的地方。

  3. Table Module(表模块):一个处理数据库表中所有行的业务逻辑的单一实例。复杂度介于 Transaction Script 和 Domain Model 之间。特别适合提供 Record Set 抽象的环境(如 .NET DataSet)。一个对象处理所有”合同”行,而不是每个合同一个对象。

Service Layer(服务层):一个额外的模式,用一层服务定义应用的边界,建立一组可用操作,并协调每个操作中的应用响应。它可以是 Domain Model 上的薄外观(facade),也可以包含重要的工作流逻辑。

领域对象如何与数据库交互:

  1. Table Data Gateway(表数据网关):一个充当数据库表网关的对象。每个表一个实例;该表的所有 SQL 都在这个类中。返回简单数据结构(Record Sets)。适合与 Transaction Script 和 Table Module 搭配使用。

  2. Row Data Gateway(行数据网关):一个充当数据库中单条记录网关的对象。每行一个实例。对象的字段对应列。不包含领域逻辑——仅用于数据访问。

  3. Active Record(活动记录):一个包装行、封装数据库访问并在该数据上添加领域逻辑的对象。类似于 Row Data Gateway 但包含业务规则。适用于领域逻辑不太复杂且领域模型与数据库模式紧密对应的情况。

  4. Data Mapper(数据映射器):一层映射器,在对象和数据库之间移动数据,同时保持两者独立。领域对象不知道数据库的存在,数据库也不知道对象的存在。当 Domain Model 与数据库模式有显著差异时必不可少。实现最复杂但提供最大程度的解耦。

Fowler 将此称为 “object-relational impedance mismatch”(对象-关系阻抗失配) ——在对象(标识、继承、关联、封装)和关系表(行、外键、连接、规范化)之间映射的根本困难。

  • Identity Map(标识映射):确保每个数据库行在每个会话中只加载一次,防止同一行的多个内存副本造成不一致。本质上是按主键索引的注册表。
  • Unit of Work(工作单元):维护受业务事务影响的对象列表,并协调写入更改和解决并发问题。追踪新建、脏(已修改)和已删除的对象,然后在一次协调操作中刷新到数据库。
  • Lazy Load(延迟加载):一个不包含你需要的所有数据但知道如何获取它的对象。四种变体:延迟初始化、虚拟代理、值持有者、幽灵。Fowler 警告了朴素延迟加载可能导致的 “ripple loading”问题(N+1 查询)。
  • Identity Field(标识字段):在对象中保存数据库 ID 字段,以维护内存对象与数据库行之间的标识关系。
  • Foreign Key Mapping(外键映射):将对象间的关联映射为表间的外键引用。
  • Association Table Mapping(关联表映射):将多对多关联映射到数据库中的链接表。
  • Dependent Mapping(依赖映射):将子类的映射委托给父类的映射器,用于子类在其父类之外没有意义或标识的情况。
  • Embedded Value(嵌入值):将作为 Value Object 的对象映射到拥有实体的表的列中。
  • Serialized LOB(序列化大对象):通过将对象图序列化为单个大对象(如 BLOB/CLOB)并存储在单个数据库列中来保存。
  • Single Table Inheritance(单表继承):将整个类层次结构映射到带有类型鉴别列的单个数据库表。
  • Class Table Inheritance(类表继承):将层次结构中的每个类映射到自己的数据库表。
  • Concrete Table Inheritance(具体表继承):将每个具体类映射到包含所有继承字段的自己的数据库表。
  • Inheritance Mapper(继承映射器):一种组织处理继承层次结构的数据库映射器的结构。
  • Model View Controller (MVC):将表示分为三个角色——Model(领域对象/数据)、View(显示)和 Controller(处理用户输入并更新模型)。Fowler 强调 MVC 的关键分离是 Model 与表示(View + Controller)之间的分离,而不主要是 View 和 Controller 之间的分离。
  • Page Controller(页面控制器):一个处理网站特定页面或动作请求的对象。每个逻辑页面一个控制器。
  • Front Controller(前端控制器):一个接收所有请求并分发给适当命令/处理器对象的单一处理器对象。为公共处理(安全、会话管理)提供单一入口点。
  • Template View(模板视图):通过在静态 HTML 页面中嵌入标记来渲染信息。模板经过处理以将标记替换为动态数据(如 JSP、ASP、ERB)。
  • Transform View(转换视图):逐元素转换领域数据并生成 HTML 的视图。XSLT 是典型示例。
  • Two Step View(两步视图):分两步将领域数据转换为 HTML:首先将领域数据转换为逻辑表示结构,然后将逻辑结构渲染为 HTML。当你希望全站外观一致且可在一处更改时非常有用。
  • Application Controller(应用控制器):处理屏幕导航和应用流程的集中点。将表示与流程逻辑解耦。
  • Remote Facade(远程外观):在细粒度对象上提供粗粒度外观,以提高网络效率。“分布式对象设计第一定律”(归于 Fowler,最初来自 Waldo 等人):“不要分布你的对象。” 分布引入了延迟、复杂性和部分失败。当必须分布时,使用粗粒度接口。
  • Data Transfer Object (DTO,数据传输对象):一个在进程间传输数据的对象。它是一个没有业务逻辑的简单容器,通过将数据批量打包到一次传输中来减少远程调用次数。

Fowler 的著名警告:“进程间调用比进程内调用昂贵几个数量级。” 企业架构中的头号错误是引入不必要的分布。

  • Optimistic Offline Lock(乐观离线锁):允许多个会话并发访问相同数据,但在提交时检测冲突(使用版本号或时间戳)。如果检测到冲突,事务将被回滚。适用于冲突较少的情况。
  • Pessimistic Offline Lock(悲观离线锁):通过只允许一个会话同时访问数据来防止冲突。使用锁(读锁、写锁、读写锁)。更安全但减少并发,可能导致死锁。
  • Coarse-Grained Lock(粗粒度锁):用单个锁锁定一组相关对象,而不是逐个锁定。减少锁的复杂性和开销,但可能锁定超出严格必要的范围。
  • Implicit Lock(隐式锁):将锁集成到应用框架中,使开发人员无需手动管理锁,减少忘记获取锁的机会。

在多服务器环境中,会话状态存储在哪里:

  • Client Session State(客户端会话状态):将会话状态存储在客户端(如 cookie、隐藏表单字段、URL 参数)。简单且可扩展(无服务器亲和性),但受带宽、安全性和数据大小的限制。
  • Server Session State(服务器端会话状态):将会话状态存储在服务器上,通常以发送给客户端的会话 ID 为键。编程简单,但引入服务器亲和性问题和内存消耗。
  • Database Session State(数据库会话状态):将会话状态存储在数据库中。可在服务器故障中存活,避免服务器亲和性,但增加数据库负载和序列化复杂性。
  • Gateway(网关):一个封装对外部系统或资源访问的对象。
  • Mapper(映射器):一个在两个独立对象之间建立通信的对象。
  • Layer Supertype(层超类型):一层中所有对象的超类型。放置公共行为的地方。
  • Separated Interface(分离接口):在不同于其实现的包中定义接口,实现依赖反转。
  • Registry(注册表):一个其他对象可以用来查找公共对象和服务的知名对象(本质上是全局查找)。
  • Value Object(值对象):一个小而简单的对象(如 Money 或 DateRange),其相等性基于字段值而非标识。不可变。
  • Money(货币):表示货币值、正确处理币种和舍入的模式。Fowler 特别指出了用浮点数表示货币的危险。
  • Special Case(特殊情况,即 Null Object):一个为特定情况提供特殊行为的子类,避免条件逻辑。
  • Plugin(插件):在配置时而非编译时链接类,实现灵活扩展。
  • Service Stub(服务桩):当真正的服务不可用时,为测试或开发提供服务的桩实现。
  • Record Set(记录集):表格数据的内存表示——数据库查询的结果,可以与数据库断开连接并进行操作。
  • Fowler 全书使用 revenue recognition(收入确认) 的例子——一个计算合同收入何时可以确认的系统。这个例子特意选择了足够复杂的业务规则,以展示 Transaction Script、Table Module 和 Domain Model 方法之间的差异。
  • “分布式对象设计第一定律:不要分布你的对象”是企业软件中被引用最多的名言之一。
  • Fowler 用 money(货币)date range(日期范围) 作为 Value Object 的典型示例——如果内容相同则相等,无论内存标识如何。
  • impedance mismatch(阻抗失配) 的隐喻贯穿全书。
  • 不确定时,从最简单的模式开始(如 Transaction Script),只在复杂性需要时才演进到更复杂的模式(Domain Model)。
  • 在任何非平凡应用中,将 Unit of WorkIdentity Map 配合使用来管理对象持久化。
  • Gateway 模式是将应用与外部依赖隔离的第一道防线。
  • 过早分布对象——远程调用的开销是最常见的性能杀手。
  • 当 Transaction Script 就足够时使用 Domain Model——过度工程化领域层。
  • 忽视阻抗失配——假装对象和关系表天然1:1映射会导致脆弱、泄漏的抽象。
  • 未能管理乐观/悲观并发——有并发用户的企业应用如果没有明确的并发策略,将出现数据损坏。
  • 朴素地使用延迟加载,导致 N+1 查询问题。

3. Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans

Section titled “3. Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans”

软件中最重要的复杂性不是技术层面的,而是在领域本身——软件所服务的业务领域。当领域的复杂性没有通过精心建模来正面应对时,再多的技术手段也无法挽救项目。解决方案是将领域模型置于开发过程的中心,并建立一套开发人员和领域专家共享的通用语言(ubiquitous language)。

  • 通用语言是开发人员和领域专家共享的语言,用于代码、对话、文档和图表中。它既不是业务术语表,也不是技术规范——它是模型本身的语言。
  • 如果语言出现分裂(开发人员使用一套术语,领域专家使用另一套),模型将碎片化,软件将无法正确捕捉领域。
  • 改变语言就是改变模型。当领域专家说出不符合当前模型的内容时,这是模型必须演进的信号。
  • Evans 使用 cargo shipping system(货运系统) 作为全书示例。当团队在”handling events”与”delivery specification”与”routing”之间纠结时,解决方案来自于提炼语言,直到开发人员和航运专家都能用相同的术语流畅地推理。

Model-Driven Design(模型驱动设计)

Section titled “Model-Driven Design(模型驱动设计)”
  • 模型不是图表或文档——它是驱动设计决策的有组织的知识体系。
  • 代码是模型的主要表达。如果代码不反映模型,模型就是虚构的。
  • 动手建模者(Hands-on modelers):写代码的人也必须参与建模。如果建模由单独的团队完成然后”交接”,模型与代码的连接将退化。

Entities(实体,又称引用对象)

  • 具有贯穿时间和不同表示的独特标识的对象。一个人、一个订单、一个银行账户——即使所有属性都改变了,实体仍然是”同一个”实体。
  • 必须仔细定义标识。有时是自然键(如社会安全号码),有时是代理键(生成的ID)。
  • Entity 的关注点在于标识的连续性和生命周期。

Value Objects(值对象)

  • 描述某些特征或属性但没有概念标识的对象。具有相同属性的两个 Value Object 是可互换的。
  • 示例:地址、颜色、金额、日期范围。
  • Value Object 应该是不可变的。它们可以自由共享和复制。
  • Evans 强调,许多默认被建模为 Entity 的概念实际上应该是 Value Object——这大大简化了设计。

Aggregates(聚合)

  • 一组关联对象的集群,作为数据变更的单元来处理。每个聚合有一个根实体(Aggregate Root)和一个边界。
  • 聚合外部的对象只能持有对聚合根的引用,永远不能引用内部元素。
  • 聚合边界内的所有不变量由聚合根在任何状态变更时强制执行。
  • 聚合是一致性的基本单位——一个事务应最多修改一个聚合。
  • Evans 用 Car(汽车) 聚合作为例子:Car 是根,车轮、引擎等是内部元素。你不能从外部直接引用车轮——你要通过 Car。

Repositories(仓储)

  • 提供某种类型所有对象的内存集合的假象。客户端通过指定条件请求对象;仓储封装存储、检索和搜索机制。
  • 仓储仅为聚合根提供——你不会为每个实体或值对象创建仓储。
  • 它们将领域模型与数据访问技术解耦。

Factories(工厂)

  • 封装创建复杂对象或聚合所需的知识。当创建逻辑变得复杂时(特别是聚合必须在有效、一致的状态下创建时),Factory 提供一个干净的接口。
  • Factory 可以是独立对象、聚合根上的 Factory Method,甚至是 Service 上的方法。

Services(服务)

  • 当领域中的重要过程或转换不是实体或值对象的自然职责时,应表达为 Domain Service(领域服务)
  • Domain Service 是无状态的,其接口用领域模型的术语定义。
  • 不要与 Application Services(协调用例)或 Infrastructure Services(提供技术能力如发送邮件)混淆。
  • Evans 警告:不要将 Service 作为垃圾堆。如果行为自然属于实体或值对象,它应该在那里。Service 用于真正跨越多个对象的操作。

Domain Events(领域事件)(在后来的 DDD 著作中更完整地引入,但概念起源于此):

  • 领域中发生的、领域专家关心的事情。事件是不可变的事实记录。
  • 用于解耦聚合——一个聚合可以发布事件,其他聚合或系统可以对此做出反应。

Modules(模块,即包)

  • 模块是一种建模工具,不仅仅是代码组织的便利。它们应该反映领域模型中有意义的划分。
  • 模块的名称应该是通用语言的一部分。
  • 模块间低耦合,模块内高内聚——与类设计相同的原则,但在更大的尺度上。

Evans 引入了几种使领域模型易于使用和演进的模式:

  • Intention-Revealing Interfaces(意图揭示接口):命名类和方法以描述其效果和目的,而非其机制。客户端应该不需要阅读实现就能理解方法的功能。
  • Side-Effect-Free Functions(无副作用函数):尽可能将领域逻辑放在返回结果而无可观察副作用的函数中。这使系统更容易测试和推理。
  • Assertions(断言):声明操作的后置条件和类的不变量。即使语言不强制执行,它们也可以作为文档并可被测试。
  • Conceptual Contours(概念轮廓):分解设计元素使其与领域中稳定、有意义的划分对齐。当模型的边界与领域的自然轮廓匹配时,设计更容易演进。
  • Standalone Classes(独立类):减少依赖网络——每个依赖都是负担。低耦合的极致是一个可以完全独立理解的类。
  • Closure of Operations(操作的闭合):在合适的地方,定义一个操作,其返回类型与参数类型相同。例如:两个 Money 值组合返回一个 Money。这受数学闭合性启发,使表达式可组合。

这是 DDD 中处理大规模系统和多团队的部分:

Bounded Context(限界上下文)

  • Bounded Context 是一个特定领域模型被定义和适用的边界。相同的词(如”Account”)在不同的 Bounded Context 中可能有完全不同的含义。
  • 每个 Bounded Context 应该有自己的通用语言、自己的模型,以及(理想情况下)自己的代码库或模块。
  • Evans 讲述了一个项目的故事,其中同一术语”Policy”在一个大型保险应用的不同部分意味着不同的东西——未能识别这一点导致了数月的混乱和集成缺陷。

Context Map(上下文映射)

  • Context Map 是一份文档(通常是可视化的),显示系统中所有 Bounded Context 之间的关系。它是现实地图,不是愿景——它记录实际存在的情况。
  • 上下文间关系的模式:
    • Shared Kernel(共享内核):两个团队共享领域模型的一个子集。变更需要双方团队同意。范围小,明确约定。
    • Customer-Supplier(客户-供应商):一个上下文(上游)向另一个(下游)提供数据。上游团队可能会也可能不会满足下游团队的需求。
    • Conformist(跟随者):下游团队简单地遵从上游模型,接受它提供的一切。用于下游团队对上游没有影响力的情况。
    • Anticorruption Layer (ACL,防腐层):下游团队构建一个翻译层,将上游模型转换为自己的模型。在与遗留系统或外部系统集成时至关重要,因为它们的模型不应泄漏到你的领域中。
    • Open Host Service(开放主机服务):定义一个协议,以一组服务的形式提供对子系统的访问。开放它以便多个消费者可以轻松集成。
    • Published Language(发布语言):使用文档完善的共享语言进行集成(如 XML schema、标准数据格式)。
    • Separate Ways(各行其道):当集成成本超过收益时,不要集成。让每个上下文走自己的路。
    • Big Ball of Mud(大泥球):承认现实——系统的某些部分没有可辨识的架构。在它周围画一个边界,不要让它泄漏出去。

Distillation(提炼)

  • Core Domain(核心域):模型中最有价值、最具差异化的部分。这是最好的开发人员应该工作的地方,也是建模投入最多的地方。
  • Generic Subdomains(通用子域):模型中必要但不具差异化的部分(如货币转换、地址验证)。使用现成方案或更简单的方法。
  • Domain Vision Statement(领域愿景声明):一份简短的文档(约一页),描述核心域及其代表的价值主张。
  • Highlighted Core(突出核心):一份简短文档(或注释),标识模型的核心元素,使其易于发现。
  • Cohesive Mechanisms(内聚机制):将不是核心域概念的复杂算法或计算分离到单独的机制中。领域模型委托给它们,但不会被它们搞得混乱。

Large-Scale Structure(大尺度结构)

  • 针对超大模型的可选组织模式:
    • Evolving Order(演进秩序):让大尺度结构涌现和演进;不要提前过度指定。
    • System Metaphor(系统隐喻):一个协调设计的单一隐喻(借自 XP)。有效时很强大,但隐喻误导时很危险。
    • Responsibility Layers(职责层):将领域组织为宽泛的职责层(如策略层、承诺层、运营层、决策支持层)。
    • Knowledge Level(知识层):将模型分为”知识级别”(规则、配置)和”操作级别”(日常实体)。知识级别描述操作级别能做什么。
    • Pluggable Component Framework(可插拔组件框架):当许多限界上下文必须互操作时,定义接口允许上下文被插入和替换。
  • cargo shipping system(货运系统) 是主要的贯穿示例。Delivery Specification、Itinerary、Handling Event 和 Route 等概念在全书中被迭代地精炼。
  • Evans 讲述了一个 PCB(印刷电路板)设计 项目的故事,突破来自于团队认识到”net”(组件间的电气连接)的概念是领域的核心,而非物理布局。
  • Anticorruption Layer(防腐层) 的隐喻:它就像一个翻译和适配器,让你可以说自己的语言,同时仍能与外部系统通信。
  • “Big Ball of Mud(大泥球)“——Evans 不假装它不存在;他承认它并说最好的做法就是遏制它。
  • 迭代建模会话(有时称为”模型探索漩涡”):建模、编码、与领域专家讨论、精炼的快速循环。
  • 在建模会话中使用 CRC 卡(Class-Responsibility-Collaboration)或非正式草图;不要过度使用 UML 正式化。
  • 朝更深洞察重构:当你发现一个更有表达力的模型时,重构代码以匹配。这不仅仅是代码清理——而是模型改进。
  • 在代码中而非仅在文档中强制执行聚合边界。
  • Anemic Domain Model(贫血领域模型):一个实体只有 getter 和 setter、所有行为都在 Service 中的模型。Evans 警告这是一种反模式,放弃了面向对象设计的好处。
  • 不投入通用语言——允许开发人员和领域专家说不同的语言。
  • 聚合做得太大。聚合应在仍能强制执行不变量的同时尽可能小。大聚合导致并发争用和事务失败。
  • 忽视战略设计——只关注战术模式(实体、值对象),而让限界上下文模糊、单体模型不受控制地增长。
  • 将 DDD 视为一套机械应用的模式,而非思考领域复杂性的方式。

4. Designing Data-Intensive Applications — Martin Kleppmann

Section titled “4. Designing Data-Intensive Applications — Martin Kleppmann”

当今大多数应用是数据密集型而非计算密集型——瓶颈在于数据的量、复杂性和变化速率,而非原始 CPU 速度。构建可靠、可扩展和可维护的数据系统需要深入理解用于存储、检索、处理和传输数据的工具和抽象。Kleppmann 的论点是:通过理解这些系统背后的基本原理,你可以在数据库、消息队列、缓存和处理框架的纷繁复杂的生态中做出明智的权衡,而不是基于炒作选择工具。

基础:可靠性、可扩展性、可维护性

Section titled “基础:可靠性、可扩展性、可维护性”

数据系统的三个基本关注点:

  • Reliability(可靠性):即使出现问题(硬件故障、软件缺陷、人为错误),系统仍能正确工作。故障(fault)不是失败(failure)——故障是组件偏离规格;失败是系统整体停止提供所需服务。目标是容错:从不可靠的组件构建可靠的系统。

    • 硬件故障:磁盘故障(硬盘 MTTF 约10-50年)、内存错误、网络分区、断电。传统上通过冗余处理(RAID、双电源)。云环境使多机冗余成为必需。
    • 软件故障:更难预测的系统性错误。例如:一个 Linux 内核 bug 导致所有实例同时崩溃;一个慢服务触发的级联故障。
    • 人为错误:是宕机的首要原因。缓解措施:设计最小化出错机会的系统、提供沙箱环境、充分测试、支持快速回滚、建立详细监控。
  • Scalability(可扩展性):随着系统增长(数据量、流量或复杂度),应有合理的方式应对。可扩展性不是一维标签(“系统 X 可扩展”)——你必须问”如果参数 X 增长,我们的应对计划是什么?”

    • 描述负载:使用特定于你系统的负载参数(如每秒请求数、读写比、同时活跃用户数、缓存命中率)。
    • Twitter 的 fan-out 问题:作为关键示例。当用户发推文时,是写一次然后在读取时查询所有关注者的时间线(读时扇出)?还是预计算每个关注者的时间线并写入所有关注者的缓存(写时扇出)?Twitter 从方法1迁移到方法2,然后采用混合方案:大多数用户使用写时扇出,但名人(拥有数百万关注者)使用读时扇出。
    • 描述性能:延迟 vs. 响应时间。使用百分位数(p50、p95、p99、p999)而非平均值,因为平均值掩盖了长尾。尾部延迟很重要,因为最慢的请求往往属于最有价值的客户(数据最多的用户)。队头阻塞(Head-of-line blocking):慢请求可能延迟队列中的后续请求。
    • 纵向扩展(垂直扩展,更强大的机器)vs. 横向扩展(水平扩展,更多机器)。大多数系统采用实用的混合方式。
  • Maintainability(可维护性):系统成本的大部分在于持续维护。三个设计原则:

    • Operability(可运维性):使运维团队能轻松保持系统运行。
    • Simplicity(简单性):通过使用良好的抽象来管理复杂性。Kleppmann 引用了 accidental complexity(来自实现而非问题域的偶然复杂性)作为缺陷的主要来源。
    • Evolvability(可演进性,即可扩展性):使系统易于适应新需求。
  • 关系模型(SQL):数据组织为关系(表),每个关系是元组(行)的无序集合。自1970年代以来占主导地位。优势:连接(join)、模式强制、成熟的优化。
  • 文档模型(NoSQL):数据存储为自包含文档(JSON/BSON)。更适合一对多关系和具有文档结构的数据。对多对多关系和连接较弱。
  • 图模型:用于数据以多对多关系为主的场景。两种模型:属性图(Neo4j)和三元组存储(RDF)。查询语言包括 Cypher 和 SPARQL。
  • 关系 vs. 文档的争论:文档数据库提供模式灵活性、因局部性带来的更好性能,以及更接近应用对象的数据模型。关系数据库提供更好的连接支持、多对多关系支持和模式强制。Kleppmann 认为这两种模型正在趋同——关系数据库添加 JSON 支持,文档数据库添加类连接功能。
  • Schema-on-read(读时模式)(文档)vs. Schema-on-write(写时模式)(关系):类似于动态类型 vs. 静态类型。两者都非普遍更优——取决于数据是同质的还是异质的。

两大存储引擎家族:

  • 日志结构存储引擎:SSTables、LSM-Trees(被 LevelDB、RocksDB、Cassandra、HBase 使用)。写优化。数据顺序写入内存中的 memtable,然后刷新到磁盘上已排序的不可变文件(SSTables)。后台压缩合并 SSTables。Bloom filter 优化对不存在键的读取。
  • 面向页的存储引擎:B-Trees(被大多数传统 RDBMS 使用)。将数据库分割为固定大小的页(通常 4KB)。平衡树结构,每页可原地更新。Write-ahead log (WAL) 确保崩溃恢复。

比较:LSM-trees 通常具有更好的写吞吐量和更有效的数据压缩。B-trees 具有更可预测的读性能和更强的事务语义。LSM-trees 中的压缩可能干扰正在进行的读写。

  • OLTP vs. OLAP:Online Transaction Processing(在线事务处理)系统处理大量短查询(点读、更新)。Online Analytical Processing(在线分析处理)系统处理少量但复杂的查询,扫描大量数据。这导致了不同的架构。
  • 数据仓库:数据的独立只读优化副本,通常使用 star schema(星型模式)(或 snowflake schema 雪花模式),包含一个中心事实表和周围的维度表。ETL(Extract-Transform-Load,抽取-转换-加载)过程填充仓库。
  • 列式存储:按列而非按行存储数据,极大提高分析查询的压缩率和扫描性能。列压缩技术:位图编码、游程编码。物化视图和数据立方体预聚合常见查询。
  • 数据编码格式:JSON、XML、CSV(人类可读但有限),Binary JSON 变体(MessagePack、BSON),Thrift 和 Protocol Buffers(基于模式、紧凑),Avro(基于模式、模式演进友好)。
  • Schema evolution(模式演进):前向兼容(新代码读旧数据)和后向兼容(旧代码读新数据)。Thrift/Protobuf 使用字段标签;Avro 结合使用写者模式和读者模式。
  • 数据流模式:通过数据库(数据库是给未来的自己的消息)、通过服务(REST、RPC)、通过异步消息(消息代理)。
  • REST vs. RPC:Kleppmann 指出 RPC 试图让远程调用看起来像本地调用,但这从根本上有缺陷,因为网络调用具有不同的失败模式(超时、重试、幂等性)、不同的延迟特征和不同的数据编码需求。

为什么要复制:让数据在地理上接近用户、容忍机器故障、扩展读吞吐量。

  • Single-leader replication(单主复制):一个副本(leader)接受写入;其他副本(follower)复制 leader 的写入日志。Follower 服务读取。简单,但 leader 是写入的瓶颈和单点故障。

    • 同步 vs. 异步复制:同步保证数据持久性但速度慢。异步速度快但在 leader 故障时有数据丢失风险。半同步(一个 follower 同步,其余异步)是常见折中。
    • 复制延迟问题:读自己的写、单调读、一致前缀读。Kleppmann 用生动的例子说明——例如,用户提交表单后刷新,却看不到自己的提交,因为读请求命中了一个滞后的 follower。
  • Multi-leader replication(多主复制):多个节点接受写入。用于多数据中心设置、离线客户端(每个设备是一个”leader”)。主要挑战是写冲突——如果两个 leader 并发修改相同数据,哪个写入获胜?冲突解决策略:last write wins(LWW,最后写入胜出——简单但丢数据)、合并值、自定义解决逻辑、CRDTs。

  • Leaderless replication(无主复制,Dynamo 风格):任何副本都可以接受写入。使用 quorum reads and writes(法定人数读写):如果有 n 个副本,要求 w 次写入和 r 次读取,其中 w + r > n 以确保重叠。即使有法定人数,边界情况仍然很多:sloppy quorums(宽松法定人数)、hinted handoff(提示交接)、并发写入需要 version vectors(版本向量)。

Partitioning(分区,即 Sharding 分片)

Section titled “Partitioning(分区,即 Sharding 分片)”

当数据大到单机放不下时:

  • 按键范围分区:每个分区拥有一个连续的键范围。支持高效范围查询。如果访问模式不均匀则有热点风险。例如:按时间戳分区会将所有写入集中到当前时间范围的分区。
  • 按键哈希分区:哈希函数将键均匀分布到各分区。破坏了排序(无法在哈希上做高效范围查询)但很好地分散负载。Consistent hashing(一致性哈希)是常用技术。
  • 二级索引与分区:document-partitioned indexes(文档分区索引,即本地索引——每个分区维护自己的二级索引)vs. term-partitioned indexes(术语分区索引,即全局索引——索引本身被分区,支持高效查询但需要分布式写入)。
  • Rebalancing(再平衡):添加/删除节点时,数据必须重新分布。策略:固定分区数(预分割)、动态分区、按节点比例分区。
  • Request routing(请求路由):客户端如何知道哪个节点持有给定分区?方法:任何节点都可以路由(gossip protocol 八卦协议,如 Cassandra)、独立的路由层(如基于 ZooKeeper 的,如 Kafka、HBase)、或客户端感知。
  • ACID:Atomicity(原子性——全有或全无,即”可中止性”保证)、Consistency(一致性——维护应用不变量,实际上是应用的责任而非数据库的)、Isolation(隔离性——并发事务互不干扰)、Durability(持久性——已提交的数据不会丢失)。

  • Kleppmann 指出 “ACID”是不精确的营销术语——不同数据库对这些保证的实现程度差异很大。ACID 中的”C”尤其具有误导性,因为它是应用层面的概念。

  • 隔离级别

    • Read committed(读已提交):无脏读、无脏写。许多数据库的默认级别。通过行级锁实现写入隔离,通过返回旧的已提交值实现读取隔离。
    • Snapshot isolation(快照隔离,即可重复读):每个事务看到的是事务开始时数据库的一致快照。通过 MVCC(Multi-Version Concurrency Control,多版本并发控制) 实现——数据库保留每个对象的多个版本。
    • Serializability(可串行化):最强级别——结果等同于事务串行执行。三种实现方式:
      1. Actual serial execution(真正串行执行):在单线程上逐个运行事务。由于 RAM 变得足够便宜可以将活跃数据集保存在内存中而变得可行。被 VoltDB、Redis 使用。需要短事务(存储过程)。
      2. Two-phase locking (2PL,两阶段锁):读者阻塞写者,写者阻塞读者。读操作使用共享锁,写操作使用排他锁。谓词锁或索引范围锁防止幻读。经典方案但在争用下性能差。
      3. Serializable Snapshot Isolation (SSI,可串行化快照隔离):建立在快照隔离之上的乐观方法。事务在不阻塞的情况下进行,但在提交时数据库检查冲突(检测因并发写入导致的过期数据读取)。如果检测到冲突,事务被中止并重试。被 PostgreSQL(9.1起)和 FoundationDB 使用。
  • Write skew and phantoms(写偏斜与幻读):快照隔离无法防止的微妙并发异常。例如:两个医生都检查至少有一个医生在值班,然后各自决定下班,导致没有医生在值班。模式是:读一个条件,做一个决定,写一些改变该条件的东西。幻读:一个事务中的写改变了另一个事务中搜索查询的结果。

  • CAP theorem(CAP 定理):在网络分区存在时,你必须在一致性和可用性之间做选择。Kleppmann 认为 CAP 经常被误解,其实际用处不如人们认为的那么大——实际的权衡更加细微。
  • Linearizability(线性一致性):最强的一致性模型——操作看起来在调用和响应之间的某个时刻原子地生效。使分布式系统表现得好像只有一份数据副本。用于:leader 选举、唯一性约束、跨通道排序。
  • Causality and causal consistency(因果性与因果一致性):比线性一致性弱但仍能捕获重要的排序关系。因果相关的事件必须以正确的顺序被看到;并发事件可以以任何顺序被看到。Lamport timestamps(Lamport 时间戳) 提供与因果一致的全序,但无法区分并发与因果排序的事件。Vector clocks(向量时钟) 可以。
  • Consensus(共识):让多个节点就某事达成一致。等价问题:leader 选举、原子提交、全序广播。FLP impossibility result(FLP 不可能结果):在一个即使只有一个节点可能崩溃的异步系统中,不存在保证达成共识的确定性算法。实践中,共识算法使用超时来打破僵局。
    • Paxos:基础性共识算法,但出了名地难以理解和实现。
    • Raft:为可理解性而设计的共识算法。与 Paxos 等价但更容易正确实现。
    • ZooKeeper:一个提供共识、leader 选举、分布式锁和配置管理的协调服务。不是通用数据库,而是分布式系统的构建块。
  • Unix 哲学:Kleppmann 将 Unix 管道与批处理做了详细类比。Unix 工具(sort、uniq、awk)通过 stdin/stdout 组合,使用字节流的统一接口。批处理系统共享这种可组合、单一用途转换的哲学。
  • MapReduce:由 Google 推广的编程模型。Map:接受输入记录并发出键值对。Reduce:接受一个键的所有值并合并它们。Hadoop 是最知名的开源实现。MapReduce 作业从 HDFS(Hadoop Distributed File System)读写。
    • 优势:处理任意量级的数据、容错(重试失败的任务)、易于并行化。
    • 劣势:高延迟(不适合实时)、多步骤工作流笨拙(必须串联多个 MapReduce 作业)、将中间状态物化到 HDFS 是浪费的。
  • 超越 MapReduce:数据流引擎如 SparkFlinkTez 泛化了 MapReduce 模型。它们将计算建模为算子的 DAG(有向无环图),可以将中间状态保存在内存中,并在整个管道上进行优化。对于多步骤计算,这些引擎的性能显著优于 MapReduce。
  • 批处理中的 Join 算法:sort-merge join、broadcast hash join、partitioned hash join。
  • 输出问题:批处理作业应该产出什么?从批处理作业直接写入生产数据库有风险(部分失败导致不一致状态)。更好的做法是构建不可变输出(新文件、新数据库)并原子切换。
  • 事件流:一个无界的、增量处理的数据集。事件由生产者生成,由消费者消费。消息代理在中间调解。
    • 基于日志的消息代理(Kafka、Amazon Kinesis):消息存储在持久、有序、分区的日志中。消费者从日志中的一个位置(offset)开始读取。消息在消费后不被删除——在保留期内持续存在。这支持重放和多消费者。
    • 传统消息代理(RabbitMQ、ActiveMQ):消息在确认后被删除。不支持重放。更适合任务队列,每条消息应由一个消费者恰好处理一次。
  • 数据库与流:数据库写入就是一个事件。Change data capture (CDC,变更数据捕获) 将数据库变为事件流。数据库的 write-ahead log 成为其他系统可以消费的流。这使得派生数据系统(缓存、搜索索引、数据仓库)可以保持同步。
  • Event sourcing(事件溯源):不存储当前状态,而是存储导致当前状态的每个事件。当前状态通过重放事件来派生。事件是不可变的事实。这提供了完整的审计日志和重建过去状态的能力。[解读] 这与 DDD 中领域事件和聚合的概念高度一致。
  • 流处理应用:event-time vs. processing-time(事件可能无序或迟到;窗口化(windowing)策略处理这种情况——tumbling 滚动窗口、hopping 跳跃窗口、sliding 滑动窗口、session 会话窗口)。流 Join:stream-stream(在时间窗口内连接两个事件流)、stream-table(用数据库查找丰富事件)、table-table(物化视图维护)。
  • 流处理中的容错:microbatching(微批处理,Spark Streaming)、checkpointing(检查点,Flink)、事务、幂等写入。

数据系统的未来(Kleppmann 的综合)

Section titled “数据系统的未来(Kleppmann 的综合)”
  • 派生数据与数据集成问题:大多数应用使用多个数据存储和处理系统(数据库、缓存、搜索索引、分析系统)。保持它们同步是最困难的问题之一。Kleppmann 主张将一个系统作为 system of record(记录系统,即事实来源),通过可靠的管道(CDC、事件流)从中派生所有其他表示。
  • 解绑数据库:传统数据库将存储、索引、查询处理、复制和事务捆绑在一个系统中。Kleppmann 建议我们可以”解绑”这些关注点——为每个使用专门工具,并通过流和批处理作业组合它们。
  • Lambda architecture vs. Kappa architecture:Lambda 架构维护并行的批处理和流处理管道。Kappa 架构通过仅使用流处理(需要时从日志重新处理)来简化。Kleppmann 因其简单性而倾向于 Kappa 方法。
  • 伦理:最后一章讨论了数据密集型应用的伦理影响——监控、算法偏见、数据所有权、隐私。Kleppmann 认为工程师有道德责任考虑他们的系统将如何被使用。
  • Twitter 的 fan-out 问题:负载参数如何驱动架构决策的典型案例。
  • 值班医生场景:展示 write skew(写偏斜),一种大多数隔离级别无法防止的微妙并发缺陷。
  • “happens-before”关系:在分布式系统中推理因果关系的基础概念。
  • Unix 管道作为批处理的隐喻:Kleppmann 做了详细的类比,展示了 Doug McIlroy 在1964年”花园水管”组合的愿景如何成为现代数据处理的蓝图。
  • 基于 ACID 合规的营销宣称选择数据库,而不理解它实际提供的隔离级别。
  • 假设”NoSQL”意味着没有事务,或分布式系统不能有事务。这些是设计选择,不是物理定律。
  • 使用 last-write-wins (LWW) 冲突解决而不理解它会悄悄丢弃写入。
  • 忽视复制延迟——假设从 follower 的读取总是与最近的写入一致。
  • 为不需要的可扩展性做设计——在架构层面的过早优化。
  • 将批处理和流处理视为根本不同的东西,而它们实际上是同一频谱上的点。

5. Fundamentals of Software Architecture: An Engineering Approach — Mark Richards & Neal Ford

Section titled “5. Fundamentals of Software Architecture: An Engineering Approach — Mark Richards & Neal Ford”

软件架构是一个比历史上被认为的更广泛的学科——它不仅包括技术结构,还包括架构特征(“-ilities”)、架构决策、设计原则和团队动态。核心信息是:架构应被视为一个工程学科,具有客观度量、适应度函数和有原则的权衡,而非仅凭直觉指导的艺术。软件架构中的一切都是权衡——软件架构第一定律:“软件架构中的一切都是权衡。“第二定律:“为什么比怎么做更重要。“

架构由四个维度组成:

  1. 架构特征(“-ilities”):与功能不直接相关的系统成功标准(性能、可扩展性、可用性、安全性等)。
  2. 架构决策:系统应如何构建的规则。例如:“只有服务层可以访问数据库。“决策即约束。
  3. 设计原则:系统应如何构建的指南(非硬性规则)。例如:“服务间优先使用异步消息以提升性能。”
  4. 结构:所使用的架构风格类型(微服务、分层、事件驱动等)。
  • 一个架构特征满足三个标准:(1) 它指定了非领域的设计考量,(2) 它影响设计的某些结构方面,(3) 它对应用成功至关重要或很重要。
  • 显式特征:在需求中明确说明(如”系统必须处理1000个并发用户”)。
  • 隐式特征:未说明但必要(如可用性——没人会说”我希望系统可用”,但每个人都假设它可用)。
  • 作者提供了广泛的分类,包括:可用性、可靠性、可扩展性、性能、安全性、可部署性、可测试性、敏捷性、容错性、弹性、可恢复性等等。
  • 运维特征:可用性、连续性、性能、可恢复性、可靠性/安全性、健壮性、可扩展性。
  • 结构特征:可配置性、可扩展性、可安装性、可重用性、本地化、可维护性、可移植性、可升级性。
  • 横切特征:可访问性、可归档性、认证、授权、法律合规、隐私、安全、可支持性、易用性。
  • 不要试图同时实现所有特征——关键的权衡技能是为给定系统识别真正重要的 3-7个特征。“最不糟糕”的架构是在最重要特征上做出最佳权衡的架构。
  • 组件是模块的物理封装。架构师以组件为单位思考,而非单个类。
  • 自顶向下分解:从系统的顶层分区开始。两种主要方法:
    • 技术分区(分层):按技术功能组织(表示、业务、数据)。优点:技术关注点清晰分离,符合许多开发人员的思维方式。缺点:功能变更需要修改多个层,领域内聚度低。
    • 领域分区:按业务领域组织(客户、订单、支付)。优点:变更局限在一个组件内,与 DDD 限界上下文对齐,更适合独立部署单元。缺点:跨领域有一定代码重复。
  • 作者强烈倾向于现代系统(尤其是微服务)使用领域分区
  • 组件识别流程:从需求中识别初始组件、将用户故事/用例分配给组件、分析角色和职责、分析架构特征、重构组件。这是迭代的。
  • 组件耦合度量:传入耦合(afferent)、传出耦合(efferent)。不稳定性 = 传出 / (传入 + 传出)。抽象度 = 抽象元素 / 总元素。主序列是在抽象度 vs. 不稳定性图中从 (0,1) 到 (1,0) 的线。靠近”无用区”(高抽象度、低不稳定性)或”痛苦区”(低抽象度、高不稳定性)的组件有问题。

本书系统地涵盖了架构风格,按单体或分布式组织:

单体风格:

  • Layered Architecture(分层架构):最常见的默认架构。层通常包括表示层、业务层、持久层和数据库。技术分区。层可以是”开放的”(允许跳过)或”封闭的”(所有请求必须通过)。Sinkhole anti-pattern(天坑反模式):请求穿过多层而每层未增加有意义的处理。如果超过20%的请求是天坑,该架构可能不合适。
  • Pipeline Architecture(管道架构):管道与过滤器。每个过滤器是一个自包含的处理步骤,通过管道连接到其他过滤器。过滤器可以是:producer(源)、transformer(映射)、tester(过滤/归约)、consumer(接收器)。对数据处理工作流简单而强大。
  • Microkernel Architecture(微内核架构):(也称为插件架构)。一个核心系统加上插件组件。核心包含最小功能;插件扩展它。例如:VS Code、Eclipse、保险理赔处理引擎。核心本身可以是单体,也可以使用分层或管道模式。

分布式风格:

  • Service-Based Architecture(基于服务的架构):单体和微服务之间的混合体。通常包含 4-12 个粗粒度、独立部署的服务,共享单一数据库。比微服务简单——无编排、无容器管理。对于向分布式架构迁移的团队来说是务实的起点。
  • Event-Driven Architecture(事件驱动架构):围绕事件的产生、检测和反应而构建。两种拓扑:
    • Broker topology(代理拓扑):事件发布到中央代理(通道),感兴趣的处理器订阅。无中央协调者。高度解耦但难以协调、错误处理和监控。
    • Mediator topology(中介拓扑):中央中介接收事件并通过向事件处理器发送命令来编排工作流。更好的控制和错误处理,但引入了与中介的耦合。
    • Request-reply(请求-回复)模式:当事件驱动系统中需要同步响应时,使用 correlation ID 和 reply queue。
  • Space-Based Architecture(基于空间的架构):通过消除数据库瓶颈来实现极端可扩展性。使用跨多个处理单元复制的内存数据网格。名称来自”tuple space”(Linda/JavaSpaces)。组件:processing unit(应用逻辑 + 内存数据)、virtualized middleware(消息网格、数据网格、处理网格、部署管理器)、data pump(异步写入数据库)、data writer、data reader。最适合负载峰值不可预测且极高的应用(如演唱会票务)。
  • Orchestration-Driven Service-Oriented Architecture(编排驱动的面向服务架构):带有 ESB(企业服务总线)、WSDL、SOAP 和重量级治理的经典 SOA。本书主要将其作为警示来描述——一种承诺复用但交付了复杂性的架构风格。ESB 变成了瓶颈和耦合点。“复用是神话”论点:使服务真正可复用的开销往往超过收益。
  • Microservices Architecture(微服务架构):每个服务建模一个限界上下文(来自 DDD),拥有自己的数据,并可独立部署。关键特征:限界上下文(领域分区)、每服务独立数据、用于外部访问的 API 层、运维自动化(CI/CD、容器、监控)。挑战:通信(同步 REST/gRPC vs. 异步消息)、无分布式事务下的数据完整性、服务发现、监控和调试。
    • Sidecar pattern and service mesh(边车模式与服务网格):为每个服务附加一个 sidecar 来处理横切关注点(日志、监控、安全)。Service mesh(服务网格)(Istio、Linkerd)管理所有 sidecar。
    • 粒度解构/集成驱动因素:何时拆分服务(服务范围/功能、代码易变性、可扩展性、容错性)以及何时合并服务(数据库事务、工作流/编排、共享代码、数据关系)。
  • 架构决策是在满足架构重要需求的备选方案中做出的有理由的选择。
  • Architecture Decision Records (ADRs,架构决策记录):一种轻量级的决策文档格式。每个 ADR 包含:标题、状态、上下文、决策、后果。本书主张 ADR 是必要的文档。
  • 决策中的反模式:
    • Covering Your Assets(自我保护):害怕做决策,架构师拖延或拒绝承诺。克服方法是等到”最后负责时刻”——当你有足够信息或延迟会造成更大伤害时做决定。
    • Groundhog Day(土拨鼠日):同一个决策不断被重新讨论,因为利益相关者不理解理由。克服方法是记录决策及其理由(ADR)。
    • Email-Driven Architecture(邮件驱动架构):重要决策埋没在邮件线程中,无人能找到。克服方法是使用 ADR 和共享决策日志。

借鉴自进化计算:

  • 架构适应度函数是提供某个架构特征的客观完整性评估的任何机制。它们是验证架构是否随时间得到维护的自动化检查。
  • 示例:验证层间无循环依赖的自动化测试、响应时间超过阈值时报警的监控检查、验证导入规则的依赖分析工具(如表示层不导入数据层)、CI/CD 管道中的代码度量门控。
  • 整体适应度函数评估特征的组合(如负载下的安全性 AND 性能)。
  • 适应度函数旨在使架构可演进——架构可以变化,而适应度函数防止退化。
  • 技术广度 vs. 技术深度:开发人员需要深度(少数技术的专业知识),架构师需要广度(了解众多技术及其适用场景)。知识金字塔:你知道的、你知道你不知道的、你不知道你不知道的。架构师必须主动扩展第二类。
  • 分析权衡:每个架构决策都有权衡。架构师的工作不是找到完美解决方案,而是找到”最不糟糕”的权衡组合。可以应用 ATAM 风格的分析(来自 Bass/Clements/Kazman)。
  • 理解业务领域:不理解业务领域的架构师无法做出好的架构选择。顶级架构特征来自业务而非技术偏好。
  • 架构拓扑:多少架构师、他们的角色,以及他们与团队的关系。选项包括:整个系统一个架构师、架构团队、嵌入开发团队的架构师。
  • 控制频谱:从完全控制(架构师做所有技术决策)到完全委托(团队决定一切)。两个极端都不行;有效的架构师为其组织找到合适的点。
  • Conway’s Law(康威定律,再议):作者讨论了”反向康威策略”(Inverse Conway Maneuver)——有意地构建团队以产生期望的架构(例如,如果你想要微服务,就创建小型跨功能团队,每个团队拥有一个服务)。
  • 展示架构决策:本书专门章节讨论沟通技术——如何向开发人员 vs. 高管展示,如何有效地画图,如何促进架构研讨会。
  • 谈判与领导:无法谈判的架构师,其决策将不被遵循。本书讨论了建立共识、处理分歧和无正式权力情况下领导的技巧。
  • “架构是难以改变的东西”:归于 Martin Fowler。作者将此作为指导原则——如果容易改变,它是设计决策;如果难以改变,它是架构决策。
  • Frozen caveman anti-pattern(冰冻穴居人反模式):一个曾被过去的失败烧伤的架构师,现在不加区分地应用那个教训,无论上下文如何。(“我1997年参与的项目因为 X 失败了,所以我们永远不能做 X。”)
  • Goldilocks Governance(金发女孩治理)模型:不要太多控制,不要太少——刚刚好。
  • “架构师应该写代码” ——但不要在关键路径上。“代码审查”和”概念验证”模式让架构师保持动手实践而不成为瓶颈。
  • Architecture katas(架构卡塔):引导式练习,团队在给定的需求和约束下练习设计架构,然后展示和评审。基于 Ted Neward 的想法。
  • Architecture Decision Records (ADRs):本书提供了详细的模板和示例。
  • 适应度函数工具:ArchUnit(Java)、NetArchTest(.NET)、CI 中的依赖分析、自定义监控脚本。
  • 画图:推荐 C4 model(由 Simon Brown 提出),用于一致的、层次化的系统图:Context(上下文)、Containers(容器)、Components(组件)、Code(代码)。
  • 试图同时实现太多架构特征——导致过度工程化、无法构建的系统。
  • 因为流行而选择架构风格,而非因为它适合问题的特征。
  • “象牙塔架构师”反模式——在不了解开发团队日常现实的情况下做决策。
  • 在分布式架构中过度依赖同步通信,导致紧耦合和级联失败。
  • 忽视康威定律——设计一个与组织沟通结构不匹配(或不有意对抗)的架构。
  • 不记录架构决策,导致”我们当初为什么这么做?“综合征。
  • 在可逆替代方案存在时过早做出不可逆决策。

这五本书中有几个反复出现的主题:

  1. 权衡是架构的本质。 每本书都强调没有完美的架构——只有权衡。Bass 等人通过 ATAM 将其形式化,Richards/Ford 称之为第一定律,Fowler 警告不要过早分布,Kleppmann 绘制了一致性/可用性权衡的全景图,Evans 坚持建模选择始终意味着放弃某些东西。

  2. 复杂性管理。 Evans 处理领域复杂性,Kleppmann 处理数据复杂性,Fowler 处理企业集成复杂性,Bass 等人处理质量属性复杂性,Richards/Ford 处理结构复杂性。所有人都同意:简单性是美德,偶然复杂性是敌人,良好的抽象是主要武器。

  3. 领域的首要性。 Evans 将此作为其全部论点;Fowler 通过领域逻辑模式展示它;Richards/Ford 倡导领域分区优于技术分区;Kleppmann 展示数据模型必须匹配领域;Bass 等人将架构建立在利益相关者驱动的质量属性之上,而这些源自业务领域。

  4. Conway’s Law(康威定律)。 在 Bass 等人和 Richards/Ford 中被显式讨论,在 Evans(限界上下文映射到团队)和 Kleppmann(数据系统边界映射到组织边界)中隐含存在。

  5. 文档与沟通。 Bass 等人用大量篇幅讨论 Views and Beyond;Richards/Ford 提倡 ADR 和 C4 图;Evans 坚持通用语言;Fowler 提供了共享的模式词汇表;Kleppmann 为分布式系统建立了共享的概念词汇表。