Informatics 43 — 第四周课程笔记
目录
- 从需求到结构 — 为什么需要架构
- 架构师(Architect)— 一个类比(以及它的局限)
- 定义软件架构
- 架构在实践中 — 案例分析
- 软件架构的组成要素
- 规范性架构 vs. 描述性架构
- 架构侵蚀(Architectural Erosion)
- 架构风格 — 定义
- 六种架构风格
- 混合风格
- 演化与架构
- 与基本原则的联系
- Quiz 4 复习要点
- 总结与记忆框架
1. 从需求到结构 — 为什么需要架构
1.1 需求与代码之间的鸿沟
在第二、三周之后,我们手上已经有了需求规约和一套用例(use cases)。自然而然要问的下一个问题是:接下来呢? 面对同一套需求,系统可以有许多种可能的结构方式 —— 不同的模块划分、不同的职责分配、不同的通信方式。架构就是有意识地做这些结构决策的学科。
整个过程是这样的:
架构处在中间 —— 它不是需求(需求说明系统要做什么),也不是代码(代码精确指定每一个细节怎么做)。它是联接两者的高层结构。
1.2 为什么架构在软件工程中如此重要
架构有五条具体的价值:
| 收益 | 含义 |
|---|---|
| Intellectual control(思维掌控) | 让庞大复杂的系统仍然能被人脑理解 |
| Conceptual integrity(概念完整性) | 让系统有一个连贯的"故事",而不是各种风格拼凑出来的补丁 |
| Effective project communication(有效的项目沟通) | 为团队提供共同的词汇和图示,用来推理系统 |
| Reusability(可复用性) | 结构良好的组件可以在多个系统或变体之间复用 |
| Maintainability(可维护性) | 方便管理一组变体系统,并支持长期演化 |
2. 架构师(Architect)— 一个类比(以及它的局限)
2.1 (建筑)架构师做什么
"软件架构"这个名字借自建筑学,这个类比确实有参考价值。一位建筑架构师:
- 在项目中扮演一个独特角色,与施工工人分工明确
- 有广泛的训练、丰富的经验、深入的领域理解
- 领导团队、善于沟通、是决策者
- 工作在比实际施工者更高的抽象层级上
- 通常担当与关键业务利益相关者/客户对接的接口
软件架构师在软件项目中扮演的是类似的角色。
2.2 类比失效的地方
这个类比可以作为入门直觉,但不宜推得太远 —— 软件和建筑有本质区别:
- 软件服务的用途比建筑广泛得多
- 我们对建筑的了解远远多于软件(几个世纪 vs. 几十年)
- 软件比物理材料更加可塑 —— 建筑建成后修改代价极高,软件修改相对便宜
- 软件是一台机器,而不是静态结构 —— 它有运行时行为
- 软件是不可见的 —— 你无法像看建筑那样看到、走进、或检查它
把这个类比当作直觉保留,但要记住:软件架构需要面对行为、变化与不可见性,而建筑架构并不需要。
3. 定义软件架构
3.1 主要定义
"principal"(主要的)这个词承担了很多分量。不是每一个设计决策都是架构级的 —— 只有那些足以塑造整个系统的决策才算。这些主要决策涵盖:
| 方面 | 处理的问题 |
|---|---|
| Structure(结构) | 系统如何划分成各个部分 |
| Behavior(行为) | 这些部分随时间如何表现 |
| Interaction(交互) | 各部分之间如何通信 |
| Non-functional properties(非功能属性) | 性能、可扩展性、安全性、可靠性等 |
3.2 架构即蓝图(Blueprint)
还有一个互补的定义:
软件架构是软件系统构建(construction)与演化(evolution)的蓝图。
"演化"和"构建"同样重要 —— 架构不仅关乎把系统造出来,更关乎让系统能够优雅地随时间变化。
3.3 其他有用的定义
几位著名作者给出了各自的表述,每一个都强调了不同的侧面:
| 来源 | 定义 | 强调点 |
|---|---|---|
| Clements, Bass, Kazman | "用于推理系统所需的一组结构,包括软件元素、元素之间的关系,以及二者的属性。" | 推理 / 分析 |
| Spinellis | "你的整个系统……将如何分解为进程或服务;数据如何存储、传输和处理;以及所有部分如何协同工作,以满足所需的功能、可靠性、容量、可扩展性、可维护性和可移植性。" | 分解与非功能属性 |
| Fowler | "系统的最高层级划分;那些难以改变的决策。" | 难以改变的部分 |
| Natarén | "清晰定义的多个高层组件,它们协同工作,构成你的系统并解决你的问题。" | 高层组件的协作 |
这四个定义从不同角度指向同一件事:架构 = 重要的、高层的、难以改变的、塑造系统的决策。
4. 架构在实践中 — 案例分析
4.1 案例:电子邮件(Email)
电子邮件的功能规约很短:
- 发送与接收消息
- 获取自己的邮件,而不是别人的
- 编址方案
- 存储消息
- 电子形式
- 屏蔽垃圾邮件(spam)
在架构层面,电子邮件通常被建模为 client ↔ client 的通信(背后由服务器中介)。架构捕捉的是高层交互模式,而不是某个邮件客户端的实现细节。
4.2 案例:万维网(WWW / REST)
Web 可以用多种方式来描述 —— 同一个架构可以有几种不同但都正确的图示和叙述。Web 架构(也称 REST 参考架构)的关键性质有:
- Web 是一组资源(resources)的集合,每个资源都有唯一的名字(URL)
- URL 可用于确定互联网上某台机器(origin server)的身份,资源可从那台机器获得
- 客户端(clients)(用户代理 / Web 浏览器)向服务器请求资源
- 客户端操作的是资源的表示(representations),而不是资源本身
- 用户代理和源服务器之间的所有通信都必须通过一个简单、通用的协议(HTTP)
- 所有通信必须是完全自包含的(stateless)
关于 Web 架构的三个重要观察:
- 没有哪一段代码单独"实现"了这个架构 —— 它是整个系统的性质,不是某个文件或程序的性质
- 这些风格约束在代码中看不出来 —— 你读任何一个 HTTP 服务器的源代码,都看不到"REST"这几个字
- Web 是世界上最成功的应用之一,而它只能从架构的视角才能被充分理解
这个例子很关键:它说明架构是一种凌驾于任何具体代码库之上的思考方式。图示、约束和协议规则本身就是架构。
4.3 案例:Facebook
Facebook 展示了大量功能与非功能需求如何共同驱动架构决策。
功能需求包括: 好友列表、状态更新、信息流(news feed)、评论/点赞/反应、消息、照片、位置服务、第三方应用,等等。
非功能需求包括:
- 效率 / 性能
- 可扩展性(Scalability)
- 可用性(Availability)
- 安全性(Security)
- 隐私(Privacy)
- 可靠性(Reliability)
- 可移植性(Portability)
架构必须同时满足这两类需求。一个能提供功能行为、但在可扩展性或可用性上失败的结构,对 Facebook 来说就不是一个可接受的架构。
4.4 案例:Client-Server(客户端-服务器)
最常见的分布式系统架构。它的定义性特征:
- 客户端向服务器发起请求
- 对数据、安全、访问进行集中式(centralized)控制
Client-server 之所以无处不在,正是因为集中化简化了许多关注点(特别是安全性和数据一致性)。第 9 节会把它作为六种风格之一详细讨论。
5. 软件架构的组成要素
软件架构由三类构件搭建而成:
| 要素 | 含义 |
|---|---|
| Components(组件) | 做实际工作的部分(模块、服务、进程等) |
| Connectors(连接器) | 组件之间通信的机制(调用、管道、消息队列、网络、事件……) |
| Configurations(配置) | 组件与连接器具体如何排布 —— 拓扑结构 |
components + connectors + configurations 这三元组,是下面每一种架构风格使用的共同词汇。当你读到 "pipe-and-filter" 或 "publish-subscribe" 时,每种风格本质上都是一份命名的配方,规定要用哪些组件、哪些连接器、什么样的配置。
6. 规范性架构 vs. 描述性架构
6.1 同一个系统的两种视角
每一个真实系统实际上都有两份架构:
| 视角 | 含义 | 别称 |
|---|---|---|
| Prescriptive architecture(规范性架构) | 设计者所期望的架构 | as-designed / as-intended |
| Descriptive architecture(描述性架构) | 实际在运行代码中呈现出来的架构 | as-implemented / as-realized |
6.2 为什么会出现差异
对于简单或年轻的系统,两者可能一致。对于大型、长期演化的系统(课上讨论的例子:Linux、iRODS、HADOOP),规范性和描述性架构可能差异巨大。规范性架构图干净、模块化;描述性架构图则暴露出各种捷径、补丁和本不该有的依赖。
7. 架构侵蚀(Architectural Erosion)
7.1 什么是架构侵蚀
理想流程是:当系统要演化时,先修改规范性架构,再更新实现使之一致。实际情况往往是:系统(从而是描述性架构)被直接改动,而规范性架构没有被同步更新。久而久之,两者的差距越拉越大。
7.2 为什么会发生
侵蚀很少是单一原因造成的。常见的推动因素:
- Developer sloppiness —— 走捷径但没有记录下来
- Short deadlines —— 规范性更新被"就这一次"跳过(然后就再也没补上)
- Lack of (documented) prescriptive architecture —— 如果原本的设计意图从未被写下来,开发者就无从"违反"它
- Code optimizations —— 横跨模块边界的性能优化
- Inadequate techniques or tool support —— 没有自动化手段检查代码是否符合设计
架构侵蚀既是设计问题,也是维护问题。避免它需要有意为之:把规范性架构写下来,让它保持最新,并持续检查一致性。
8. 架构风格 — 定义
8.1 什么是架构风格
一个架构风格(architectural style)是:
- 一组命名的架构设计决策集合,这组决策:
- 适用于某个特定的开发上下文,
- 约束架构设计决策,
- 并在每一个使用它的系统中产生有益的性质。
- 等价地:一组命名的、常见的 components / connectors / configurations 组合,其中每一个组件/连接器都有自己的职责。
8.2 为什么风格有用
风格本质上是一份久经验证的配方。采用一种风格,就继承了它带来的好处(模块化、可扩展性等),但也要接受它的约束(必须按它的规则玩)。本节剩下的内容涵盖六种被广泛使用的风格,每种都给出一个启发式的例子和一组收益。
9. 六种架构风格
课程介绍六种架构风格。这不是完整清单 —— 还有很多其他风格 —— 但这六种是你需要掌握的。
9.1 Model-View-Controller(MVC)
例子:自动取款机(ATM)
一台 ATM 的目标包括取款、存款、转账、查询余额,同时还要做到清晰的关注点分离:
- 团队想尝试不同的 UI
- 团队想隔离业务规则,因为这些规则经常变
MVC 把系统分成三部分:
| 部分 | 职责 | ATM 中的映射 |
|---|---|---|
| Model | 保存数据和业务规则 | 银行账户、业务规则 |
| View | 向用户展示信息 | ATM 的显示屏 |
| Controller | 处理用户输入,并协调 model 与 view | 输入处理器 |
关键收益: Modularity(模块化)、Anticipation of Change(对变化的预期)—— 你可以在不碰 model 的情况下换 view,也可以在不碰 UI 的情况下改业务规则。
MVC 特别针对 UI 层的关注点。 实际中,人们说的 MVC "controller" 几乎总是指由 UI 驱动的控制逻辑(处理按钮点击、表单提交等)。这种模式之所以存在,是因为把数据、渲染、UI 逻辑混在一起写出的代码极难修改 —— 改一个颜色、一个指标、一条业务规则都会变成横跨多处的大改动。MVC 正是把这三件事分开,让它们各自独立变化。

9.2 Client-Server(客户端-服务器)
例子:在线多人视频游戏
目标:
- 多人、在线
- 控制对数据和玩家的访问
结构:一台游戏服务器配多个游戏客户端(game client 1、game client 2、game client 3……)。客户端发起请求;服务器保存集中化的状态。
关键收益:
- 在大量客户端之间共享数据和服务
- 对访问、功能和数据的集中化控制
主要缺点: 中央服务器是单点负载与单点故障 —— 随着客户端数量增长,一台服务器很容易被压垮;一旦宕机,所有客户端都会失去服务。客户端-服务器系统的扩展手段(服务器复制、负载均衡、缓存等)很大程度上就是在回应这一弱点。
云计算改变了 client-server。 现代大多数 client-server 应用都运行在云上的服务器上:
| 云之前 | 有云之后 |
|---|---|
| 软件开发者必须管理物理服务器 | 服务器由第三方管理(由 AWS 开创) |
| 单一数据中心,扩展困难 | 云厂商还提供应用级服务 |
云的优点:可扩展性、更大的算力、可能更划算。 云的缺点:安全/隐私问题,并且在某些规模下可能并不划算。

9.3 Layered(分层)
例子:餐厅
目标:
- 为顾客准备并提供食物
- 强制一种特定的交互协议
- 把无关细节抽象掉
- 把变更的影响最小化
- Separate concerns(分离关注点):让每一部分只专注于某一件具体的任务
结构:系统被划分为多层(layers),每一层只与紧挨着它的上下两层通信。一个真实世界的例子是 Android,它被组织成一个分层的栈(kernel、libraries、application framework、applications)。
关键收益: Modularity、Abstraction、Anticipation of Change、Reuse。
最底层通常是最可复用的。 Android 这个例子很具体:Linux kernel 位于最底层,而它不仅被 Android 复用,还被互联网上大多数服务器复用。越往上层,越贴近具体应用;越往下层,越通用、越可复用。这也是为什么给一个 app 换皮(替换最上层的 UI)很容易,但替换内核几乎是不可想象的。
餐厅之所以能运转,是因为服务员不做菜、厨师不直接面对顾客点单、顾客不进厨房。每一层都通过定义好的接口与它接触的层对接。

9.4 Peer-to-Peer(P2P,对等)
例子:Skype
目标:
- 在线(多人)视频聊天和语音通话
- 在多人之间共享同样的功能,并且不让视频/音频流变卡
结构:peer 之间(skype 1、skype 2、skype 3)直接通信 —— 没有中央服务器去中介每一次通话。每个 peer 扮演的角色基本相同。
关键收益: Efficient(高效:没有中心瓶颈)、Robust(健壮:没有单点故障 —— 某个 peer 掉线,其余系统照常运转)。
其他真实例子: BitTorrent、Bitcoin,广义上还包括互联网本身 —— 这些系统都具备 P2P 性质:没有任何一个节点是不可或缺的。
实际上,"纯粹"的 P2P 很少见。 大多数部署在真实世界的 P2P 系统仍然带有一小块 client-server 组件,用于 peer 自己做不好的事情 —— 例如 Skype 需要一个中央目录来查找"某个用户名对应哪个 IP 地址";BitTorrent 传统上也需要 tracker 来做类似的事。批量流量(视频、音频、文件)走 peer-to-peer,但薄薄一层"协调"往往仍是集中式的。

9.5 Pipe-and-Filter(管道与过滤器)
例子:编译器
目标:
- 把高级语言翻译成计算机可以执行的低级语言
- 把翻译的不同步骤分离开,这样你可以方便地添加 / 修改 / 删除 / 替换某一步
结构:一个 filter(过滤器)是一个读取输入流、产生输出流的组件。一个 pipe(管道)是把流从一个 filter 输送到下一个 filter 的连接器。
对于一个编译器:
scanner --> stream (tokens) --> parser --> stream (parse trees) --> translator
^filter ^pipe ^filter ^pipe ^filter经典的 Unix shell 例子:
ls -l | grep "Aug" | sort每个命令都是一个 filter;每个 | 都是一条 pipe。前一个 filter 的输出就是下一个 filter 的输入。
关键收益: Modularity、Reuse、Anticipation of Change(你可以在不动其他 filter 的情况下插入、删除或重排某个 filter)。

9.6 Publish-Subscribe("Pub-Sub",发布-订阅)
例子:股票行情机(stock market ticker)
目标:
- 从海量信息(股票价格)中筛选,只把订阅者感兴趣的信息广播出去
结构:一个 subscription manager(订阅管理器)位于数据源(股票市场)和订阅者(trader 1、trader 2、trader 3)之间。订阅者登记自己的兴趣;管理器只把相关事件路由给每个人。
关键收益: Efficiency(订阅者不会被不想要的数据淹没)、Scalability(增加订阅者不需要修改发布者)。

10. 混合风格
现实系统很少能干净地归入单一风格 —— 混合往往是必要的。例如,一个股票行情系统可以同时结合 pub-sub(把价格推给交易员)和 client-server 子结构(股票市场服务器、订阅管理器、一个被多个组件共享的股票数据库),再额外接一块"购买规则"组件。
看到真实的架构图时,预期会是多种风格的组合。"这个系统用了 pub-sub 并且 client-server 并且 layered"是一个正常、健康的描述,不是设计混乱的标志。
11. 演化与架构
11.1 演化不可避免
所有成功的软件都会演化。 演化是软件开发不可或缺的一部分,必须尽可能地被容纳/规划进去。
架构是容纳演化的关键工具。 第 6 节的规范性/描述性划分、第 9 节的风格选择、以及下面的基本原则,存在的很大一部分原因就是:让未来的演化更便宜。
11.2 Web —— 演化的典范例子
万维网大概是最清楚的一个例子 —— 一个架构让它自己最初的设计者都没有预见到的用途成为可能。最早的版本(1991 年)几乎只是带超链接的纯文本(例如 CERN 最早的那一页)。到 1990 年代末,它已经能支持电子商务(早期的 Amazon)。今天,它支撑着视频流、社交网络、实时应用,以及更多。
这些后来的用途在最初的设计里一个都没有被预想到。但因为 REST 参考架构里选择的骨架 —— URL、HTTP、无状态请求、资源表示 —— 足够扎实、足够通用,Web 才能在同一副骨架上不断叠加新能力,三十多年不需要重新设计。这就是架构驱动演化的最佳范例。

11.3 趋势:从单体(monolithic)到微服务(microservices)
业界当前一个具体的演化趋势:单体应用(整个系统作为一个大的可部署单元)越来越多地被拆成微服务(microservices)(许多小的、可独立部署的服务)。完成这个迁移的真实系统包括 Netflix。

12. 与基本原则的联系
第一周学过的基本原则同样适用于软件架构:
| 原则 | 在架构中的体现 |
|---|---|
| Rigor and formality(严格与形式化) | 精确的图示、精确的风格定义、精确的组件/连接器词汇 |
| Separation of concerns(关注点分离:模块化、分而治之、抽象) | 上面每一种风格都是关注点分离的一个实例 —— MVC 分离 data/UI/control,layered 分离抽象层级,pub-sub 把发布者和订阅者解耦 |
| Anticipation of change(对变化的预期) | 选择让变化廉价的风格(MVC、layered、pipe-and-filter);把规范性架构保持最新 |
| Generality(通用性) | 风格本身就是可跨许多系统使用的通用解决方案 |
| Incrementality(增量性) | 架构让增量地添加特性、filter、服务或订阅者成为可能 |
架构不是与这些原则并列的另一个主题 —— 它恰恰是这些原则最显眼的落地场所之一。
13. Quiz 4 复习要点
你需要知道并理解:
- 什么是软件架构
- 关于系统的一组主要(principal)设计决策
- 系统构建与演化的蓝图
- 为什么架构重要(思维掌控、概念完整性、沟通、可复用性、可维护性)
- 软件架构的组成要素
- Components、connectors、configurations
- 架构侵蚀
- 规范性架构 vs. 描述性架构
- 为什么会发生侵蚀
- 六种架构风格
- Model-View-Controller
- Client-Server
- Layered
- Peer-to-Peer
- Pipe-and-Filter
- Publish-Subscribe
- 每一种都要知道:启发例子、结构模式、关键收益
14. 总结与记忆框架
第 4 周知识结构
|
+-- 为什么要架构
| +-- 桥接 requirements -> code
| +-- 带来:思维掌控、概念完整性、
| | 沟通、可复用性、可维护性
| +-- 与建筑架构师的类比(但软件是可塑的、
| 不可见的、是一台机器,而且我们对它了解更少)
|
+-- 软件架构的定义
| +-- 关于系统的一组 PRINCIPAL 设计决策
| +-- 涵盖:结构、行为、交互、非功能属性
| +-- CONSTRUCTION 与 EVOLUTION 的蓝图
| +-- 其他定义:Clements/Bass/Kazman、Spinellis、Fowler、Natarén
|
+-- 架构在实践中
| +-- Email -> client 到 client
| +-- WWW / REST -> resources + URLs + HTTP + stateless
| | (没有哪一个文件"就是"架构)
| +-- Facebook -> 大量功能 + 大量非功能需求
| +-- Client-Server -> 最常见的分布式架构
|
+-- 架构的组成要素
| +-- components (做工作的部分)
| +-- connectors (各部分如何通信)
| +-- configurations(如何排布)
|
+-- 规范性 vs. 描述性
| +-- prescriptive = as-designed / as-intended
| +-- descriptive = as-implemented / as-realized
| +-- 两者发生偏离的例子:Linux、iRODS、Hadoop
|
+-- 架构侵蚀
| +-- 描述性架构随时间偏离规范性架构
| +-- 原因:草率、deadline、无文档、
| 代码优化、工具支持不足
|
+-- 架构风格(定义)
| +-- 命名的设计决策集合
| +-- 适用于某一上下文
| +-- 约束后续决策
| +-- 产生有益的性质
|
+-- 六种风格
| +-- MVC (ATM) -> 模块化、对变化的预期
| | (特别针对 UI 层的关注点)
| +-- Client-Server (视频游戏) -> 共享数据/服务、集中化控制
| | 缺点:单台服务器容易被压垮
| | (云改变了 client-server)
| +-- Layered (餐厅, -> 模块化、抽象、
| | Android) 对变化的预期、复用
| | (最底层最可复用,例如 Linux 内核)
| +-- Peer-to-Peer (Skype, -> 高效、健壮
| | BitTorrent, (纯 P2P 少见,通常还需要一层
| | Bitcoin) 小的 client-server 用于发现 peer)
| +-- Pipe-and-Filter (编译器, -> 模块化、复用、
| | ls | grep | sort) 对变化的预期
| +-- Publish-Subscribe(股票行情机) -> 效率、可扩展性
|
+-- 混合风格
| +-- 真实系统通常是多种风格的组合
|
+-- 演化
| +-- 所有成功的软件都会演化
| +-- 架构是管理演化的关键工具
| +-- 典范例子:Web
| | (1991 CERN 纯文本 -> 1997 Amazon -> 现代社交/视频)
| | -> REST 骨架不变,特性在其上叠加超过 30 年
| +-- 趋势:单体 -> 微服务(Netflix)
|
+-- 基本原则(仍然是同样 5 条)
+-- 严格与形式化
+-- 关注点分离(模块化、分而治之、抽象)
+-- 对变化的预期
+-- 通用性
+-- 增量性一句话总结
软件架构是关于系统的一组主要设计决策 —— 由 components、connectors、configurations 组成 —— 位于需求与代码之间;它同时以规范性(intended)和描述性(realized)两种形式存在,并且这两者会随时间彼此偏离(架构侵蚀);它通过一组命名的、可复用的架构风格(MVC、client-server、layered、peer-to-peer、pipe-and-filter、publish-subscribe)来表达,而这些风格的全部意义,就是把软件工程的基本原则 —— 特别是关注点分离和对变化的预期 —— 落到实处,让系统能够优雅地演化。