质量属性与架构战术

当我们说「这是个好系统」时,到底在说什么

上一节谈到,架构关注的不只是「系统能做什么」,更是「系统做得有多好」——可用性、性能、安全性、可修改性这些「非功能」的维度。这些词听起来直觉、却出奇地难写进合同里:

  • 「系统应当高可用」——多高?停机多久算停机?计划维护算不算?
  • 「系统应当快速响应」——多快?平均响应还是 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 把架构师做的设计决策归纳为七个类别

设计决策的七大类别

  1. 职责分配(Allocation of Responsibilities):哪个组件负责什么
  2. 协调模型(Coordination Model):组件之间如何对话——同步/异步、保证投递/最佳努力
  3. 数据模型(Data Model):核心数据抽象、操作、性质
  4. 资源管理(Management of Resources):CPU、内存、连接、线程的分配与回收
  5. 架构元素映射(Mapping among Architecture Elements):组件如何映射到进程、进程如何映射到处理器
  6. 绑定时间决策(Binding Time Decisions):什么时候确定一个值——设计时、编译时、部署时、运行时
  7. 技术选择(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 应用的关键需求。

可用性最常用的定义性公式是:

Availability=MTBFMTBF+MTTR\text{Availability} = \frac{\text{MTBF}}{\text{MTBF} + \text{MTTR}}

其中 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)关心系统变更的成本——时间、金钱、对其他功能或质量属性的影响。

准备变更也有成本,做出变更也有成本。架构师要在两者之间做权衡。

计划变更的四个问题

回答以下四个问题,能让模糊的「我们需要灵活的系统」变成可分析的工程问题:

  1. 什么会变?(What can change?)——功能、容量、技术、质量属性?
  2. 变化的可能性有多大?(What is the likelihood of the change?)
  3. 什么时候变、谁来变?(When is the change made and who makes it?)
  4. 变更的成本是多少?(What is the cost of the change?)

准备变更的成本权衡公式

如果未来变更次数比预期少很多,过早准备变更机制反而是浪费。可以用一个简单的不等式刻画这个权衡:

NC无机制  <  C安装机制+NC有机制N \cdot C_{\text{无机制}} \;<\; C_{\text{安装机制}} + N \cdot C_{\text{有机制}}

其中:

  • NN 是预计的变更次数
  • C无机制C_{\text{无机制}} 是没有可修改性机制时单次变更的成本
  • C安装机制C_{\text{安装机制}} 是引入机制的一次性成本
  • C有机制C_{\text{有机制}} 是有了机制后单次变更的成本

如果左边 ≥ 右边,说明引入机制划算;否则就是过度设计。这个公式不是用来精确算钱的,而是提醒架构师不要为了「以防万一」预设过多扩展点——每一个抽象层、每一个配置文件、每一个插件接口,都是要还的债。

可修改性通用场景

场景部分 可能值
刺激源 终端用户、开发者、系统管理员
刺激 添加 / 删除 / 修改功能,或改变质量属性、容量、技术
构件 代码、数据、接口、组件、资源、配置
环境 运行时、编译时、构建时、初始化时、设计时
响应 修改、测试、部署
响应度量 受影响构件的数量 / 大小 / 复杂度;工时;日历时间;金钱;对其他功能或质量属性的影响;引入的新缺陷

可修改性场景示例

  • 刺激源:开发者
  • 刺激:希望修改 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 准备成本

推迟绑定虽然能让系统「更灵活」,但每多一个推迟绑定点都会增加初始化和调试复杂度。回到上面那个 NN 的公式——只有当该参数确实会被频繁改变时,推迟绑定才划算。

可修改性的设计检查清单

类别 关键问题
职责分配 可能发生哪些变更(技术、法律、社会、商业、客户)?将同时变化的职责放在同一模块,将不同时变化的放在不同模块
协调模型 哪些功能 / 质量属性可能在运行时变?是否使用降低耦合的协调模型(发布订阅)、推迟绑定的(企业服务总线 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)是把质量属性、子属性、具体场景层层细化的树状结构——SEIATAM 评估方法里大量使用。

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 全部功能。

关注的质量属性:性能(快速追溯检索)、平台兼容性、扩展性、易组件上传、易安装、直观界面、文档兼容、数据机密性、广泛采用

用户故事

  1. 我需要能用 C++ 编写组件,并简单地集成到 TraceLab 实验中
  2. 用 TraceLab 跑实验的时间不能比独立运行慢太多
  3. 我需要在 Linux 上运行 TraceLab
  4. 我需要访问基准数据集来对比新算法
  5. 我需要访问已有追溯矩阵的数据集

Anti-stories

  1. 如果 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)——本节战术的「上一层抽象」,把多个战术组合成可复用的整体架构方案。届时回看本节的战术分类,会发现它正是模式背后的「积木库」。