fedorthinks

速成课 · No. 03

架构几乎从来不是凭空发明的。你要做的是识别:哪一种已知的形状契合眼前的问题——以及这个形状要收取什么代价。

17 个模式 · 每个一张图 · 末尾附速查表

§ 01

模式是对一个在成千上万个系统里反复出现的问题的、经过验证的答案。每个模式都有名字——而且几乎总是英文名,人们在文档里搜的、面试里聊的,都是这个名字。

模式是一个答案的名字,不是一条规则

下棋时你不会每盘都重新发明开局。你认出"这是西西里防御"——于是已经知道它大致会通往哪里。

架构模式(architectural pattern) = 反复出现的问题 + 经过验证的解决形状 + 已知的代价。当你说"服务需要对别处发生的事情做出反应",这已经是一个被解决、被命名的问题——Event-driven architecture 模式。懂这套目录,意味着你不再重新发明,而是开始做选择。

架构模式 ≠ 设计模式

设计模式是你怎么摆放一个房间里的家具。架构模式是整栋房子的平面图。

设计模式(design patterns)——Factory、Observer、Strategy——活在一个模块的代码内部。架构模式决定系统里有哪些大"盒子"、它们之间有哪些连线:什么被单独部署、谁和谁对话、数据住在哪里。这门课讲的是平面图,不是家具。

地图:六组 17 个模式

地铁线路图:先看清整条线路,再一站一站地坐。

这门课的所有模式分成六组。每组回答一个问题:怎么组织单个应用、依赖指向哪里、怎么切分系统、各部分怎么通信、怎么在数据里保存真相,以及罕见问题需要哪些形状。

One app insideLayeredModular monolithMVC / MVVMDependency directionHexagonalClean / OnionSplitting apartMicroservicesServerlessCommunicationRequest–responseEvent-drivenQueues / pub-subPipe-and-filterDataCQRSEvent sourcingSagaSpecial shapesMicrokernelSpace-basedPeer-to-peer
六组模式——第 2–7 章依次讲解。

怎么读每个模式

护照:每本的栏目都一样,所以很容易比较。

接下来每个模式都是同样的结构:英文名(记住它——这就是术语)、一个生活中的画面、一张图,以及一张卡片,包含这些栏目:问题(哪里痛)、思路(怎么解决)、例子、代价(你要付出什么)、何时使用,以及和其他模式的关联。最重要的规则:不存在最好的模式,只有合适的模式,而每个模式都有一张账单,要在签字之前读明白。

你不该挑那个出名的模式。你该挑那个你付得起代价的模式。

§ 02

大多数系统起步时都是单个可部署的应用。最初三个模式讲的是怎么安排它的内部,让它不至于变成一团烂泥。

Layered Architecture —— 分层架构

一家餐厅:大堂接单,厨房做菜,储藏室存货。服务员不下厨,厨师不跑地窖——每个人只和下面那一层打交道。

最常见的模式。代码被切成一层层横向的层(layers),请求从上往下穿过它们。每一层只知道它下面那一层。

PresentationController · HTTP · UIBusiness logicServiceData accessRepositoryDatabasePostgresrequestgoes down
请求逐层往下走:controller → service → repository → 数据库。
PatternLayered Architecture· 分层架构, n-tier
问题
所有代码堆成一团:改界面会弄坏数据库访问。
思路
把代码按楼层摆放:表现层、业务逻辑、数据访问、数据库。
例子

Web 应用:Controller 调 Service,Service 调 Repository,Repository 打到 Postgres。

代价
所有东西作为一个整体部署;一个小改动 = 重新部署整个应用。
何时使用
几乎总是可以作为单个应用的起步形状。
关联
是 Modular Monolith 的基础;层内部常常会用到 Hexagonal。

Modular Monolith —— 模块化单体

一栋公寓楼:一个屋顶、一个大门,但每套公寓有自己的墙和门。没人穿过邻居的卧室去走楼梯。

仍然是一个应用,但内部按业务含义切成一个个模块(modules)——计费、商品目录、账户。模块之间只通过公开接口对话,不碰别人的表。

ONE deployable appBillingmoduleown tablesCatalogmoduleown tablesAccountsmoduleown tablespublic interface only✗ a module never reaches into another module's tables
一个应用,内部有结实的墙:模块之间只通过公开接口对话。
PatternModular Monolith· 模块化单体
问题
单体变成"一大团泥巴":什么都依赖什么。
思路
按业务模块切分,接口清晰,但作为一个整体部署。
例子
catalog 模块只调用 billing 的公开函数,而不是它的表。
代价
需要守住边界的纪律(如果不用工具检查,边界很容易被打破)。
何时使用
几乎对所有人都是正确的默认选项——有微服务的清晰,没有网络的麻烦。
关联
自然地长成 Microservices:需要时把某个模块抽出去变成服务。

MVC / MVVM —— 让屏幕离脑子远一点

一座剧场:剧本、演员和导演、舞台和灯光,是不同的活儿。换布景不用重写整出戏。

这些模式在界面和数据与规则之间立起一道墙,这样改变某样东西长什么样,不会弄坏它做什么。

Controllerhandles inputViewwhat you seeModeldata + rulesupdatesstate shown inuser action
MVC:用户 → Controller → Model(数据和规则)→ View(看到的东西)。
PatternMVC / MVVM· Model–View–Controller / Model–View–ViewModel
问题
逻辑散落在各个界面里;改设计 = 重写规则。
思路

Model 是数据和规则,View 是显示,Controller(或 ViewModel)是两者之间的黏合剂。

例子

Django 和 Rails 基于 MVC;移动应用和富 web 应用基于 MVVM,由 ViewModel 持有屏幕状态。

代价
界面一大,"黏合剂"就会膨胀;要留意别让逻辑漏进 View。
何时使用
任何有界面的应用。
关联
这就是用在 UI 上的 Layered 分层。

单体不等于一团乱。没有内部隔墙的单体,才是。

§ 03

两个模式,同一个想法:业务规则在中心,对 framework、数据库和 web 一无所知。所有技术性的东西都在外面,接到核心上。

Hexagonal Architecture —— 端口与适配器

一台游戏机有标准接口。游戏不在乎你插的是手柄、方向盘还是街机摇杆——它只读"输入"。

装着业务逻辑的核心声明一组端口(ports)——像"保存订单"或"扣款"这样的接口。外面的**适配器(adapters)**针对具体技术实现这些端口。Driving adapters 调用核心(HTTP API、CLI、测试),driven adapters 被核心调用(数据库、支付、邮件)。

Domainbusiness rulesHTTP APIdriving adapterCLI / testsdriving adapterPostgresdriven adapterStripedriven adapterIn-memorytest adapterthe core defines ports; adapters plug into them
核心声明端口;适配器从外面接进来——换掉它们,核心不会察觉。
PatternHexagonal Architecture· Ports and Adapters, 六边形架构
问题
业务逻辑长进了 framework 和数据库里——没法测试,也没法改。
思路
核心只和端口(接口)对话;技术住在适配器里。
例子
端口 OrderRepository:生产环境用 Postgres 适配器,测试里用一个内存列表。
代价
更多接口和文件;对一个小小的 CRUD 来说是多余的。
何时使用
有真正的业务规则,而且它们要比所用的技术活得更久。
关联
Clean / Onion 是同一个想法,只是画成了圆圈。

Clean / Onion Architecture —— 依赖一律指向内

一颗洋葱:心在正中,外面裹着一层层。剥掉外皮不会伤到心——而心并不依赖外皮。

同样的原则,画成一圈圈同心圆。中心是实体(entities),外面一圈是用例(use cases),再外面是适配器,最外层是 framework 和数据库。核心规则是 dependency rule:每一支依赖箭头都指向中心。

Frameworks · DB · WebInterface adaptersUse casesEntitiesdependenciespoint inward
Clean / Onion:外圈依赖内圈,永远不会反过来。
PatternClean Architecture / Onion Architecture· 整洁架构 / 洋葱架构
问题
换 framework 或数据库,就得重写业务逻辑。
思路
Dependency rule:内圈不从外圈 import 任何东西。
例子

"金额超过 1 万的发票需要审批"这条规则,是一个普通的 class,不 import web framework 或 ORM。

代价
更多的层,以及层与层之间的数据映射。
何时使用
规则复杂、生命周期很长的产品。
关联
是 Hexagonal 的双胞胎;常常住在 Modular Monolith 的模块内部。

Framework 是细节。把它当墙纸看待,而不是地基。

§ 04

到了某种规模——负载的,或者团队数量的——一个应用就撑不住了。这些模式把系统切成独立的部分,作为交换,塞给你一张网络,连同它所有的故障。

Microservices —— 微服务

一座美食广场:每个摊位有自己的厨房、菜单和收银台。披萨摊忙起来就多加两个厨师;沙拉摊不需要。

系统被拆成一个个小服务(services):每个拥有一项业务能力和自己的数据库,单独部署、单独伸缩。服务之间通过网络对话——HTTP 或消息总线。

API gatewayOrdersown deployown DBInventoryown deployown DBDeliveryown deployown DBnetwork / message bus between services
每个服务有自己的部署和数据库;它们之间是网络。
PatternMicroservices· 微服务(前身是 SOA)
问题
很多团队在同一份代码里互相干扰;系统各部分的负载极不均匀。
思路
独立的服务,各自的数据库,各自部署。
例子
orders、inventory、delivery 三个服务;黑五那天只伸缩订单和支付。
代价
分布式系统:网络延迟、服务之间没有简单的事务、运维和调试都很复杂。
何时使用
大团队、不均匀的负载。对一个五个人的创业公司来说是杀鸡用牛刀。
关联

从 Modular Monolith 长出来;通过 Event-driven 和 Queues 通信;跨服务的操作需要 Saga。

Serverless —— 按需运行的函数

与其养一辆闲着的车,不如叫出租车,只在计价器跑着的时候付钱。

你写一些小函数(functions),云在事件发生时运行它们,自己从零伸缩到上千个副本再缩回去,按每次运行收费。

Eventfile uploadedFunctionruns on demandStoragethumbnail saved0 → 1000 copies, then back to 0you pay per run, not per idle server
事件 → 函数需要跑几次就跑几次 → 结果。按运行次数付费。
PatternServerless· FaaS, Functions as a Service
问题
服务器闲着也要花钱,而负载是一阵一阵来的。
思路
代码只在事件发生时运行;伸缩交给云。
例子
图片上传到存储 → 一个函数生成缩略图 → 两次上传之间什么都不运行。
代价
Cold start(首次运行的延迟)、运行时间限制、被云厂商绑定。
何时使用
断断续续、由事件触发的活儿:处理文件、webhook、定时任务。
关联
天然和 Event-driven 搭配。

分布式系统把函数调用变成网络调用。它们每一个都可能失败。

§ 05

一旦部分不止一个,架构就大半在它们之间的连线里了。它们怎么对话,决定了它们怎么出故障。

Request–Response —— 问完就等

在柜台点单:你问,你站着等,拿到咖啡才往前走。

客户端发出请求(request),然后等待响应(response)。这是同步通信:REST、gRPC、一次数据库查询。

ClientServerrequestresponseblocked,waitingworking
服务器干活时,客户端被阻塞,一直在等。
PatternRequest–Response· 请求-响应, 同步调用
问题
要继续往下走,现在就需要一个答案。
思路
调用 → 等结果 → 继续。
例子
结账时调用支付 API,等待确认。
代价
时间上的耦合:服务器慢了或宕了,调用方也跟着卡住。
何时使用
默认选项——当你真的需要立刻拿到答案时。
关联
和异步的 Event-driven、Queues 正好相反。

Event-driven Architecture —— 宣布它,让监听者各自反应

一间新闻编辑室:新闻一出,体育、天气、广告各自做出反应。播报员不等他们,甚至不知道谁在听。

组件不去调用别人,而是发布一个事件(event)——一个已经发生的事实:"OrderPlaced"。谁感兴趣,谁就订阅并做出反应。两种形式:broker(事件自由地传开)和 mediator(由一个协调者按顺序推进各步)。

Order servicepublishesEvent bus“OrderPlaced”Emailsends receiptInventoryreserves stockAnalyticscounts saledoesn't know or waitfor who listens
服务发布一个事件;邮件、库存和分析各自独立地做出反应。
PatternEvent-driven Architecture· 事件驱动架构
问题
一个服务不得不知道并调用所有关心"某事发生了"的服务。
思路
发布事实;订阅者自己、各自独立地反应。
例子
"OrderPlaced" → 给客户发邮件、在仓库预留、写入分析。
代价
流程难以追踪和测试;事件可能来两次,或者顺序错乱。
何时使用
一个动作引发很多互相独立的反应;副作用很慢。
关联

运行在 Queues / Pub-Sub 之上;是 Event Sourcing 和 Saga 的基础。

Message Queue 与 Pub/Sub —— 各部分之间的邮局

邮局替你存着信,直到收件人准备好。你把信扔进邮筒就走了;他醒来时再读。

**消息队列(message queue)**或 pub/sub broker 站在发送方和接收方之间,这样它们不必同时在线。在队列里,一条消息由一个 worker 取走;在 pub/sub 里,一条消息会发给所有订阅者。

Producerdrops a job, leavesqueue holds messagesWorker 1Worker 2take jobs when free
Producer 放下任务就走;workers 有空时再取任务。
PatternMessage Queue / Pub-Sub· 消息队列 / 发布-订阅
问题
慢活儿拖慢了给用户的响应;负载高峰压垮系统。
思路
把任务放进队列,稍后按自己的节奏处理。
例子

一份很重的报表被放进队列(RabbitMQ、Kafka、SQS),worker 在后台生成它,网站保持飞快。

代价
结果变成延后的;要考虑重试和重复消息。
何时使用
后台任务、削平高峰、服务之间的通信。
关联
是 Event-driven 和 Microservices 的传输层。

Pipe-and-Filter —— 给数据装一条流水线

一条灌装线:洗瓶、灌装、封盖、贴标。每个工位只做一件事,然后把瓶子传下去。

数据流过一串独立的步骤——过滤器(filters),由**管道(pipes)**连接起来。每个 filter 只做一种转换。

data flows →InputTokenizefilterParsefilterOptimizefilterOutputeach step does one thing and passes the result on (like grep | sort | uniq)
数据依次穿过各个 filter,就像 Unix 管道。
PatternPipe-and-Filter· 管道-过滤器, pipeline
问题
复杂的数据处理变成了一个巨大的函数。
思路
拆成简单的步骤,每一步有输入、有输出。
例子

grep | sort | uniq、一个 ETL 作业、一个编译器(分词 → 解析 → 优化 → 生成)。

代价
当各步需要回头、或者互相商量时,就不太合适。
何时使用
工作本身是一串清晰的转换。
关联
各步之间常常用 Queues 连接。

同步是打电话。异步是写信。

§ 06

这些模式不是关于盒子的,而是关于你怎么写、怎么读、怎么保存那份真相。

CQRS —— 写模型和读模型分开

图书馆有一份用来找书的快速目录,还有一间单独的后屋,把书归档上架。查找为一件事优化,存放为另一件事优化。

CQRS(Command Query Responsibility Segregation)把写模型——改变数据的命令(commands)——和读模型——取出数据的查询(queries)——分开。每一边都可以单独优化、单独伸缩。

Commandschange dataWrite modelrules, validationWrite DBnormalizedQueriesread dataRead modelfast viewsRead DBdenormalizedsync
命令进入写模型,查询走单独的读模型;两者之间做同步。
PatternCQRS· Command Query Responsibility Segregation
问题
一个数据模型既不适合复杂的写入,也不适合快速的读取。
思路
两个模型:写模型带着所有规则,读模型是预先算好的"视图"。
例子
仪表盘从一个去规范化的视图里读,订单写进一个事务型数据库。
代价
两个模型要保持同步;读取可能稍有滞后(eventual consistency)。
何时使用
读比写多出好几倍,或者两者形状差别很大。不是默认选项。
关联
常常和 Event Sourcing 配对:日志里的事件用来构建读模型。

Event Sourcing —— 存历史,而不是存状态

银行不只存你的余额,而是存下每一笔收入和支出。余额只是它们的总和。丢了余额,按账本重算就行。

不去覆盖当前状态,而是存一条事件流(event log),只往末尾追加。当前状态通过**重放(replay)**事件得到。

event log (append-only)AccountOpenedDeposited +100Withdrew −30Deposited +5replayBalance = 75current state is derived
事件日志 → 重放 → 当前状态。
PatternEvent Sourcing· 事件溯源
问题
不仅要知道"现在是什么",还要知道"是怎么走到这一步的"。
思路
事件日志是真相来源;状态从它计算出来。
例子

账户:"开户、+100、−30、+5",而不是"余额:75"——完整的审计,还能回答"上周二是什么样?"。

代价
"现在是什么"的查询更难了;事件不能修改——历史是永久的。
何时使用
金融、审计、复杂的业务流程。
关联
几乎总是和 CQRS 一起用;天然和 Event-driven 搭配。

Saga —— 一个可以倒回去的长操作

订一趟旅行:机票、酒店、租车。租车没订成——你就取消酒店和机票。没有一个"全部撤销"的按钮,只能一步一步倒回去。

当一个业务动作要经过好几个各有自己数据库的服务时,没法做成一个共同的事务。Saga 是一串本地事务;某一步失败时,就运行补偿动作(compensations),撤销前面的步骤。

1. Create order2. Reserve stock3. Charge card ✗Release stockcompensationCancel ordercompensationfailure → undo the earlier steps, in reverse
第 3 步失败 → 补偿动作按相反顺序撤销第 2 步和第 1 步。
PatternSaga· saga, 带补偿的分布式事务
问题
一个操作涉及好几个服务,却没有共同的事务。
思路
按顺序执行各步,每一步都配一个出错时用的"撤销"步。
例子
创建订单 → 预留商品 → 扣卡;扣卡失败——释放商品,取消订单。
代价
每一步都需要补偿;各步之间数据暂时不一致。
何时使用
横跨 Microservices 的业务操作。
关联
需要 Microservices;各步常常通过 Event-driven 来协调。

一个事务是一颗意图的原子。当它横跨多个服务时,你得自己把这颗原子造出来。

§ 07

这些模式是为某一种具体的痛而存在的。当你正好有这种痛时再去拿它们——一秒也别提前。

Microkernel —— 小核心加插件

一把电钻:一个马达,十几个钻头。钻还是那把钻——活儿需要什么就换什么钻头。

一个极简的**核心(core)提供基础,其余一切都以插件(plugins)**的形式加入,插件接到核心上,彼此互不依赖。

CoreminimalGitPythonThemesLinterDockerCopilot
核心加可插拔的插件——就像代码编辑器和它的扩展。
PatternMicrokernel· Plugin Architecture, 插件架构
问题
功能很多,经常变,而且不同客户要的不一样。
思路
稳定的小核心 + 互相独立的插件。
例子
带扩展的 VS Code 和浏览器;一个保险系统,地区规则就是一个插件。
代价
要仔细设计插件 API 和它们的兼容性。
何时使用
平台型产品,按客户定制。
关联
插件常常像 Hexagonal 那样通过端口接入。

Space-based Architecture —— 数据放内存,没有瓶颈

一座体育场有二十个售票窗口,每个都有自己的一份座位图——而不是只有一个窗口排着长队。

当唯一的数据库在巨大负载下成为瓶颈时,工作被分给许多 processing units,每个都把数据放在内存里。内存在它们之间同步(data grid),数据稍后再异步写进数据库。

Load balancerUnit 1data in memoryUnit 2data in memoryUnit 3data in memoryUnit 4data in memoryin-memory data grid, replicated between unitsDatabasewritten later, async
负载分散到内存里存着数据的节点上;数据库不再是瓶颈。
PatternSpace-based Architecture· 基于空间的架构(tuple space)
问题
一个数据库扛不住高峰时的并发负载。
思路
数据放在多个节点的内存里,节点之间同步,稍后再写数据库。
例子
限时秒杀,或者一场涌入大量同时出价的在线拍卖。
代价
非常复杂、非常贵;内存和数据库之间有丢数据的风险。
何时使用
只在真正巨大、忽高忽低的并发负载下。
关联
像 Queues 一样用到缓存和异步写入。

Peer-to-Peer —— 没有中心

一场百乐餐:每个人既是厨师又是客人。没有餐厅,没有服务员——这群人自己喂饱自己。

节点——peers——直接互相通信,没有中心服务器。每个节点同时是客户端和服务器。

peerpeerpeerpeerpeerpeerno central server —everyone is client and server
每个节点直接连着其他节点;没有中心服务器。
PatternPeer-to-Peer· P2P, 对等网络
问题
中心服务器是一个昂贵又脆弱的单点故障。
思路
参与者自己在彼此之间分担负载和数据。
例子
BitTorrent:每个下载的人同时也在上传——人越多,网络越强。
代价
很难保证一致性、安全和控制。
何时使用
文件分发、区块链、不依赖中心主机的韧性。
关联
和 Request–Response 这类客户端-服务器形状正好相反。

冷门模式是药,不是保健品。冲着确诊的病去吃它。

§ 08

模式是一套词汇,不是只能点一道菜的菜单。真实的系统会组合好几种——而最明智的一步,往往是最简单的那一步。

模式之间是有关联的

一栋房子同时用到承重墙、水管和电线——而不是"唯一正确的那种建筑模式"。

模式不会单独存在。模块化单体会长成微服务;微服务需要事件、队列和 saga;Hexagonal 和 Clean 是同一个想法;CQRS 和 Event Sourcing 成对出现。

Modular monolithMicroservicesgrows intoSaganeedsEvent-driventalks viaQueues / pub-subbuilt onHexagonalClean / Onionsame ideaCQRSEvent sourcingoften pairedevents feed
本课各模式之间的主要关联。

一个真实的系统,可能对外是一个模块化单体,每个模块里是 Clean Architecture,界面用 Request–Response,慢吞吞的副作用走事件,重活前面挡一个队列。问题不是"哪个模式",而是**"哪些模式,放在哪里"**。

从最简单的形状起步

你不会为了盖一间小木屋去浇一座体育场的地基。

几乎每个系统都该作为 Modular Monolith 起步,并挣得它的复杂度。Microservices、CQRS、Event Sourcing、Space-based——都是对规模和团队大小问题的回答,而这些问题你也许永远不会有。最便宜的分布式系统,是你没造的那个。

在选一个模式之前
  • 这个形状解决的是哪个反复出现的问题? 用一句话说出来。
  • 我现在有这个问题吗——还是在猜未来?
  • 它要花多少代价? 在延迟、运维、复杂度、金钱上。
  • 还能用的最简单的模式是哪个? 从它开始。
  • 我能不能以后再用它,等痛真的冒出来?
你过度设计了的迹象
  • 服务比工程师还多。
  • 一个用户动作要碰到五个服务和两个队列。
  • CQRS 或 Event Sourcing 比读取或审计问题来得还早。
  • 团队里没人能凭记忆把系统画出来。
  • 选这个模式,只因为大公司都这么做。

速查表:全部 17 个模式

组织单个应用

Layered分层, n-tier

代码按楼层摆放:表现 → 逻辑 → 数据 → 数据库。请求从上往下走。

Modular Monolith模块化单体

一个应用,切成接口清晰的模块。最好的默认选项。

MVC / MVVMModel–View–Controller

在界面和数据与规则之间立一道墙。

依赖的方向

HexagonalPorts and Adapters

核心声明端口,技术通过适配器接入。

Clean / Onion整洁 / 洋葱

同心圆,所有依赖都指向中心(dependency rule)。

拆成多个部分

Microservices微服务

独立的服务,各有数据库和部署。代价是分布式系统。

ServerlessFaaS

函数由事件触发,云负责伸缩,按运行付费。

各部分怎么对话

Request–Response同步调用

问完就等答案。简单,但在时间上耦合。

Event-driven事件驱动

发布一个事实,订阅者自己做出反应。

Message Queue / Pub-Sub消息队列

任务在队列里等着,直到有 worker 空出来。

Pipe-and-Filter管道-过滤器

数据穿过一串简单的步骤。

数据

CQRSCommand Query Responsibility Segregation

写和读用不同的模型。

Event Sourcing事件溯源

存事件日志;状态就是它的重放结果。

Sagasaga

跨多个服务的步骤 + 出错时的补偿。

特殊形状

Microkernel插件架构

小核心加互相独立的插件。

Space-based内存 data grid

数据放在多个节点的内存里,数据库不再是瓶颈。

Peer-to-PeerP2P

节点直接通信,没有中心。

最好的架构,是那个能撑过你真实需求的、最简单的架构——再多一个模式都不要。

速成课结束 · 8 章 · 17 个模式

接下来是实践。每一个模式都能展开成好几本书:Mark Richards 的 Software Architecture Patterns、Fundamentals of Software Architecture、Designing Data-Intensive Applications、Building Microservices。但在你采用任何一个模式之前——先说出你的问题。模式是答案;问题才是那个让它配得上的提问。