需求为什么总在变

做需求最怕听到两句话:一句是「这个很简单,你先做出来看看」,另一句是「怎么又变了」。需求变更本身并不可怕,真正的问题是我们没有为变化留出结构——把每次改动都当成补丁打在原来的设计上,系统迟早会变成一团无法维护的泥球。

需求分析的目标不是把所有需求一次性问清楚(这基本不可能),而是建立一套能低成本吸收变化的结构。它的产出有两部分:一是明确、可验收的需求描述,二是支撑这些需求的架构骨架。

需求决定「做什么」,架构决定「怎么放」。前者描述问题空间,后者规划解决空间;两者之间需要一条清晰的映射链。

这条映射链一旦断裂,就会出现最典型的两类问题:需求做了,但结构放不下(改一处动全身);或者结构很漂亮,但没解决用户的真实问题(过度设计)。本文沿着「需求 → 模型 → 架构 → 决策 → 演进」的顺序,把这条链补全。

本文核心知识点
  • 用户故事用「角色—功能—价值」承载需求,配合验收标准确定「做完」的含义。
  • 领域建模用实体、值对象与聚合描述业务概念及其一致性边界。
  • 分层架构通过依赖方向控制耦合,让业务逻辑不依赖具体框架。
  • ADR用一页纸记录每次架构决策的背景、选项、结论与后果。
  • 质量属性性能、可用性、可维护性等非功能需求,往往比功能更能决定架构。
软件架构示意:自上而下的分层模块方框、放大镜与相连卡片组成的蓝色扁平插画
从需求到架构:上层的业务概念向下映射为分层结构,依赖方向决定了改动的传播范围。

从用户故事到验收标准

承接敏捷实践,需求通常以用户故事的形式进入待办列表。但用户故事只是「需求的一半」——它说清了价值,却没说清边界。补上边界靠验收标准:一组可执行的、非黑即白的判断条件。

一条好的验收标准要满足三点:

  • 可验证:能写成一个自动化或手工的测试用例,而不是「体验流畅」这类主观描述。
  • 覆盖边界:除了正常路径,必须包含异常路径与边界值。
  • 无歧义:不同的人读完后,对「通过」的判定一致。

把验收标准写清楚,等于提前完成了一次需求评审。很多所谓的「需求变更」,其实是当初验收标准没写清、双方各自脑补了不同版本。等到验收时才发现分歧,返工成本已经翻了好几倍。

用例与领域建模

有了逐条需求,还要把它们拼成一张业务全景图。这一步通常分两层:用例视角描述「角色如何与系统交互」,领域视角描述「系统中存在哪些概念、它们如何关联」。

领域建模的关键动作是识别实体(有唯一标识、会随时间变化的对象,如订单、用户)、值对象(靠属性值区分、不可变的对象,如金额、地址)与聚合(一组必须保持一致性的对象边界)。聚合边界划得对不对,直接决定了后续事务与并发设计的复杂度。

# 领域模型示例:订单聚合
class Order:                      # 聚合根,对外唯一入口
    id: OrderId                   # 实体标识
    items: list[OrderItem]        # 订单项,聚合内对象
    status: OrderStatus           # 值对象 / 枚举

    def add_item(self, sku, qty, price):     # 唯一入口:外部只能经由聚合根修改
        self.items.append(OrderItem(sku=sku, qty=qty, price=price))

    def total(self) -> Money:                # 计算不变式在聚合内维护
        return Money(sum(i.qty * i.price for i in self.items))

class OrderItem:
    sku: str
    qty: int
    price: Money                  # 值对象,不可变

领域模型不必事无巨细。原则是只建模当下会用到的概念,随着业务演进再补充。过度建模和完全建模一样有害:前者会拖慢交付,后者会把设计绑死在还没验证的假设上。

分层架构与依赖方向

架构的核心问题可以浓缩成一句话:谁依赖谁。经典的分层架构把系统划分为表现层、应用层、领域层与基础设施层,并规定依赖只能由外向内——领域层不依赖框架、不依赖数据库,甚至可以完全不知道外面长什么样。

src/
├── presentation/     # 表现层:控制器、DTO、参数校验
│   └── order_controller.py
├── application/      # 应用层:用例编排、事务边界
│   └── place_order_service.py
├── domain/           # 领域层:实体、值对象、领域服务(不依赖框架)
│   ├── order.py
│   └── pricing.py
└── infrastructure/   # 基础设施层:数据库、消息、第三方网关
    ├── order_repository.py
    └── payment_gateway.py

# 依赖方向:presentation -> application -> domain
#          infrastructure -> domain(实现 domain 定义的接口)
# domain 不反向依赖任何一层

依赖倒置是这条规则的关键工具:领域层定义接口(如 OrderRepository),基础设施层提供实现。这样领域逻辑就能独立于具体存储技术被测试与复用。反过来说,一旦领域层 import 了某个 ORM 框架,你的业务逻辑就被牢牢绑死在了框架的版本上——升级框架时会付出成倍的代价。

分层不是目的,可控的改动范围才是目的。判断一个分层是否健康,可以问自己:改一个业务规则,需要动几层?如果答案是一层,说明边界清晰;如果需要横跨四层,那分层的意义就所剩无几了。

用 ADR 记录架构决策

架构方案一旦确定,往往会被后来的成员视为「本来就该这样」,其中的权衡与背景全部丢失。解决办法是架构决策记录(ADR):用一页纸记录一次决策的上下文、选项、结论与后果。

# ADR-007:订单查询采用 CQRS 读写分离

## 状态
已接受(2026-10-14)

## 背景
订单列表查询 QPS 高、维度多,与写模型的聚合边界冲突。

## 选项
1. 单一模型同时承担读写(简单,但读模型被迫迁就写模型)
2. 读写分离 CQRS(复杂度上升,但两侧可独立优化)

## 决策
采用方案 2,读模型以异构方式构建。

## 后果
- 正向:查询性能与扩展性提升,读侧可自由选型。
- 负向:需要处理最终一致性,团队需掌握事件驱动模式。
要素说明
状态提议 / 已接受 / 已废弃 / 被替代
背景是什么问题促使我们做这个决定
决策最终选择了什么,一句话讲清
后果做这个决定之后,好处与代价分别是什么

ADR 的价值不在「决策」本身,而在「选项」与「后果」——它逼你把被否决的方案也写下来。当半年后有人问「当初为什么不用消息队列」,翻一翻 ADR 就有答案,而不是靠某个人模糊的记忆。