第 4 周 · 软件工程课程阅读笔记

这页整理的是 IN4MATX 43 第 4 周 的课程笔记,方便快速回顾软件工程里的定义、原则、框架和课堂重点。下面这条路径可以带你回到 Notes、Course Notes、课程主页,或定位到当前笔记。将鼠标悬停在右侧的 ≡ 图标上可以打开目录。

最后编辑于 2026 年 5 月 31 日

Informatics 43 — 第四周课程笔记

目录

  1. 从需求到结构 — 为什么需要架构
  2. 架构师(Architect)— 一个类比(以及它的局限)
  3. 定义软件架构
  4. 架构在实践中 — 案例分析
  5. 软件架构的组成要素
  6. 规范性架构 vs. 描述性架构
  7. 架构侵蚀(Architectural Erosion)
  8. 架构风格 — 定义
  9. 六种架构风格
  10. 混合风格
  11. 演化与架构
  12. 与基本原则的联系
  13. Quiz 4 复习要点
  14. 总结与记忆框架

1. 从需求到结构 — 为什么需要架构

1.1 需求与代码之间的鸿沟

在第二、三周之后,我们手上已经有了需求规约和一套用例(use cases)。自然而然要问的下一个问题是:接下来呢? 面对同一套需求,系统可以有许多种可能的结构方式 —— 不同的模块划分、不同的职责分配、不同的通信方式。架构就是有意识地做这些结构决策的学科。

整个过程是这样的:

需求(Requirements)  ⟶  架构与设计(Architecture & Design)  ⟶  代码(Code)\boxed{\textbf{需求(Requirements)} \;\longrightarrow\; \textbf{架构与设计(Architecture \& Design)} \;\longrightarrow\; \textbf{代码(Code)}}

架构处在中间 —— 它不是需求(需求说明系统要做什么),也不是代码(代码精确指定每一个细节怎么做)。它是联接两者的高层结构。

1.2 为什么架构在软件工程中如此重要

架构有五条具体的价值:

收益 含义
Intellectual control(思维掌控) 让庞大复杂的系统仍然能被人脑理解
Conceptual integrity(概念完整性) 让系统有一个连贯的"故事",而不是各种风格拼凑出来的补丁
Effective project communication(有效的项目沟通) 为团队提供共同的词汇和图示,用来推理系统
Reusability(可复用性) 结构良好的组件可以在多个系统或变体之间复用
Maintainability(可维护性) 方便管理一组变体系统,并支持长期演化

2. 架构师(Architect)— 一个类比(以及它的局限)

2.1 (建筑)架构师做什么

"软件架构"这个名字借自建筑学,这个类比确实有参考价值。一位建筑架构师:

  • 在项目中扮演一个独特角色,与施工工人分工明确
  • 有广泛的训练、丰富的经验、深入的领域理解
  • 领导团队、善于沟通、是决策者
  • 工作在比实际施工者更高的抽象层级上
  • 通常担当与关键业务利益相关者/客户对接的接口

软件架构师在软件项目中扮演的是类似的角色。

2.2 类比失效的地方

这个类比可以作为入门直觉,但不宜推得太远 —— 软件和建筑有本质区别:

  • 软件服务的用途比建筑广泛得多
  • 我们对建筑的了解远远多于软件(几个世纪 vs. 几十年)
  • 软件比物理材料更加可塑 —— 建筑建成后修改代价极高,软件修改相对便宜
  • 软件是一台机器,而不是静态结构 —— 它有运行时行为
  • 软件是不可见的 —— 你无法像看建筑那样看到、走进、或检查它

把这个类比当作直觉保留,但要记住:软件架构需要面对行为、变化与不可见性,而建筑架构并不需要。


3. 定义软件架构

3.1 主要定义

软件架构=关于系统的一组主要(principal)设计决策\boxed{\textbf{软件架构} = \text{关于系统的一组}\textit{主要(principal)设计决策}}

"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 架构的三个重要观察:

  1. 没有哪一段代码单独"实现"了这个架构 —— 它是整个系统的性质,不是某个文件或程序的性质
  2. 这些风格约束在代码中看不出来 —— 你读任何一个 HTTP 服务器的源代码,都看不到"REST"这几个字
  3. 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 什么是架构侵蚀

架构侵蚀=系统实际(描述性)架构与期望(规范性)架构逐渐偏离的现象\boxed{\textbf{架构侵蚀} = \text{系统实际(描述性)架构与期望(规范性)架构逐渐偏离的现象}}

理想流程是:当系统要演化时,先修改规范性架构,再更新实现使之一致。实际情况往往是:系统(从而是描述性架构)被直接改动,而规范性架构没有被同步更新。久而久之,两者的差距越拉越大。

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 正是把这三件事分开,让它们各自独立变化。

MVC 映射到 ATM:ATM 显示屏(view)、规则组件(controller)、银行账户(model)通过箭头表示彼此之间的调用关系。


9.2 Client-Server(客户端-服务器)

例子:在线多人视频游戏

目标:

  • 多人、在线
  • 控制对数据和玩家的访问

结构:一台游戏服务器配多个游戏客户端(game client 1、game client 2、game client 3……)。客户端发起请求;服务器保存集中化的状态。

关键收益:

  • 在大量客户端之间共享数据和服务
  • 对访问、功能和数据的集中化控制

主要缺点: 中央服务器是单点负载与单点故障 —— 随着客户端数量增长,一台服务器很容易被压垮;一旦宕机,所有客户端都会失去服务。客户端-服务器系统的扩展手段(服务器复制、负载均衡、缓存等)很大程度上就是在回应这一弱点。

云计算改变了 client-server。 现代大多数 client-server 应用都运行在云上的服务器上:

云之前 有云之后
软件开发者必须管理物理服务器 服务器由第三方管理(由 AWS 开创)
单一数据中心,扩展困难 云厂商还提供应用级服务

云的优点:可扩展性、更大的算力、可能更划算。 云的缺点:安全/隐私问题,并且在某些规模下可能并不划算。

在线多人游戏的 client-server 结构:三个 game client 同时向一台中心化的 game server 发起请求。


9.3 Layered(分层)

例子:餐厅

目标:

  • 为顾客准备并提供食物
  • 强制一种特定的交互协议
  • 把无关细节抽象掉
  • 把变更的影响最小化
  • Separate concerns(分离关注点):让每一部分只专注于某一件具体的任务

结构:系统被划分为多层(layers),每一层只与紧挨着它的上下两层通信。一个真实世界的例子是 Android,它被组织成一个分层的栈(kernel、libraries、application framework、applications)。

关键收益: Modularity、Abstraction、Anticipation of Change、Reuse。

最底层通常是最可复用的。 Android 这个例子很具体:Linux kernel 位于最底层,而它不仅被 Android 复用,还被互联网上大多数服务器复用。越往上层,越贴近具体应用;越往下层,越通用、越可复用。这也是为什么给一个 app 换皮(替换最上层的 UI)很容易,但替换内核几乎是不可想象的。

餐厅之所以能运转,是因为服务员不做菜、厨师不直接面对顾客点单、顾客不进厨房。每一层都通过定义好的接口与它接触的层对接。

Android 的分层架构:最上层是 Applications,其下为 Application Framework、Libraries 与 Android Runtime,最底层是 Linux Kernel。


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,但薄薄一层"协调"往往仍是集中式的。

Skype 的 P2P 拓扑:skype 1、skype 2、skype 3 三个 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)。

Unix `ls -l | grep


9.6 Publish-Subscribe("Pub-Sub",发布-订阅)

例子:股票行情机(stock market ticker)

目标:

  • 从海量信息(股票价格)中筛选,只把订阅者感兴趣的信息广播出去

结构:一个 subscription manager(订阅管理器)位于数据源(股票市场)和订阅者(trader 1、trader 2、trader 3)之间。订阅者登记自己的兴趣;管理器只把相关事件路由给每个人。

关键收益: Efficiency(订阅者不会被不想要的数据淹没)、Scalability(增加订阅者不需要修改发布者)。

股票行情机的 pub-sub 结构:stock market 把事件推给 subscription manager(连接器),后者只把相关事件路由给 trader 1、trader 2、trader 3。


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 才能在同一副骨架上不断叠加新能力,三十多年不需要重新设计。这就是架构驱动演化的最佳范例。

1991 年 CERN 第一个万维网页面(info.cern.ch)截图 —— 纯文本加蓝色超链接。三十多年过去,同一套架构骨架现在支撑着视频流、电子商务与实时应用。

11.3 趋势:从单体(monolithic)到微服务(microservices)

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

单体架构 vs. 微服务架构对比:左侧是一个把所有功能打包在一起的可部署单元,右侧是多个可独立部署的小服务,通过彼此通信协作。


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. 总结与记忆框架

一句话总结

软件架构是关于系统的一组主要设计决策 —— 由 components、connectors、configurations 组成 —— 位于需求与代码之间;它同时以规范性(intended)和描述性(realized)两种形式存在,并且这两者会随时间彼此偏离(架构侵蚀);它通过一组命名的、可复用的架构风格(MVC、client-server、layered、peer-to-peer、pipe-and-filter、publish-subscribe)来表达,而这些风格的全部意义,就是把软件工程的基本原则 —— 特别是关注点分离和对变化的预期 —— 落到实处,让系统能够优雅地演化。

快捷键[ 上一篇] 下一篇L 切换语言