质量属性与架构战术
当我们说「这是个好系统」时,到底在说什么
上一节谈到,架构关注的不只是「系统能做什么」,更是「系统做得有多好」——可用性、性能、安全性、可修改性这些「非功能」的维度。这些词听起来直觉、却出奇地难写进合同里:
- 「系统应当高可用」——多高?停机多久算停机?计划维护算不算?
- 「系统应当快速响应」——多快?平均响应还是 99 分位?空载还是峰值?
- 「系统应当易于修改」——什么类型的修改?谁来改?多长时间内改完?
如果这些问题没有可验证的答案,架构师就只能靠「感觉」做决策——选了缓存却不知道能省多少延迟,加了冗余却不知道能扛住几次故障。本节的核心任务,就是把这些模糊的「质量诉求」翻译成可写下来、可测量、可验证的工程语言。
我们会沿着三条主线展开。第一条是概念基础:什么是质量需求,怎样用一个统一的「场景模板」描述它。第二条是七大质量属性:可用性、互操作性、可修改性、性能、安全性、可测试性、易用性——每一个都配一组战术(Tactics),即针对该属性的可复用设计技巧。第三条是架构上显著的需求(ASRs):怎样从客户、文档、商业目标里挖出真正决定架构走向的那少数几个需求。
走完这三条线,你就能在面对一个新系统时,有底气说出:「这个系统要满足这几个 ASR,对应这几个质量属性,每个属性我准备用这几个战术去落地,理由是这样这样。」这正是架构师把「直觉」转化为「工程」的过程。
三种需求:功能、质量、约束
在讨论质量之前,先把「需求」这个词拆清楚。一个软件项目里飘着的需求大致分三类。
三类需求
- 功能需求(Functional Requirements):系统必须做什么——它的行为、它对干系人提供的价值
- 质量需求(Quality Requirements,又称质量属性 Quality Attributes):系统做得有多好——在功能之上的期望特性
- 约束(Constraints):已经做出的、自由度为零的设计决策——「必须用 Java 8」「必须部署在公司私有云」
这三者的关系不是并列的。功能回答「能不能用」,质量回答「用得多好」,约束则是一些不容讨论的前提条件。
flowchart TD
P[一个软件项目的需求]
P --> F[功能需求<br>Functional<br>系统必须做什么]
P --> Q[质量需求<br>Quality<br>系统做得多好]
P --> C[约束<br>Constraints<br>不可商榷的设计决策]
F -.被质量限定.-> Q
C -.限制.-> F
C -.限制.-> Q
classDef root fill:#eee,stroke:#495057,stroke-width:2px
classDef func fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef qual fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cons fill:#ffebee,stroke:#c62828,stroke-width:2px
class P root
class F func
class Q qual
class C cons
功能需求是结构无关的
一个有意思的观察:功能本身和系统的内部结构基本无关。同样是「让学生在线选课」这个功能,可以做成单体应用、可以做成微服务、可以做成无服务器架构——只要选课能跑通,从功能视角看它们都「满足需求」。
Functionality is largely independent of structure, because it could exist as a single monolithic system without any internal structure.
那为什么我们还需要架构?因为选什么结构会决定质量属性的上限——单体跑得快但难扩展,微服务能水平扩展但增加运维复杂度,无服务器易扩展但有冷启动延迟。架构约束的,正是「把功能映射到不同结构」时的取舍——而这种取舍,只有在质量属性重要时才浮现出来。
质量需求是功能的「限定词」
质量需求并不独立存在,它们总是修饰着功能需求或整体产品。同样是「用户登录」:
- 加上「500 ms 内返回」——成了性能质量需求
- 加上「99.99% 时间可用」——成了可用性质量需求
- 加上「每次失败尝试必须记录」——成了安全性质量需求
这就是 SEI 教材里反复强调的那句话:
Quality requirements are qualifications of the functional requirements or of the overall product.
约束是不容讨论的前提
约束的特征是「零自由度」——它已经被决定了,架构师不在它身上做选择,只能接受它并调整其他设计决策来与之相容。
约束的来源非常多样:客户指定的技术栈、公司内部的中间件、必须遵守的行业法规、团队成员熟悉的语言、已有系统的协议……架构师面对约束时的姿态不是「能不能挑战」,而是「在这个边界内,我还能做什么样的设计」。
非功能需求:可观察的与不可观察的
「质量需求」「质量属性」「非功能需求」(Non-Functional Requirements, NFRs)「架构需求」(Architectural Requirements)这几个词在不同教材里被混用——它们本质上指的是同一类东西。本节后续统一使用 NFRs。
质量不能事后补
It is not possible to get the functionality right and then try to accommodate non-functional requirements (NO retro-fitting quality).
NFRs 必须在每一个设计决策中都被考虑——你不能先做完一个「能跑」的系统,再回头给它「加上」性能或安全性。这是架构师反复要向项目经理强调的事情。
NFRs 一般分两大类:
| 类别 | 含义 | 例子 |
|---|---|---|
| 可观察的(External / Observable) | 系统执行时对外表现出来的、用户能感受到的 | 性能、安全性、可用性、易用性 |
| 不可观察的(Internal / Not Observable) | 不在执行时显现,而是在维护、集成、测试过程中体现 | 可修改性、可移植性、可复用性、可测试性 |
这个划分有点反直觉——「可测试性」明明很重要,怎么是「不可观察的」?关键在于观察的视角:用户从外部用系统时不会感受到「这个系统好测试」,但开发者在维护时会强烈地体会到。两类 NFRs 都重要,但它们的测量主体不同——可观察的由用户/监控系统度量,不可观察的由开发团队的工时和缺陷数度量。
为什么质量是架构层面的事
SEI 教材把这一点说得很直白:没有任何质量属性完全依赖于设计、实现或部署中的某一层——它们是从架构开始、贯穿到代码、再渗透到部署的全局性质。
No quality attribute is entirely dependent on design, nor is it dependent on implementation or deployment.
所以架构层是最合适的处理质量问题的层次——再往上太抽象(变成商业目标),再往下太具体(陷在代码细节里)。
用「场景」精确刻画质量属性
谈了这么多「质量很重要」,但每个人对「高可用」「快」「易修改」的理解都不一样。怎么把它精确化?答案是质量属性场景(Quality Attribute Scenario)。
质量属性场景
质量属性场景是用一种结构化的方式描述某个质量属性需求的小段「故事」——什么人、在什么条件下、对系统做了什么、系统应该怎么响应、响应的好坏怎么测量。
场景是后续做架构评估、选战术、写测试的共同基础——没有场景就只有口号。
场景分两种:
- 通用场景(General Scenarios):系统无关的模板,用于「启发」该质量属性下可能的需求。每个质量属性都有自己的通用场景模板,本节后面会逐一展示
- 具体场景(Concrete Scenarios):把通用场景翻译到具体系统的版本,可写入需求文档、可测试
具体场景是通用场景的实例——架构师拿着通用场景模板和干系人开会,把每一栏都填上当前系统的具体内容,就得到了具体场景。
场景的六要素
无论哪个质量属性,场景都由六个部分组成:
flowchart LR
SS[Source<br>刺激源] --> S[Stimulus<br>刺激]
S --> AR[Artifact<br>受影响部分]
subgraph ENV[Environment 环境]
AR
end
AR --> R[Response<br>系统响应]
R --> RM[Response Measure<br>响应度量]
classDef source fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef stim fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef art fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef resp fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef env fill:#f8f9fa,stroke:#888,stroke-width:1px,stroke-dasharray: 5 5
class SS source
class S stim
class AR art
class R,RM resp
| 要素 | 含义 |
|---|---|
| 刺激(Stimulus) | 一个需要被系统考虑的条件/事件——它到达系统时,触发我们要研究的行为 |
| 刺激源(Source of Stimulus) | 产生刺激的实体(人、系统、传感器) |
| 响应(Response) | 刺激到达后系统采取的活动 |
| 响应度量(Response Measure) | 响应必须以某种方式可度量,否则需求无法测试 |
| 环境(Environment) | 刺激发生时的系统条件——正常运行、过载、降级、启动 |
| 构件(Artifact) | 整个系统、或者系统的特定部分——刺激具体作用在哪里 |
举一个性能场景的例子:
- 刺激源:外部的电商客户端
- 刺激:发起一笔交易请求
- 构件:订单处理子系统
- 环境:双十一峰值流量
- 响应:处理交易并返回结果
- 响应度量:95% 请求的响应时间 ≤ 200 ms
注意「响应度量」这一栏——这是把质量需求从口号变成工程的关键。少了它,所有人都觉得「我们要快」,但没人知道「快」是多少。
战术:质量属性的「设计技巧」
有了场景能描述需求,接下来的问题是:怎么满足这些需求?答案是战术(Tactics)。
战术
战术是影响某个质量属性响应控制的设计决策。
A tactic is a design decision (e.g. redundancy) that influences the control of a quality attribute response.
一组战术的集合称为架构策略(Architectural Strategy)。
战术比设计模式更细粒度——一个模式(如代理模式)通常组合了多个战术(缓存、限流、鉴权代理)来兼顾多个质量属性,而每个战术只针对一个质量属性。可以这样理解:
flowchart LR
QAS[质量属性场景<br>需求]
T[战术<br>对单一属性的设计技巧]
P[模式 / 风格<br>组合多个战术]
A[架构<br>整体决策集合]
QAS --> T
T --> P
P --> A
classDef scenario fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tactic fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef pattern fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef arch fill:#ffebee,stroke:#c62828,stroke-width:2px
class QAS scenario
class T tactic
class P pattern
class A arch
战术的几个关键性质:
- 战术可以由其他战术组合——比如「冗余」可以拆成「数据冗余」和「计算冗余」,架构师根据需求选其一或两者都用
- 战术形成层次结构——同一个战术有多种实现细化
- 设计模式应用战术来实现承诺的好处——模式本身的「好处」往往就是它体现的若干战术
把战术想成菜谱里的「单一调味技巧」(如「加酸提鲜」),把模式想成「一道完整的菜」(如「水煮鱼」要同时用麻、辣、烫、鲜多种技巧)。
七大设计决策类别
每个战术最终会落到具体的设计决策上,而 SEI 把架构师做的设计决策归纳为七个类别:
设计决策的七大类别
- 职责分配(Allocation of Responsibilities):哪个组件负责什么
- 协调模型(Coordination Model):组件之间如何对话——同步/异步、保证投递/最佳努力
- 数据模型(Data Model):核心数据抽象、操作、性质
- 资源管理(Management of Resources):CPU、内存、连接、线程的分配与回收
- 架构元素映射(Mapping among Architecture Elements):组件如何映射到进程、进程如何映射到处理器
- 绑定时间决策(Binding Time Decisions):什么时候确定一个值——设计时、编译时、部署时、运行时
- 技术选择(Choice of Technology):用什么语言、框架、中间件、数据库
这七大类是后面**每个质量属性「设计检查清单」**的统一格式——讨论可用性的时候,我们会问「为了可用性,这七个类别的决策应该怎么做」;讨论安全性的时候,再问同样的七个问题。这种结构化的对照让架构评审有迹可循。
质量属性的全景
工业界教材列出的常见质量属性多达二十余个,下面这张表给一个不完全清单:
| 列 1 | 列 2 |
|---|---|
| 适应性(Adaptability) | 可扩展性(Extensibility) |
| 可用性(Availability) | 模块性(Modularity) |
| 可配置性(Configurability) | 可移植性(Portability) |
| 灵活性(Flexibility) | 可复用性(Reusability) |
| 互操作性(Interoperability) | 可测试性(Testability) |
| 性能(Performance) | 可审计性(Auditability) |
| 可靠性(Reliability) | 可维护性(Maintainability) |
| 响应性(Responsiveness) | 可管理性(Manageability) |
| 可恢复性(Recoverability) | 可持续性(Sustainability) |
| 可伸缩性(Scalability) | 可支持性(Supportability) |
| 稳定性(Stability) | 易用性(Usability) |
| 安全性(Security) |
不是所有项目都关心全部属性。架构师的工作是和干系人一起,从这张表里挑出对当前系统真正重要的几个,把它们写成场景,再为每个场景挑选合适的战术。本节接下来挑七个最重要的属性逐一展开:可用性、互操作性、可修改性、性能、安全性、可测试性、易用性。
每个属性会按这个固定结构讲:定义 → 通用场景 → 战术分类 → 各战术解释。这种重复正是为了让大家熟悉这个分析框架。
可用性
「凌晨两点,电商网站的下单接口挂了,运维被叫起床。」——这就是可用性失守时的真实代价。
可用性
可用性(Availability)衡量系统在需要时能正常提供服务的比例,是大多数 IT 应用的关键需求。
可用性最常用的定义性公式是:
其中 MTBF(Mean Time Between Failures,故障间平均时间)刻画系统多久才会坏一次,MTTR(Mean Time To Repair,平均修复时间)刻画系统坏了多久能恢复。两个变量按 1:1 出现在分子分母,意味着可用性既靠少坏,也靠快修——稳定性差但修得飞快的系统,可能比稳定性高但修起来慢吞吞的系统可用性更高。
计划停机不算
Scheduled downtimes 一般不计入可用性计算。每周二凌晨三点的 15 分钟维护窗口,对用户来说仍是「停机」,但不会从这个公式里扣分——这是行业惯例,写 SLA 时要约定清楚。
服务等级协议
把可用性数字翻译成「能停机多久」,就有了一张著名的对照表,叫服务等级协议(Service Level Agreement, SLA):
| 可用性 | 每 90 天停机 | 每年停机 |
|---|---|---|
| 99.0%(两个 9) | 21 小时 36 分钟 | 3 天 15.6 小时 |
| 99.9%(三个 9) | 2 小时 10 分钟 | 8 小时 0 分 46 秒 |
| 99.99%(四个 9) | 12 分 58 秒 | 52 分 34 秒 |
| 99.999%(五个 9) | 1 分 18 秒 | 5 分 15 秒 |
| 99.9999%(六个 9) | 8 秒 | 32 秒 |
每多一个「9」,对系统的要求大约成数量级地上升——五个 9 意味着一年只能停机 5 分钟,单纯靠人工操作根本来不及响应,必须有自动化的故障切换机制。
AWS EC2 的 SLA
Amazon EC2 在其官方 SLA 中承诺年度可用率不低于 99.95%。如果 AWS 没达到这个数字,客户有资格申请服务积分(Service Credit)。
99.95% 意味着 AWS 每年可以「合法地」停机大约 4 小时 22 分钟——这就是云厂商和客户之间的契约。
故障的语言:Outage、Failure、Fault、Error
讨论可用性必须把这几个词分清楚——它们在中文里都常被翻译成「故障」,但在工程语境下指代不同的东西。
flowchart LR
F[Fault<br>故障源/故障原因]
E[Error<br>错误<br>故障到失败之间的中间状态]
Fa[Failure<br>失败<br>系统未能交付预期服务]
O[Outage<br>停机<br>服务不可用的时间段]
F -->|发展为| E
E -->|表现为| Fa
Fa -->|累积为| O
classDef fault fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef error fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef failure fill:#ffebee,stroke:#c62828,stroke-width:3px
classDef outage fill:#eee,stroke:#495057,stroke-width:2px
class F fault
class E error
class Fa failure
class O outage
| 术语 | 含义 |
|---|---|
| 故障源(Fault) | 失败的原因——一段坏代码、一根松动的网线、一个手抖的运维 |
| 错误(Error) | 故障发生到失败显现之间的中间状态——已经偏离正确,但还没失败 |
| 失败(Failure) | 系统未能交付预期服务的可观察事件 |
| 停机(Outage) | 服务不可用的时间段——可用性度量的最终对象 |
可用性工作的核心是通过缓解故障源(Fault)来减少停机时间。系统的每个部分都可能有故障源,但故障不一定立即导致失败——有错误恢复机制就能把「故障 → 错误 → 失败」这条链条切断在中间环节。
为失败做准备
成熟的工程实践提供了几种系统化分析故障的方法。
风险分析(Hazard Analysis)按严重性给故障分级:
- 灾难性 / 危险(Catastrophic / Hazardous)——人员伤亡、重大经济损失
- 重大 / 轻微(Major / Minor)——影响业务但不危险
- 无影响(No Effect)——可以忽略
故障树分析(Fault Tree Analysis, FTA)用布尔逻辑树把顶层失败拆分到底层故障源。比如「系统 D 失败」可能是「A 失败」与「B 或 C 失败」共同导致——这棵树画出来,就能逆推哪些组件必须冗余。
flowchart TD
D[D 失败]
G1{AND}
A[A 失败]
G2{OR}
B[B 失败]
C[C 失败]
D --> G1
G1 --> A
G1 --> G2
G2 --> B
G2 --> C
classDef top fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef gate fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef leaf fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
class D top
class G1,G2 gate
class A,B,C leaf
FMECA(Failure Mode, Effects, and Criticality Analysis,失效模式、影响及关键性分析)则反过来——从每个组件出发,列出它的失效模式(Open / Short / Other)、各模式发生的概率、以及每种模式对系统的影响是关键的还是非关键的。这个方法依赖类似系统的历史失效数据。
可用性通用场景
把上面这些放进六要素的场景模板:
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 内部 / 外部:人、硬件、软件、物理基础设施、物理环境 |
| 刺激 | 故障:遗漏(omission)、崩溃(crash)、错误时序(incorrect timing)、错误响应(incorrect response) |
| 构件 | 处理器、通信通道、持久存储、进程 |
| 环境 | 正常运行、启动、关机、修复模式、降级运行、过载运行 |
| 响应 | 阻止故障变为失败;检测故障(记录、通知);从故障中恢复(禁用故障源、暂时不可用、修复或屏蔽、降级运行) |
| 响应度量 | 系统必须可用的时长 / 可用性百分比(如 99.999%)/ 检测时间 / 修复时间 / 降级运行的时长 / 可被处理或预防的故障比例 |
一个可用性场景的具体例子:
可用性场景示例
- 刺激源:心跳监控器(Heartbeat Monitor)
- 刺激:发现服务器无响应
- 构件:业务进程
- 环境:正常运行
- 响应:通知运维人员,并继续运行
- 响应度量:系统应在指定时间内仍能服务请求
可用性战术的全貌
可用性战术围绕「故障」这个中心组织起来——你要么检测故障、要么从故障恢复、要么预防故障:
flowchart TD
F[故障到达]
M[故障被屏蔽<br>或被修复]
F --> A[Availability Tactics]
A --> D[检测故障<br>Detect Faults]
A --> R[从故障恢复<br>Recover from Faults]
A --> P[预防故障<br>Prevent Faults]
D --> D1[Ping/Echo]
D --> D2[Heartbeat]
D --> D3[Exception]
D --> D4[Voting]
D --> D5[Self-Test]
R --> R1[准备与修复<br>Preparation and Repair]
R --> R2[重新引入<br>Reintroduction]
R1 --> R1a[Active Redundancy]
R1 --> R1b[Passive Redundancy]
R1 --> R1c[Spare]
R1 --> R1d[Rollback]
R1 --> R1e[Retry / Degradation]
R2 --> R2a[Shadow]
R2 --> R2b[State Resync]
R2 --> R2c[Escalating Restart]
R2 --> R2d[Non-Stop Forwarding]
P --> P1[Removal from Service]
P --> P2[Transactions]
P --> P3[Predictive Model]
P --> P4[Exception Prevention]
A --> M
classDef root fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef cat fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef sub fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef tac fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class A root
class D,R,P cat
class R1,R2 sub
class D1,D2,D3,D4,D5,R1a,R1b,R1c,R1d,R1e,R2a,R2b,R2c,R2d,P1,P2,P3,P4 tac
class F,M io
检测故障
故障检测是恢复或预防的前提——感知不到的故障无法处置。常见战术:
- Ping/Echo:一个组件向另一个发送 ping,期望在预定时间内收到 echo。常用于一组共同负责一个任务的组件之间——任何成员长时间不回应,就被怀疑有问题
- Heartbeat(心跳,又称 dead man timer):一个组件周期性主动发出心跳,另一个组件被动监听。心跳消息可以顺便携带数据。一旦心跳停了,源组件被假定失败,故障修复组件被通知
- Exception(异常):通过异常机制识别故障。异常处理器通常运行在引入异常的同一进程中——这是它和上面两个的区别:ping/echo 与 heartbeat 跨进程,exception 在进程内
- Voting(投票):在冗余处理器上跑相同输入,将结果送给一个投票器(Voter)。如果某个进程的结果偏离多数,它被判定故障
- Self-Test(自检):组件主动跑一段诊断代码,自我验证
「为什么 Heartbeat 比 Ping/Echo 常见?」——因为 Heartbeat 的发起方就是被监控者本身,不需要监控方主动发起请求,对网络压力小。
从故障恢复 — 准备与修复
故障一旦被检测到,下一步是把系统拉回正常状态。这一类战术分两个子集:准备与修复(让系统切回好状态)和重新引入(把修好的故障组件放回系统)。
准备与修复类战术:
- 主动冗余(Active Redundancy):所有冗余组件并行响应同一事件——它们处于相同状态。只采用其中一个的响应,其他丢弃。一旦某个失败,停机时间几乎为零——备份是当前状态,切换只需要几个时钟周期
- 被动冗余(Passive Redundancy):一个组件(主, primary)响应事件,并把状态更新告诉其他副本(从, secondary)。失败时系统得先确认从节点状态足够新,才能恢复服务——比主动冗余慢,但代价低
- 备件(Spare):一个待命的备用计算平台,配置成能替代多种失败组件——便宜但切换慢
- Rollback(回滚):把系统状态退回到一个已知良好的检查点
- Retry(重试):对偶发故障,重试是简单有效的恢复
- Degradation(降级):在修复期间以降低的功能水平继续运行——比如电商支付坏了仅展示商品但不能下单,比整体宕机要好
- Reconfiguration(重配置):在修复期间重新分配资源
从故障恢复 — 重新引入
一个组件修好了之后并不能直接「热接」回系统——它的状态可能落后了几秒甚至几分钟。重新引入类战术处理这个问题:
- Shadow(影子运行):先前失败的组件被恢复后,先在「影子模式」里运行一段时间,模仿正常组件的行为,确保正确无误后再放回服务
- State Resynchronization(状态再同步):被动冗余和主动冗余战术都需要这个——恢复中的组件先把状态升级到当前,才能回到服务
- Checkpoint/Rollback(检查点 / 回滚):检查点是系统一致状态的定期或事件触发的记录;如果出错,可以把系统回滚到最近的检查点
预防故障
最理想的做法是在故障发生之前就处理它:
- Removal from Service(停服维护):在预期失败前,把组件主动从系统中拿掉做维护——典型如重启
- Transactions(事务):把多个连续步骤捆绑起来,使整个束可以一次性回滚——这就是数据库事务在可用性语境下的解读
- Process Monitor(进程监视器):一旦检测到进程故障,监视器创建一个新实例替代它——和「备件」战术联动
可用性的设计检查清单
把这些战术按七大设计决策类别铺开,得到可用性的设计检查清单。这种清单的价值在于评审时可以逐项对照:
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 哪些职责需要高可用?是否分配了「检测、记录、通知、禁用、降级、屏蔽、修复」相应的额外职责? |
| 协调模型 | 协调机制能否检测到故障?能否在通信降级、启停、过载下工作?是否支持构件替换(如服务器替换不停机)? |
| 数据模型 | 哪些数据抽象的故障会导致系统失败?这些抽象在故障时能否被禁用、暂不可用、屏蔽、修复?(例如服务器临时不可用时缓存写请求,恢复后回放) |
| 架构元素映射 | 哪些构件可能产生故障?映射是否足够灵活以支持恢复?(如失败处理器上的进程能否动态重分配) |
| 资源管理 | 故障发生时是否有足够的剩余资源来记录、通知、屏蔽、降级?关键资源在指定时间区间内是否一定可用? |
| 绑定时间 | 故障组件的替换时机——运行时?热插拔? |
| 技术选择 | 选用的语言 / 中间件是否支持上述战术?是否有成熟的高可用框架? |
互操作性
"Charlene said that Kim told her that Trevor heard that Heather wants to come to your party."
这是教材里讲互操作性时引用的一句话——信息从一方传到另一方,每经过一道转手,原意都可能走样。两个软件系统之间的对话也面临同样的问题。
互操作性
互操作性(Interoperability)刻画两个或多个系统在特定上下文下,能否通过接口有意义地交换信息的程度。
互操作性可以再细分两个层次:
- 语法互操作性(Syntactic Interoperability):能交换数据——双方对消息格式、字段类型有共识
- 语义互操作性(Semantic Interoperability):能正确解读数据——双方对每个字段的含义有共识
举个例子:A 系统送来一个 JSON {"price": 1000},B 系统能解析它(语法互操作);但 A 用的是元、B 以为是分(语义不互操作),下单时金额就错了一百倍。语法层面相对容易,语义层面才是真正的难关。
互操作性还和「和谁、和什么、在什么情况下」(with whom, with what, under what circumstances)这三件事强绑定——脱离上下文谈互操作性没有意义。
互操作性的两个核心方面
互操作性的工程实现围绕两件事展开:
| 方面 | 含义 |
|---|---|
| 服务发现(Discovery) | 服务消费者必须找到服务的位置、身份、接口 |
| 响应处理(Handling of the response) | 服务接收到请求后,要么向请求者回报,要么转发给其他系统,要么向所有感兴趣的方广播 |
互操作性通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 一个系统发起与另一个系统互操作的请求 |
| 刺激 | 在系统间交换信息的请求 |
| 构件 | 希望互操作的系统们 |
| 环境 | 互操作系统在运行时被发现,或在运行前已知 |
| 响应 | 请求被适当地拒绝并通知合适实体;或被适当接受并成功交换信息;并由参与方之一记录日志 |
| 响应度量 | 信息交换被正确处理的百分比;信息交换被正确拒绝的百分比 |
互操作性场景示例
- 刺激源:本车的车载信息系统
- 刺激:发起一次信息交换请求
- 环境:互操作系统在运行前已知
- 响应:交通监控系统将本车当前位置与其他信息合并,覆盖在 Google 地图上,并对外广播
- 响应度量:本车信息被正确包含的比例 ≥ 99.9%
互操作性战术
flowchart TD
R[Request to Exchange Information<br>请求]
H[Request Correctly Handled<br>正确处理]
R --> IT[Interoperability Tactics]
IT --> H
IT --> L[Locate<br>定位]
IT --> M[Manage Interfaces<br>管理接口]
L --> L1[Discover Service<br>服务发现]
M --> M1[Orchestrate<br>编排]
M --> M2[Tailor Interface<br>裁剪接口]
classDef root fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cat fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tac fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class IT root
class L,M cat
class L1,M1,M2 tac
class R,H io
- 服务发现(Discover Service):通过查询已知的目录服务来定位一个服务。可以有多级间接——通过 DNS 找注册中心,再通过注册中心找具体服务实例
- 编排(Orchestrate):用一个控制机制协调、管理、并按序调用多个特定服务——典型的微服务编排器(如工作流引擎)就是这样工作
- 裁剪接口(Tailor Interface):在不修改原接口实现的前提下,通过添加或移除能力来定制接口——比如在网关层加一层鉴权或限流,就是裁剪后的接口
服务发现解决「找得到」的问题,编排和裁剪解决「叫得动」「能定制」的问题。这三个战术几乎对应了所有现代微服务平台的核心能力:服务注册中心、API 网关、编排引擎。
互操作性的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 哪些职责需要与外部系统互操作?是否分配了「检测请求、接受、交换、拒绝、通知、记录(不可抵赖性)」职责? |
| 协调模型 | 协调机制能否满足关键性能 / 可用性 / 安全需求(流量、时效、新鲜度、抖动)?协议假设是否与不受控的外部系统一致? |
| 数据模型 | 互操作交换的核心数据抽象的语法和语义是什么?是否需要数据模型转换(如保密数据需脱敏)? |
| 架构元素映射 | 对外通信的组件是否分配在能访问网络的处理器上? |
| 资源管理 | 拒绝 / 接受请求时是否会耗尽资源?通信的资源开销是否可控?共享资源的仲裁策略是什么? |
| 绑定时间 | 互操作的对方系统是设计时已知,还是运行时发现?是否有处理已知 / 未知绑定的策略? |
| 技术选择 | 所选技术(如 Web Services)是否暴露在接口边界?是否有助于满足互操作性场景? |
可修改性
一个系统大约 80% 的成本发生在部署之后——上一节的这句话,就是可修改性的根本动因。
可修改性
可修改性(Modifiability)关心系统变更的成本——时间、金钱、对其他功能或质量属性的影响。
准备变更也有成本,做出变更也有成本。架构师要在两者之间做权衡。
计划变更的四个问题
回答以下四个问题,能让模糊的「我们需要灵活的系统」变成可分析的工程问题:
- 什么会变?(What can change?)——功能、容量、技术、质量属性?
- 变化的可能性有多大?(What is the likelihood of the change?)
- 什么时候变、谁来变?(When is the change made and who makes it?)
- 变更的成本是多少?(What is the cost of the change?)
准备变更的成本权衡公式
如果未来变更次数比预期少很多,过早准备变更机制反而是浪费。可以用一个简单的不等式刻画这个权衡:
其中:
- 是预计的变更次数
- 是没有可修改性机制时单次变更的成本
- 是引入机制的一次性成本
- 是有了机制后单次变更的成本
如果左边 ≥ 右边,说明引入机制划算;否则就是过度设计。这个公式不是用来精确算钱的,而是提醒架构师不要为了「以防万一」预设过多扩展点——每一个抽象层、每一个配置文件、每一个插件接口,都是要还的债。
可修改性通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 终端用户、开发者、系统管理员 |
| 刺激 | 添加 / 删除 / 修改功能,或改变质量属性、容量、技术 |
| 构件 | 代码、数据、接口、组件、资源、配置 |
| 环境 | 运行时、编译时、构建时、初始化时、设计时 |
| 响应 | 修改、测试、部署 |
| 响应度量 | 受影响构件的数量 / 大小 / 复杂度;工时;日历时间;金钱;对其他功能或质量属性的影响;引入的新缺陷 |
可修改性场景示例
- 刺激源:开发者
- 刺激:希望修改 UI
- 构件:代码(UI 层)
- 环境:设计时
- 响应:修改完成并通过单元测试
- 响应度量:3 小时内完成
可修改性战术
可修改性战术围绕「限制变更的传播范围」展开:让一次修改影响尽可能少的模块。
flowchart TD
CA[Change Arrives<br>变更到达]
CM[Change Made within Time and Budget<br>在时间和预算内完成]
CA --> MT[Modifiability Tactics]
MT --> CM
MT --> S[减小模块大小<br>Reduce Size]
MT --> C[增加内聚<br>Increase Cohesion]
MT --> R[降低耦合<br>Reduce Coupling]
MT --> D[推迟绑定<br>Defer Binding]
S --> S1[Split Module<br>拆分模块]
C --> C1[Increase Semantic Coherence<br>增加语义一致性]
R --> R1[Encapsulate<br>封装]
R --> R2[Use an Intermediary<br>使用中介]
R --> R3[Restrict Dependencies<br>限制依赖]
R --> R4[Refactor<br>重构]
R --> R5[Abstract Common Services<br>抽取共有服务]
classDef root fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cat fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tac fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class MT root
class S,C,R,D cat
class S1,C1,R1,R2,R3,R4,R5 tac
class CA,CM io
- 拆分模块(Split Module):如果被修改的模块包含太多能力,修改成本会很高——把它拆成更小的模块,让单次修改只影响其中一个
- 增加语义一致性(Increase Semantic Coherence):一个模块里两个职责 A、B 不为同一目的服务,就应该分到不同模块——这正是 SRP(单一职责原则)在架构层的回响
- 封装(Encapsulate):给模块引入显式接口,降低一处变更传播到其他模块的概率——这是面向对象设计的基本功
- 使用中介(Use an Intermediary):在两个高耦合模块之间放一个中介,切断直接依赖。中介可以是事件总线、消息队列、依赖注入容器
- 限制依赖(Restrict Dependencies):明确禁止某些方向的依赖——比如「业务层不能直接访问数据访问层」
- 重构(Refactor):当两个模块被同一类变更同时影响时,是它们应该被合并或重新切分的信号
- 抽取共有服务(Abstract Common Services):把多个模块的相同部分抽到一个共享服务里
- 推迟绑定(Defer Binding):把某些参数的取值时机推到生命周期的更晚阶段——设计时变成编译时、编译时变成运行时。配置文件、插件、依赖注入都是推迟绑定的具体形式
推迟绑定 vs 准备成本
推迟绑定虽然能让系统「更灵活」,但每多一个推迟绑定点都会增加初始化和调试复杂度。回到上面那个 的公式——只有当该参数确实会被频繁改变时,推迟绑定才划算。
可修改性的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 可能发生哪些变更(技术、法律、社会、商业、客户)?将同时变化的职责放在同一模块,将不同时变化的放在不同模块 |
| 协调模型 | 哪些功能 / 质量属性可能在运行时变?是否使用降低耦合的协调模型(发布订阅)、推迟绑定的(企业服务总线 ESB)、限制依赖的(广播) |
| 数据模型 | 哪些数据抽象、操作、属性可能变?哪些会涉及创建、初始化、持久化、操作、转换、销毁?由谁来变? |
| 架构元素映射 | 是否需要在运行时 / 编译时 / 设计时 / 构建时改变映射方式?是否使用了推迟绑定的映射机制 |
| 资源管理 | 添加 / 删除 / 修改职责会如何影响资源使用?资源管理器是否被封装?资源策略是否被推迟绑定 |
| 绑定时间 | 对每种变更,确定最迟的绑定时机。引入推迟绑定的成本是否值得?不要引入太多互相依赖的绑定选择 |
| 技术选择 | 哪些技术让修改更容易 / 更难?技术本身可被替换吗?例如 ESB 让连接易变,但可能引入厂商锁定 |
性能
「这个按钮反应有点慢。」——一句几乎所有产品经理都听过的话,背后是性能这个最古老、也最朴素的质量属性。
性能
性能(Performance)关注时间——软件系统满足时序需求的能力。
所有系统都有性能需求,即便没有显式写出来。
性能在响应时间这个核心维度上,可以拆成两类时间:
- 处理时间(Processing Time):系统正在积极工作来响应——CPU 计算、数据库查询、文件 I/O
- 阻塞时间(Blocked Time):系统无法响应——等待资源、等待锁、等待网络、被调度
很多性能问题不在处理太慢,而在阻塞过多——一个数据库连接池耗尽,所有请求都等在那里,CPU 空转,但响应时间飙升。优化性能往往是在「让阻塞变成处理」:用异步 I/O 把等 I/O 的时间用来计算,用并发把等锁的进程切去做别的事。
性能通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 系统内部或外部 |
| 刺激 | 一个周期性、偶发或随机事件的到达(periodic / sporadic / stochastic) |
| 构件 | 系统或系统中的若干组件 |
| 环境 | 运行模式:正常、紧急、峰值负载、过载 |
| 响应 | 处理事件,或改变服务等级 |
| 响应度量 | 延迟(latency)、截止期(deadline)、吞吐量(throughput)、抖动(jitter)、未达率(miss rate) |
「事件到达模式」这一栏值得展开:
- 周期性:每隔固定时间到达——传感器采样、定时任务
- 偶发:不可预测但有节奏——用户操作、外部告警
- 随机:服从某种概率分布——电商访问、网络包
不同到达模式需要不同的设计。周期性事件用调度算法处理,随机事件用队列论模型分析。
性能场景示例
- 刺激源:用户
- 刺激:发起多笔交易
- 环境:正常运行
- 响应:处理交易
- 响应度量:平均延迟 ≤ 2 秒
性能战术:需求侧 vs 资源侧
性能优化的两条路:要么少干活(控制资源需求),要么多上人(管理资源)。
flowchart TD
EA[Event Arrives<br>事件到达]
GR[Response Generated within Time Constraints<br>在时间约束内响应]
EA --> PT[Performance Tactics]
PT --> GR
PT --> D[控制资源需求<br>Control Resource Demand]
PT --> M[管理资源<br>Manage Resources]
D --> D1[Manage Sampling Rate<br>管理采样率]
D --> D2[Limit Event Response<br>限制事件响应]
D --> D3[Prioritize Events<br>优先级]
D --> D4[Reduce Overhead<br>减少开销]
D --> D5[Bound Execution Times<br>限定执行时间]
D --> D6[Increase Resource Efficiency<br>提升资源效率]
M --> M1[Increase Resources<br>增加资源]
M --> M2[Introduce Concurrency<br>引入并发]
M --> M3[Maintain Multiple Copies of Computations<br>计算多副本]
M --> M4[Maintain Multiple Copies of Data<br>数据多副本]
M --> M5[Bound Queue Sizes<br>限制队列长度]
M --> M6[Schedule Resources<br>调度资源]
classDef root fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cat fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tac fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class PT root
class D,M cat
class D1,D2,D3,D4,D5,D6,M1,M2,M3,M4,M5,M6 tac
class EA,GR io
控制资源需求(需求侧)
- 管理采样率(Manage Sampling Rate):降低采样频率——传感器系统里最直接的减负方式。每秒采 1000 次降到 100 次,工作量直降 10 倍
- 限制事件响应(Limit Event Response):当离散事件到达太快无法即时处理时,把事件排队——这就是消息队列、缓冲池存在的理由
- 事件优先级(Prioritize Events):当不是所有事件都同等重要时,给重要事件优先权——支付请求优先于日志上报
- 减少开销(Reduce Overhead):用中间者来增加事件处理流水线上的资源——例如缓存命中减少了下游计算
- 限定执行时间(Bound Execution Times):给每个操作设上限,避免少数慢操作拖垮整体——这就是熔断器和超时机制的原理
- 提升资源效率(Increase Resource Efficiency):优化算法、减少内存分配、消除锁竞争——传统意义上的「调优」
管理资源(资源侧)
- 增加资源(Increase Resources):更快的处理器、更多内存、更快的网络——最直接但最贵
- 引入并发(Introduce Concurrency):如果请求可以并行处理,就开多个线程或协程——前提是任务可分,且开销小于并行收益
- 维护计算多副本(Maintain Multiple Copies of Computations):用负载均衡器把新工作分配给一组等价的服务器——这是无状态服务横向扩展的本质
- 维护数据多副本(Maintain Multiple Copies of Data):
- 缓存(Caching):把热数据放在更快的存储层
- 数据复制(Data Replication):把数据放在多个节点上
- 限制队列长度(Bound Queue Sizes):避免队列无限增长导致内存爆掉——队列必须有上限和淘汰策略
- 调度资源(Schedule Resources):选用合适的调度算法(FIFO、SJF、优先级)来满足时序需求
性能战术与可用性战术的张力
增加资源、维护多副本既能提升性能也能提升可用性,但「数据多副本」会引入一致性问题——多个副本的状态如何同步?同步成本多大?这就是 CAP 定理在战术层面的回响。
战术之间相互影响——这正是「权衡」的本质。
性能的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 哪些职责承担重负载、时间关键、瓶颈所在?这些职责是否分配了管理线程、调度共享资源、监控队列 / 缓冲 / 缓存的额外职责? |
| 协调模型 | 通信机制能否支持引入的并发(线程安全?)、事件优先级、调度策略?是否能捕获周期性 / 随机 / 偶发事件?同步还是异步?是否保证投递? |
| 数据模型 | 哪些数据抽象在重负载路径上?多副本能否提升性能?数据分区是否有用?能否减少创建 / 持久化 / 转换开销? |
| 架构元素映射 | 在哪些位置共置组件能减少网络负载?计算密集组件是否分配在最强处理器上?引入并发是否可行且有效? |
| 资源管理 | 哪些资源对性能关键?正常和过载时是否被监控管理?是否有按需扩容机制? |
| 绑定时间 | 编译时之后绑定的元素,绑定耗时是多少?引入的额外开销是否可接受? |
| 技术选择 | 所选技术能否设定调度策略 / 优先级 / 降需求策略 / 资源分配?技术在重负载下的特征和上限是否清楚? |
安全性
「攻击者不一定要破坏什么——光是看到不该看的东西,就已经是失败了。」
安全性
安全性(Security)衡量系统在仍向授权方提供访问的同时,保护数据和信息免受未授权访问的能力。
CIA 三性
安全性的经典刻画是 CIA 三性:
| 性质 | 含义 |
|---|---|
| 机密性(Confidentiality) | 数据和服务受到保护,不被未授权访问 |
| 完整性(Integrity) | 数据和服务不受未授权修改 |
| 可用性(Availability) | 系统对合法用户可用 |
注意 CIA 的 A 是可用性——这与上面单独讨论的可用性是同一个概念。可用性既是一个独立的质量属性,也是安全性的一个子属性——一个被 DDoS 打垮的系统,从安全视角看也是失败的。
flowchart TD
S[安全性 Security]
S --> C[机密性<br>Confidentiality]
S --> I[完整性<br>Integrity]
S --> A[可用性<br>Availability]
C -.防止未授权访问.-> D[数据 / 服务]
I -.防止未授权修改.-> D
A -.保证合法访问.-> D
classDef root fill:#ffebee,stroke:#c62828,stroke-width:3px
classDef prop fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef target fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
class S root
class C,I,A prop
class D target
安全性通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 已知 / 未知的人或系统;攻击者可能来自组织外或内 |
| 刺激 | 未授权地显示数据、修改 / 删除数据、访问服务、改变行为、降低可用性 |
| 构件 | 系统服务、系统数据、组件 / 资源、产生 / 消费的数据 |
| 环境 | 在线 / 离线;联网 / 断网;防火墙后 / 暴露在网络;完全 / 部分 / 不可用 |
| 响应 | 数据 / 服务受到保护免遭未授权访问;不被未授权操纵;交易方可识别且不可抵赖(non-repudiation);合法访问可用;记录访问、修改、攻击企图;通知合适实体 |
| 响应度量 | 单点泄露时多少系统被攻陷;攻击被检测到的时间;抵御了多少次攻击;从成功攻击中恢复的时间;多少数据易受特定攻击 |
「不可抵赖性」(Non-repudiation)这一项常被忽略——它要求交易双方都不能事后否认参与,这通常通过数字签名和审计日志实现。
安全性场景示例
- 刺激源:远程的员工
- 刺激:尝试修改自己的薪资
- 构件:薪资数据
- 环境:正常运行
- 响应:阻止修改并记录
- 响应度量:正确数据在 1 天内恢复,且攻击源被识别
安全性战术
安全战术按「检测 → 抵抗 → 响应 → 恢复」四阶段组织:
flowchart TD
A[Attack<br>攻击]
DR[System Detects, Resists, Reacts, or Recovers<br>系统检测、抵抗、响应或恢复]
A --> ST[Security Tactics]
ST --> DR
ST --> D[检测攻击<br>Detect]
ST --> R[抵抗攻击<br>Resist]
ST --> RC[响应攻击<br>React]
ST --> RV[恢复<br>Recover]
D --> D1[Detect Intrusion<br>入侵检测]
D --> D2[Detect Service Denial<br>拒绝服务检测]
D --> D3[Verify Message Integrity<br>消息完整性验证]
D --> D4[Detect Message Delay<br>消息延迟检测]
R --> R1[Identify Actors<br>识别角色]
R --> R2[Authenticate<br>认证]
R --> R3[Authorize<br>授权]
R --> R4[Limit Access<br>限制访问]
R --> R5[Limit Exposure<br>限制暴露]
R --> R6[Encrypt Data<br>加密]
R --> R7[Separate Entities<br>分离实体]
R --> R8[Change Default<br>变更默认]
RC --> RC1[Revoke Access<br>吊销访问]
RC --> RC2[Lock Computer<br>锁定]
RC --> RC3[Inform Actors<br>通知角色]
RV --> RV1[Maintain Audit Trail<br>审计日志]
RV --> RV2[Restore<br>+可用性战术]
classDef root fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef cat fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef tac fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class ST root
class D,R,RC,RV cat
class D1,D2,D3,D4,R1,R2,R3,R4,R5,R6,R7,R8,RC1,RC2,RC3,RV1,RV2 tac
class A,DR io
检测攻击
- 入侵检测(Detect Intrusion):将网络流量或服务请求模式与一组特征码或已知模式对照——典型的 IDS / IPS 系统
- 拒绝服务检测(Detect Service Denial):发现异常的高流量或耗尽资源的请求模式
- 消息完整性验证(Verify Message Integrity):用校验和或哈希值判断消息是否被篡改
- 消息延迟检测(Detect Message Delay):异常的延迟可能意味着中间人攻击
抵抗攻击
- 识别角色(Identify Actors):识别所有外部输入的来源——是谁在敲门?
- 认证(Authenticate Actors):证实角色的身份是否如其所称——通过密码、密钥、生物特征等
- 授权(Authorize Actors):验证已认证的角色是否有权访问或修改特定资源——区分「你是谁」和「你能做什么」
- 限制访问(Limit Access):对计算资源的访问加边界——防火墙、网络分段
- 限制暴露(Limit Exposure):最小化系统的攻击面——只开必要端口,不暴露多余的接口
- 加密数据(Encrypt Data):传输和静态数据都要考虑
- 分离实体(Separate Entities):把敏感组件隔离在独立的进程 / 主机 / 网络里
- 变更默认设置(Change Default Settings):默认密码、默认端口是攻击者首选目标
响应攻击
- 吊销访问(Revoke Access):在攻击进行时,对敏感资源吊销访问权——把可疑账号锁掉
- 锁定(Lock Computer):多次失败登录后锁住主机
- 通知角色(Inform Actors):通知运维、安全团队、其他系统
恢复
- 维护审计日志(Maintain Audit Trail):完整记录访问和修改历史,用于事后追溯——这也是不可抵赖性的支撑
- 恢复(Restore):与可用性战术合用——把数据回滚到可信状态
安全性的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 哪些职责需要安全保障?是否分配了「识别、认证、授权、授予 / 拒绝访问、记录、加密、识别可用性下降、恢复、校验和验证」职责? |
| 协调模型 | 与外部系统通信时,是否有认证 / 授权 / 加密机制?是否能监控异常的高需求? |
| 数据模型 | 不同敏感度的数据是否分离?访问权限是否在访问前被检查?敏感数据是否加密、密钥是否与密文分离?数据被错误修改时能否还原? |
| 架构元素映射 | 不同的映射会如何改变攻击面?是否能识别并记录可疑访问? |
| 资源管理 | 外部实体能否耗尽关键资源?关键安全操作的资源是否充足?被污染的元素能否被隔离?共享资源是否会成为越权传递数据的通道? |
| 绑定时间 | 后期绑定的组件可能不可信——是否有机制对它们做资格审查、撤销访问、记录调用? |
| 技术选择 | 所选技术是否支持身份认证、访问控制、资源保护、数据加密?是否支持当前需要的所有战术? |
可测试性
「这段代码我也不确定写对了没——反正测了感觉没问题。」——这种话出现的时候,往往就是可测试性已经失守的信号。
可测试性
可测试性(Testability)刻画软件能否容易地通过测试(通常是基于执行的测试)暴露其缺陷。
一个系统要被恰当地测试,必须能控制每个组件的输入,并观察其输出。
「能控制 + 能观察」这八个字是可测试性的核心:
flowchart LR
I[输入<br>Input]
P[Program<br>程序]
IS[内部状态<br>Internal State]
O[输出<br>Output]
Or{Oracle<br>判定器}
A[approved<br>通过]
Re[rejected<br>拒绝]
I --> P
P --> IS
IS --> O
O --> Or
Or --> A
Or --> Re
classDef io fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef prog fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef oracle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef result fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class I,IS,O io
class P prog
class Or oracle
class A,Re result
「Oracle」(神谕,判定器)是测试理论里的术语——一个能告诉你「这个输出对不对」的东西。它可以是手工编写的断言、可以是参考实现、可以是用户的眼睛。没有 Oracle 就没有测试。
可测试性通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 单元 / 集成 / 系统 / 验收测试人员;终端用户;手工执行 / 自动化测试工具 |
| 刺激 | 由于完成了一段编码(一个类、一个服务)、一个子系统的集成、整个系统实现、或交付,触发了一组测试 |
| 环境 | 设计时、开发时、编译时、集成时、部署时、运行时 |
| 构件 | 被测的系统部分 |
| 响应 | 执行测试套件并捕获结果;捕获导致故障的活动;控制和监控系统状态 |
| 响应度量 | 找到一个 / 一类故障所需的工时;达到指定状态空间覆盖率所需的工时;下一个测试发现故障的概率;测试时间;故障检测工时;测试中最长依赖链长度;测试环境准备时间;风险敞口的下降(size(loss) × prob(loss)) |
「风险敞口」(risk exposure)这个度量很微妙——它不是直接衡量「测了多少」,而是衡量「测试让潜在损失的期望下降了多少」。这个思路把测试从「合规活动」拉回了「风险管理」。
可测试性场景示例
- 刺激源:开发者完成一段代码
- 构件:代码单元
- 环境:开发时
- 响应:捕获测试结果
- 响应度量:3 小时内达到 85% 路径覆盖率
可测试性战术
可测试性战术围绕「控制 + 观察 + 限制复杂度」三个支点:
flowchart TD
TE[Test Executed<br>测试执行]
FD[Faults Detected<br>故障检测到]
TE --> TT[Testability Tactics]
TT --> FD
TT --> CO[控制和观察系统状态<br>Control and Observe]
TT --> LC[限制复杂度<br>Limit Complexity]
CO --> CO1[Specialized Interfaces<br>专用接口]
CO --> CO2[Record/Playback<br>录制 / 回放]
CO --> CO3[Localize State Storage<br>状态存储本地化]
CO --> CO4[Abstract Data Sources<br>抽象数据源]
CO --> CO5[Sandbox<br>沙箱]
CO --> CO6[Executable Assertions<br>可执行断言]
LC --> LC1[Limit Structural Complexity<br>限制结构复杂度]
LC --> LC2[Limit Nondeterminism<br>限制非确定性]
classDef root fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cat fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tac fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class TT root
class CO,LC cat
class CO1,CO2,CO3,CO4,CO5,CO6,LC1,LC2 tac
class TE,FD io
控制和观察系统状态
核心是:维护某种系统状态信息,让测试者能给状态赋值、并能按需访问。
- 专用接口(Specialized Interfaces):给组件加一些专门给测试用的接口——直接读写内部状态、注入故障。这些接口在生产环境可能被关闭
- 录制 / 回放(Record / Playback):录下导致失败的状态,重现失败——这是诊断难复现 bug 的常用武器
- 状态存储本地化(Localize State Storage):把状态集中到少数几处——而不是散落各模块。集中后状态注入更容易
- 抽象数据源(Abstract Data Sources):通过抽象接口访问数据,测试时替换为可控数据源——这就是 Mock 和 Stub 的理论依据
- 沙箱(Sandbox):把系统的一个实例从真实世界中隔离,进行实验,并撤销实验后果——容器、虚拟机、内存数据库都是沙箱
- 可执行断言(Executable Assertions):把不变量直接写进代码——
assert(x > 0),违反时立即报错
限制复杂度
复杂软件难测,因为状态空间太大、难以重现某个状态。限制复杂度的战术:
- 限制结构复杂度(Limit Structural Complexity):
- 避免、减少或解决组件间依赖
- 隔离和封装对外部环境的依赖
- 限制类继承的来源数——避免一个类继承自太多基类
- 限制继承树的深度和子类数量
- 限制多态和动态调用——这些会让测试覆盖率分析变难
- 限制非确定性(Limit Nondeterminism):非确定性系统更难测——同样的输入可能给不同输出。能用固定种子的随机就不要用真随机;能用同步就不要默认开异步;能避免就避免线程竞争
可测试性是设计决策的副作用
严格说来,没有「专门为可测试性设计的代码」这种东西——一个高内聚、低耦合、低复杂度、依赖明确的系统,本身就是好测试的。这就是为什么前十节讲的设计原则(SOLID、DRY)对可测试性贡献巨大。
可测试性的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 哪些职责最关键、需要最彻底的测试?是否分配了「执行测试套件、捕获日志、控制和观察系统状态」职责?是否实现了高内聚、低耦合、关注点分离、低结构复杂度? |
| 协调模型 | 协调机制是否支持执行测试、捕获活动、注入和监控状态?是否引入了不必要的非确定性? |
| 数据模型 | 数据抽象的实例值是否能被捕获和注入?数据生命周期能否被演练? |
| 架构元素映射 | 进程到处理器、线程到进程、模块到组件的映射是否可测?能否检测非法映射(如违反约束的部署)? |
| 资源管理 | 是否有足够资源执行测试套件?测试环境是否与生产环境一致?能否注入资源限制做测试?是否提供虚拟化资源? |
| 绑定时间 | 后期绑定的组件能否在后期绑定上下文中被测试?能否捕获绑定信息以重现故障? |
| 技术选择 | 所选技术是否支持回归测试、故障注入、录制回放?所选技术本身的可测试性如何? |
易用性
一个员工下载一个新应用——能不能在不看说明书的情况下用起来?这是易用性的最直白判定。
易用性
易用性(Usability)关注用户完成期望任务的容易程度,以及系统提供的用户支持类型。
易用性涵盖以下方面:
| 方面 | 含义 |
|---|---|
| 学习系统功能(Learning) | 用户能多快上手 |
| 高效使用(Efficiency) | 能否用最少操作完成任务 |
| 最小化错误影响(Error Impact) | 用户出错时是否能恢复 |
| 适配用户需求(Adaptation) | 能否根据用户偏好调整 |
| 增强信心和满意度(Confidence) | 用户用得是否安心 |
易用性远不只是「界面好不好看」——它也是架构层的属性。一个能撤销操作的应用,需要后端有事务历史;一个能并行操作的应用,需要异步任务模型。易用性的支撑常常在最底层。
易用性通用场景
| 场景部分 | 可能值 |
|---|---|
| 刺激源 | 终端用户(可能是某种特定角色) |
| 刺激 | 用户尝试高效使用系统、学习使用、最小化错误影响、适配系统、配置系统 |
| 环境 | 运行时或配置时 |
| 构件 | 系统或与用户交互的特定部分 |
| 响应 | 系统提供用户所需功能,或主动预测用户需求 |
| 响应度量 | 任务时间;错误数;任务完成数;用户满意度;用户知识增长;成功操作占比;错误时损失的时间 / 数据量 |
易用性场景示例
- 刺激源:用户
- 刺激:下载新应用
- 环境:运行时
- 响应:用户高效使用了应用
- 响应度量:成功完成核心任务,无需查阅文档
易用性战术
易用性战术分两组:支持用户主动和支持系统主动——前者让用户掌控,后者让系统聪明。
flowchart TD
UR[User Request<br>用户请求]
UF[User Given Appropriate Feedback and Assistance<br>用户得到反馈和协助]
UR --> UT[Usability Tactics]
UT --> UF
UT --> SU[支持用户主动<br>Support User Initiative]
UT --> SS[支持系统主动<br>Support System Initiative]
SU --> SU1[Cancel<br>取消]
SU --> SU2[Undo<br>撤销]
SU --> SU3[Pause/Resume<br>暂停 / 恢复]
SU --> SU4[Aggregate<br>聚合]
SS --> SS1[Maintain Task Model<br>任务模型]
SS --> SS2[Maintain User Model<br>用户模型]
SS --> SS3[Maintain System Model<br>系统模型]
classDef root fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef cat fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef tac fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef io fill:#eee,stroke:#495057,stroke-width:2px
class UT root
class SU,SS cat
class SU1,SU2,SU3,SU4,SS1,SS2,SS3 tac
class UR,UF io
支持用户主动
核心是:让用户能在出错时修正,能在等待时更高效。
- 取消(Cancel):用户能终止正在进行的操作——下载到一半发现错了,能停下
- 撤销(Undo):恢复到操作前的状态。要做到这一点,系统必须维护足够的状态来还原——这是为什么撤销功能的代价不小
- 暂停 / 恢复(Pause/Resume):长时间运行的操作(大文件下载)应该可以暂停后再继续
- 聚合(Aggregate):把多个低层对象聚合成一组,对组的操作等于对所有成员的操作——典型如「全选删除」
支持系统主动
核心是:识别系统用来预测自身行为或用户意图的模型。
- 任务模型(Task Model):系统理解用户正在尝试做什么,从而提供帮助——比如输入框知道你在输入电话号码,自动加格式化
- 用户模型(User Model):系统记录用户对系统的了解程度——新手看到详细提示,老手看到精简界面
- 系统模型(System Model):系统知道自己预期的行为,能据此给用户合理反馈——例如长时间任务的进度条估计
易用性的设计检查清单
| 类别 | 关键问题 |
|---|---|
| 职责分配 | 是否分配了协助用户「学习、高效完成任务、适配配置、错误恢复」的职责? |
| 协调模型 | 协调的时效、新鲜度、完整性、正确性、一致性是否影响用户体验?是否能实时响应鼠标事件?长时操作能否取消? |
| 数据模型 | 与用户可见行为相关的数据抽象是否支持撤销 / 取消?事务粒度是否合理(不要让撤销代价过大) |
| 架构元素映射 | 对用户可见的映射(哪些服务本地、哪些远程)会如何影响易用性? |
| 资源管理 | 资源限制下是否仍能让用户高效完成任务? |
| 绑定时间 | 哪些绑定决策应在用户控制下?这些选择是否会影响易用性? |
| 技术选择 | 所选技术是否支持在线帮助、培训资料、用户反馈收集?技术本身的易用性如何? |
架构上显著的需求
讲完了七大质量属性和它们的战术,回到一个根本问题——这么多需求,哪些才真正影响架构走向?
回答这个问题的概念是 ASRs(Architecturally Significant Requirements,架构上显著的需求)。
ASR
一个 ASR 是会对架构产生深远影响的需求——如果没有这个需求,架构可能完全不同。
An ASR is a requirement that will have a profound effect on the architecture.
判断一个需求是不是 ASR 的实操方法:「这个需求会不会影响某个关键架构决策的制定?」如果答案是「会」,那它就是 ASR。
QA 需求越困难、越重要,就越可能是 ASR。一个普通的「系统应该有日志功能」一般不是 ASR——它怎么实现都不会颠覆架构。但一个「系统应该容忍单数据中心的整体故障」就是 ASR——这个一上来就强制了多区域部署、强一致性还是最终一致性的取舍、跨区域同步策略等一系列架构决策。
怎么找到 ASRs
教材给出了四种系统化方法:
flowchart LR
A[ASR 收集]
A --> A1[从需求文档<br>挖掘]
A --> A2[访谈干系人<br>QAW]
A --> A3[理解商业目标<br>Business Goals]
A --> A4[在效用树中<br>组织]
A1 -.质量较低.-> AC[ASR 集合]
A2 --> AC
A3 --> AC
A4 --> AC
classDef root fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef method fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef weak fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef out fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class A root
class A1 weak
class A2,A3,A4 method
class AC out
从需求文档挖掘
最直觉的做法——但教材直接指出这条路常常不可靠:
Whether requirements are specified using the "MoSCoW" style or as a collection of "user stories", neither of these is much help in nailing down quality attributes.
需求文档以两种方式让架构师失望:
第一种失败:大部分需求规格里的内容根本不影响架构。 看看这些典型表述:
- 「系统应当模块化」(The system shall be modular)
- 「系统应当展现高可用性」(The system shall exhibit high usability)
- 「系统应当满足用户的性能期望」(The system shall meet users' performance expectations)
这些都是没有度量、没有场景的口号,对架构毫无指导意义。
第二种失败:对架构师真正有用的内容,反而不在需求文档里。 在采购语境下,需求文档代表的是采购方的兴趣,不是开发方的关注点——后者需要的关于实现约束、内部质量的信息,往往要架构师自己去挖。
教材的精确表述:「如果一个需求会影响某个关键架构决策的制定,按定义它就是 ASR」——这条判据比文档里的措辞更可靠。
通过访谈干系人:质量属性工作坊
第二种方法是质量属性工作坊(Quality Attribute Workshop, QAW)——把所有干系人聚到一起,结构化地引导出 ASRs。
QAW 的标准流程有 8 步:
flowchart TD
Q1[1. QAW 介绍<br>说明流程和目标]
Q2[2. 商业使命陈述<br>项目的商业目标]
Q3[3. 架构计划陈述<br>当前架构思路]
Q4[4. 识别架构驱动因素<br>整体需求 / 商业驱动 / 约束 / 质量属性]
Q5[5. 场景头脑风暴<br>每位干系人提出关注的场景]
Q6[6. 场景合并<br>合并相似场景]
Q7[7. 场景投票<br>优先级排序]
Q8[8. 场景细化<br>深化高优先级场景]
Q1 --> Q2 --> Q3 --> Q4 --> Q5 --> Q6 --> Q7 --> Q8
classDef setup fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef gather fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef refine fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class Q1,Q2,Q3 setup
class Q4,Q5 gather
class Q6,Q7,Q8 refine
QAW 的产出有两件:一份架构驱动因素清单,和一组干系人共同优先级化的 QA 场景。
「头脑风暴 + 投票」这个组合很关键——头脑风暴让边缘但重要的需求浮出水面,投票把分散的关注收敛到大家都同意的少数关键场景。
理解商业目标
干系人嘴上说的需求,往往和真正的商业动机之间有距离。架构师需要往下挖一层:
- 客户为什么要做这个系统?
- 谁付钱?谁用?谁评估?
- 项目失败会让谁失望?项目成功会让谁受益?
理解这些,才能判断哪些质量属性是真正承载商业价值的。例如一家初创公司说「需要高可用」,但深挖商业目标可能发现,他们真正关心的是「第一波用户不要因为偶然的崩溃而流失」——这意味着比起「永不宕机」,「快速恢复 + 良好的错误体验」反而是更准确的目标。
在效用树中组织
效用树(Utility Tree)是把质量属性、子属性、具体场景层层细化的树状结构——SEI 在 ATAM 评估方法里大量使用。
flowchart LR
U[Utility<br>效用]
U --> P[性能]
U --> M[可修改性]
U --> A[可用性]
U --> S[安全性]
P --> P1[数据延迟]
P --> P2[事务吞吐]
P1 --> P1a["(M, L) 客户数据库存储延迟 ≤ 200 ms"]
P1 --> P1b["(H, M) 实时投递视频"]
P2 --> P2a["(M, M) 最大化认证服务器吞吐"]
M --> M1[新产品类别]
M1 --> M1a["(L, H) 加入 CORBA 中间件 < 20 人周"]
M1 --> M1b["(H, L) 提供 web 化功能 < 4 人周"]
A --> A1[硬件失败]
A --> A2[COTS 软件失败]
A1 --> A1a["(L, H) 磁盘失败后 5 分钟内重启"]
A1 --> A1b["(H, M) 网络失败 1.5 分钟内检测恢复"]
S --> S1[数据机密性]
S --> S2[数据完整性]
S1 --> S1a["(L, H) 信用卡交易 99.999% 安全"]
S2 --> S2a["(L, H) 客户数据库授权 99.999% 工作"]
classDef root fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef qa fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef sub fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef sce fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class U root
class P,M,A,S qa
class P1,P2,M1,A1,A2,S1,S2 sub
class P1a,P1b,P2a,M1a,M1b,A1a,A1b,S1a,S2a sce
每个叶子场景上都标了一对字母 (X, Y)——
- 第一项
X是该场景对业务的重要性(H/M/L) - 第二项
Y是架构上实现的难度(H/M/L)
读这棵树时,双 H 的场景就是高价值 ASR——重要又难,架构必须为它们做出深思熟虑的决策;(H, L) 的场景重要但实现简单,可以延后;(L, H) 的场景可能根本不值得花架构资源去满足,应该和干系人重新协商。
效用树的妙处在于:它强迫干系人在有限注意力下做取舍,而不是「我们什么都想要」。
实践中 ASRs 的困境
教材在最后承认了一个尴尬现实:
In practice ASRs (especially NFRs) are often not elicited and are not clearly specified.
很多软件需求规格书根本不包含 NFRs,敏捷项目里 ASR 相关的用户故事也常常缺失。「都是开发到一半才发现性能不行 / 安全有洞 / 可扩展性不够」——这种事情在工业界并不罕见。
那么有没有更好的办法?教材给了一个有意思的答案:基于 Persona 的方法。
基于 Persona 的 ASR 探索
教材以 TraceLab 项目(一个由 NSF 资助 200 万美元的可追溯性研究平台)为例,演示了 Persona-driven 的 ASR 发现方法。
为什么是 Persona
传统 HCI(人机交互)领域的 Persona 构造:调研用户、分类、做使用假设、验证、写场景、设计 Persona——这个流程太重,对一个研究项目来说前期投入过大。
教材给出的折中方案是 Persona Sketches(Persona 草图)——简化版本,但保留了「用具象人物表达冲突需求」这个核心价值。
一个 Persona 包含什么
一个 Persona 草图通常有这几部分:
- 照片、姓名、角色标签——让 Persona 立体起来
- 个性化背景——这个角色的工作经历、偏好、约束
- 质量关注列表——他/她最在意什么质量属性,每个有相关性等级
- 用户故事 / 场景——具体的工作流程
- anti-stories / loss scenarios——他/她绝对不希望发生什么
举个例子(TraceLab 中的 Persona "Tom"):
Persona Tom
Tom 是一位资深的可追溯性研究员,发表过多篇用 LDA、LSI、概率方法做源代码到设计 / 需求追溯的论文,也开发了可视化算法。
Tom 偏好在 Linux 上用 C++ 编程。他打算贡献组件给 TraceLab,但已经有一套自己的研究环境,所以不一定使用 TraceLab 全部功能。
关注的质量属性:性能(快速追溯检索)、平台兼容性、扩展性、易组件上传、易安装、直观界面、文档兼容、数据机密性、广泛采用
用户故事:
- 我需要能用 C++ 编写组件,并简单地集成到 TraceLab 实验中
- 用 TraceLab 跑实验的时间不能比独立运行慢太多
- 我需要在 Linux 上运行 TraceLab
- 我需要访问基准数据集来对比新算法
- 我需要访问已有追溯矩阵的数据集
Anti-stories:
- 如果 TraceLab 经常崩溃,我不会用它
每个 Persona 各有偏好和取舍。关键不在于每个 Persona 都满足,而在于把冲突显式化——比如 Tom 要 Linux + C++,Karly 要 C# + 数据保密、Jack(架构师)要快速原型 + GUI 库支持……架构师必须做权衡,而 Persona 让权衡变得可见。
整套流程
flowchart TD
P1[1. 识别初步 Persona]
P2[2. 详化 Persona<br>探索质量关注]
P3[3. 提取用户故事]
P4[4. 分配到各 Persona]
P5[5. 头脑风暴架构决策]
P6[6. 评估架构决策对各 Persona 的影响]
P7[7. 识别架构风险与缓解措施]
P8[8. 记录对 Persona 的影响]
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> P8
P8 -.迭代.-> P1
classDef setup fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef analyze fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef decide fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef risk fill:#ffebee,stroke:#c62828,stroke-width:2px
class P1,P2 setup
class P3,P4 analyze
class P5,P6 decide
class P7,P8 risk
教材以 TraceLab 的两个具体决策为例:
决策 1:平台 / 语言选择。 经过对 Persona 的分析,最终决定用 .NET / C# 构建框架(支持快速原型 + GUI),WPF 实现 Windows GUI,并采用 MVVM 模式让 GUI 视图与业务逻辑解耦——这样后期可以用 Mono 编译到 Linux / Mac。这个决策部分满足了 Tom 和 Mary 的需求(长期可在 Linux 上用),但早期只能在 Windows 上发布。
决策 2:工作流架构。 在 Pipe-and-Filter、Services、Precedence Graph + Blackboard 三个候选风格中选择——同样通过 Persona 评估每种风格对不同研究者群体的影响。最终选择了基于工作流(workflow)的方案,所有 Persona 对插件式方案都满意,性能上虽对 Tom 略有损失,但对其他研究者收益更大。
Persona 方法的价值
Persona 方法把抽象的「干系人需求」具象化——架构师不再面对一份冰冷的需求列表,而是面对一组有姓名、有性格、有痛点的人。这种具象化降低了沟通成本,让冲突和取舍都更容易讨论。
SCRUM + ASRs
最后一块拼图:怎么把 ASR 工作嵌入敏捷开发流程?教材的答案是 SCRUM + ASRs:
flowchart TD
PI[识别初步 Persona]
PE[详化 Persona<br>探索质量关注]
PU[更新 Persona]
AD[探索架构决策与权衡]
DSM[每日站会]
SF[选择特性 +<br>关联架构组件]
SP[Sprint backlog<br>团队扩展任务]
AC[把架构拆成<br>Sprint 大小的块]
PB[产品 Backlog<br>客户排序的特性]
SC[增量构建软件,<br>包括架构]
SH[交付可发货产品]
PI --> PE
PE --> PU
PU -.迭代.-> PE
PE --> AD
AD --> DSM
DSM --> SF
SF --> SP
SF --> AC
AC --> PB
SP --> SC
SC --> SH
classDef persona fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef arch fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef sprint fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef ship fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class PI,PE,PU persona
class AD,DSM,SF,SP arch
class AC,PB sprint
class SC,SH ship
核心思路:把架构工作切成 Sprint 大小的块,每个 Sprint 同时推进特性和架构演化。每个 Sprint 内:
- 先有 Persona 驱动的质量关注探索
- 再有架构决策与权衡讨论
- 然后选出这个 Sprint 要交付的特性 + 关联的架构组件
- 团队在 Sprint backlog 上展开任务
- Sprint 结束时交付可发货产品
这种结构破除了「架构 vs 敏捷」的伪对立——架构不需要在第一天全做完,但每个 Sprint 都需要有意识地推进它。
小结
本节是 SysArch 板块的第二节,核心是把上一节抽象的「质量属性」和「非功能需求」落到工程语言里。我们的收获可以归到三层:
第一层 — 概念框架:
- 三类需求:功能、质量、约束
- 质量属性 = 非功能需求 = 架构需求,分可观察与不可观察
- 用六要素场景(刺激源、刺激、构件、环境、响应、响应度量)精确刻画质量需求
- 战术是针对单一质量属性的设计技巧,多个战术组成模式 / 风格
- 设计决策可归纳为七大类别
第二层 — 七大质量属性:
| 属性 | 关注 | 战术核心 |
|---|---|---|
| 可用性 | 系统在需要时能用的比例 | 检测 / 恢复 / 预防故障 |
| 互操作性 | 系统间正确交换信息 | 定位 / 管理接口 |
| 可修改性 | 变更的成本 | 减小模块 / 增内聚 / 降耦合 / 推迟绑定 |
| 性能 | 时序需求 | 控制需求 / 管理资源 |
| 安全性 | CIA 三性 | 检测 / 抵抗 / 响应 / 恢复 |
| 可测试性 | 暴露缺陷的容易度 | 控制观察 / 限制复杂度 |
| 易用性 | 用户完成任务的容易度 | 用户主动 / 系统主动 |
第三层 — ASRs 与方法:
- ASR 是真正影响架构走向的需求——困难且重要的 QA 需求最可能是 ASR
- 四种获取方式:从需求文档(弱)、QAW 访谈、商业目标、效用树
- Persona-Based 方法把抽象需求具象化,让冲突可见
- SCRUM + ASRs 把架构工作切成 Sprint 增量
下一节我们会接着讨论架构模式(Architectural Patterns)——本节战术的「上一层抽象」,把多个战术组合成可复用的整体架构方案。届时回看本节的战术分类,会发现它正是模式背后的「积木库」。