1. 为什么需要软件工程?

软件无处不在

现代世界完全依赖软件:国家基础设施、金融系统、医疗、娱乐、手机……可以说,没有软件,现代社会无法运转

软件的特殊性

软件和桥梁、建筑不同:

实体工程(如桥梁):
  受材料物理特性限制
  受自然规律约束
  复杂度有自然上限
软件工程:
  没有物理约束
  理论上可以无限复杂
  越复杂越难理解、越贵

这是一把双刃剑——自由度大,但失控风险也大。

软件失败的两大根本原因

  1. 系统越来越复杂:需求不断增长,旧方法跟不上
  2. 没有使用软件工程方法:很多公司"随手写代码",导致软件昂贵且不可靠

2. 什么是专业软件开发?

业余 vs 专业的本质区别


对比项 业余编程 专业软件开发
使用者 只有自己 其他用户
开发者 个人 团队
文档 不需要 必须有
维护 不需要 全生命周期维护
质量要求 能跑就行 需满足非功能属性

软件 = 程序 + 更多东西

很多人误以为软件只是代码,实际上:

软件 = 程序代码
     + 文档(用户手册、设计文档)
     + 配置文件
     + 支持网站
     + 库文件

两类软件产品

软件产品

通用产品
Generic Products

定制产品
Custom Software

面向开放市场
任何人都能买

规格由开发商控制

例:手机App
Word
PS

为特定客户开发

规格由客户控制

例:航空管制系统
医院信息系统

关键区别:谁控制需求规格?

  • 通用产品:开发商说了算,遇到问题可以改方向
  • 定制产品:客户说了算,开发商必须按规格做

3. 好软件的四个核心属性


属性 含义 举例
可接受性(Acceptability) 用户能理解和使用,与其他系统兼容 界面直观,文件格式通用
可靠性和安全性(Dependability & Security) 不崩溃、不被攻击、不造成损失 银行系统7×24不宕机
效率(Efficiency) 不浪费内存和CPU 响应快、资源占用少
可维护性(Maintainability) 能随需求变化而修改 代码结构清晰,易于扩展

4. 软件工程是什么?

定义拆解

软件工程 = 工程纪律 + 覆盖软件生产全过程
两个关键词:

  1. 工程纪律:在约束(预算、时间)内找到可行解,不追求完美
  2. 全过程:从需求到维护,不只是编码

软件过程的四个基本活动

软件规格说明
Specification

软件开发
Development

软件验证
Validation

软件演化
Evolution

  • 规格说明:客户和工程师定义要做什么
  • 开发:设计和编程
  • 验证:检查是否满足客户需求
  • 演化:随需求变化修改软件

软件工程 vs 计算机科学 vs 系统工程

计算机科学(Computer Science)
  ↓ 提供理论基础
软件工程(Software Engineering)
  ↓ 是其中的一部分
系统工程(System Engineering)
  = 硬件 + 软件 + 流程 + 部署

5. 软件工程面临的四大挑战

软件工程挑战

异构性
Heterogeneity

业务和社会变化
Business Change

安全与信任
Security & Trust

规模
Scale

跨平台:PC/手机/嵌入式
多语言遗留系统集成

需求变化快
交付时间压缩

防止恶意攻击
保护隐私数据

从小型嵌入式
到全球云系统

6. 八类应用系统及其特点


类型 例子 核心关注点
独立应用 PC上的Word、手机App 功能完整性
交互式事务系统 电商网站、网银 数据一致性、并发
嵌入式控制系统 汽车ABS、微波炉 实时性、安全性
批处理系统 电话账单、工资系统 吞吐量、准确性
娱乐系统 游戏 用户体验、响应速度
建模仿真系统 气候模型 计算性能
数据采集分析 传感器网络、大数据 可靠性、实时性
系统之系统 企业ERP 集成复杂度

7. 互联网对软件工程的影响

互联网改变了软件的组织方式:

互联网之前:
  单机程序 → 本地运行 → 本地通信
互联网之后:
  高度分布式 → 全球部署 → 基于服务

四个主要变化:

  1. 软件复用成为主流:先找现成的,再自己写
  2. 增量开发成为标准:边做边改,不可能一次把需求定死
  3. 服务化架构(SOA):软件组件 = 独立Web服务
  4. 富界面技术出现:AJAX、HTML5让浏览器变成应用平台
    **软件即服务(SaaS)**的兴起:
传统模式:买软件光盘 → 安装到电脑 → 本地运行
现代模式:订阅服务 → 云端运行 → 浏览器访问
例:Office 365、Google Docs、Adobe CC

8. 软件工程伦理

为什么工程师需要关注伦理?

软件工程师拥有巨大影响力——代码可以影响数百万人的生活、安全和隐私。

四个核心职业责任

软件工程师职业责任

保密性
Confidentiality

能力诚实
Competence

知识产权
IP Rights

计算机不滥用
No Misuse

尊重雇主客户保密信息
无论是否签协议

不接受超出能力范围的工作
如实说明自己的水平

了解专利版权法
保护雇主知识产权

不散布病毒
不非法入侵系统

ACM/IEEE 联合职业道德准则(8条原则)


编号 原则 核心含义
1 公众(PUBLIC) 行动符合公共利益
2 客户和雇主(CLIENT & EMPLOYER) 在符合公共利益前提下维护客户利益
3 产品(PRODUCT) 产品达到最高专业标准
4 判断(JUDGMENT) 保持职业判断的独立性和诚信
5 管理(MANAGEMENT) 管理者推广道德的管理方式
6 职业(PROFESSION) 提升职业声誉和诚信
7 同事(COLLEAGUES) 公平支持同事
8 自我(SELF) 终身学习,持续提升

伦理困境的典型场景

场景1:安全隐患

公司因赶工期 → 伪造了安全验证记录 → 工程师发现了
选择A:保持沉默(保密性原则)
选择B:向客户/媒体披露(公众安全原则)
两者都有正当理由,需要权衡损害程度和影响范围

场景2:军事/核武器项目

有人:坚决不参与任何军事软件开发
有人:愿意做军事系统,但不做武器系统
有人:认为国家安全优先,无异议
正确做法:雇主和员工提前明确立场,互相尊重

9. 四个案例研究

案例1:胰岛素泵控制系统(嵌入式系统)

问题背景:
  糖尿病患者 → 胰腺无法产生足够胰岛素
  传统方式:手动检测血糖 + 手动注射
自动化方案:
  血糖传感器 → 控制器计算剂量 → 微型泵注射胰岛素

血糖传感器
Blood Sensor

分析读数
Analyze

计算胰岛素剂量
Compute Dose

控制泵
Control Pump

注射胰岛素
Deliver Insulin

记录日志
Log Dose

核心挑战:这是安全关键系统

  • 血糖过低(胰岛素太多)→ 短期:大脑失能、昏迷、死亡
  • 血糖过高(胰岛素太少)→ 长期:眼、肾、心脏损伤
    两个必须满足的高级需求
  1. 系统在需要时必须能供应胰岛素(可用性)
  2. 系统必须可靠地输送正确剂量(准确性)

案例2:精神卫生信息系统(Mentcare)

这是一个信息系统,管理精神病患者的就诊记录。

系统架构:
  [Mentcare客户端]  [Mentcare客户端]  [Mentcare客户端]
         |                  |                  |
         +------------------+------------------+
                            |
                       [网络 Network]
                            |
                     [Mentcare服务器]
                            |
                      [患者数据库]

两个主要目的

  1. 生成管理报告(评估绩效)
  2. 为医疗人员提供患者信息(支持治疗)
    特殊挑战:精神病患者的特殊性
患者可能:
  - 错过预约
  - 弄丢处方
  - 忘记指示
  - 无固定住所
  - 对自己或他人有危险(需要强制住院)

法律冲突:隐私 vs 可用性

隐私最大化 → 只保存一份数据副本
可用性最大化 → 保存多份副本(服务器故障时可用)
两者存在矛盾,需要设计权衡

案例3:野外气象站(传感器数据采集)

背景:政府在偏远地区部署数百个气象站,
      用于气候变化监测和天气预报
系统组成:
  [气象站系统]  ←卫星通信→  [数据管理和存档系统]
       ↕                              ↕
  [站点维护系统] ←远程控制→  (可远程更新软件)

特殊挑战(因为无人值守、偏远部署):

1. 电池供电,太阳能/风能充电
2. 恶劣环境(极端温度、动物破坏)
3. 卫星带宽窄 → 必须本地预处理数据
4. 必须支持远程重配置
5. 必须能自我监控故障并上报

这使得软件远比功能看起来复杂。

案例4:学校数字学习环境(iLearn)

这是一个支持系统,为3-18岁学生提供数字化学习环境。

架构(三层服务):
浏览器用户界面
       |
[配置服务层]
       |
  +----+----+----+
  |    |    |    |
工具服务 应用服务 实用服务
工具服务(Utility Services):
  认证、存储、搜索、日志监控
应用服务(Application Services):
  邮件、视频会议、报纸档案、VLE
配置服务(Configuration Services):
  管理哪些服务对哪些用户可用

两种服务集成方式

集成服务(Integrated):
  提供API → 其他服务可以直接调用 → 无需重新认证
  例:统一认证服务
独立服务(Independent):
  只通过浏览器访问 → 服务间数据只能靠复制粘贴
  → 可能需要重复登录

10. 知识结构总览

软件工程第一章

为什么重要

专业软件开发

软件工程定义

伦理

案例研究

社会依赖软件
失败代价巨大

通用产品

定制产品

好软件四属性

四个基本活动

四大挑战

八类应用类型

互联网影响

职业责任

ACM/IEEE准则

伦理困境

胰岛素泵
嵌入式

Mentcare
信息系统

气象站
数据采集

iLearn
支持环境

11. 章节核心要点总结


要点 说明
软件工程的范围 不只是编码,包括规格、开发、验证、演化全过程
软件的完整含义 程序 + 文档 + 配置 + 库 + 支持网站
好软件的标准 可接受性、可靠安全性、效率、可维护性
不存在万能方法 不同系统类型需要不同的工程技术
共同基础 过程管理、可靠性、需求工程、软件复用
工程师的责任 不只是技术,还有对社会和职业的伦理责任
职业准则 ACM/IEEE的八条原则提供行为指导

12. 练习题参考思路

1.1 专业软件不只是程序本身
专业软件还包括:用户文档(如何使用)、设计文档(如何构建)、配置文件、测试套件、支持网站。这些对于软件被他人使用和维护至关重要。
1.2 通用 vs 定制软件的关键区别
通用软件的规格由开发商控制,可以根据市场反馈调整。定制软件的规格由客户控制,开发商必须严格遵守。对用户而言:通用软件可能不完全符合需求,但价格低;定制软件更合身,但价格高。
1.3 为什么软件工程长期更便宜
前期投入软件工程方法(规格说明、设计、测试)可以:

  • 早期发现错误(修复成本低)
  • 减少测试和质量保障成本
  • 降低长期维护成本
  • 避免返工
    不使用软件工程方法的代价:测试成本高、维护困难、需求变更时改动代价巨大。
    1.4 伦理问题举例
  • 收集用户数据时不充分告知(隐私问题)
  • 算法歧视(公平性问题)
  • 开发监控软件(自由 vs 安全)
  • 知道系统有安全漏洞但不报告(公众安全问题)
    1.5 不同应用类型需要不同技术
  • 嵌入式系统:需要实时操作系统、硬件接口编程、严格验证
  • 游戏:需要图形引擎、物理仿真、快速原型迭代
  • 信息系统:需要数据库设计、事务处理、访问控制
  • 批处理系统:需要吞吐量优化、错误恢复机制

软件工程第二章:软件过程详细解读

引言:什么是软件过程?

软件过程就是一套有组织的活动集合,目标是生产出软件系统。
打个比方:就像盖房子有"打地基→砌墙→盖屋顶→装修"这样的步骤,软件开发也需要一套有序的步骤。但软件比房子复杂得多,因为需求会变、技术会变、人员会变。
所有软件过程都必须包含这四个核心活动:

需求变化

软件规格说明
Specification

软件开发
Development

软件验证
Validation

软件演化
Evolution

第一节:三大过程模型

总览对比

软件过程模型

瀑布模型
Waterfall

增量开发
Incremental

集成与配置
Integration & Config

顺序执行
计划驱动
适合需求稳定

迭代交付
灵活响应
适合需求多变

复用为主
配置集成
适合有现成组件

1.1 瀑布模型(Waterfall Model)

什么是瀑布模型?

就像瀑布从上往下流,每个阶段完成后才能进入下一阶段,不能回头。

阶段1:需求分析
    ↓(瀑布流下)
阶段2:系统和软件设计
    ↓
阶段3:实现和单元测试
    ↓
阶段4:集成和系统测试
    ↓
阶段5:运行和维护
五个阶段详解

需求分析与定义
Requirements Analysis

系统和软件设计
System & Software Design

实现和单元测试
Implementation & Unit Testing

集成和系统测试
Integration & System Testing

运行和维护
Operation & Maintenance

明确服务、约束、目标
产出:需求规格文档

分配到硬件或软件
确定整体架构
产出:设计文档

将设计变成程序
单独测试每个单元
产出:程序代码

组合所有程序
整体测试
产出:完整系统

修复错误
改进功能
响应新需求

瀑布模型的核心思想

理想情况:每个阶段结束时产出一个"签批文档",下一阶段才能开始。
现实情况:各阶段之间需要反馈,就像这样:

需求分析 ←───────────────── 发现需求有问题
    ↓
系统设计 ←────────── 发现设计有问题
    ↓
实现阶段 ←── 发现代码无法实现设计
    ↓
测试阶段 ──→ 发现各种问题 ──→ 反馈给上面各阶段
什么时候用瀑布模型?

适用场景 原因
嵌入式系统(如汽车软件) 硬件已定型,软件功能必须提前明确
安全关键系统(如医疗、航空) 需要完整文档做安全分析,后期改动代价极高
多公司合作的大型系统 需要完整规格说明来协调各方独立开发

瀑布模型的问题
问题1:需求冻结过早
  ↓
用户看到"完整系统"才发现不是自己想要的
  ↓
这时候修改代价极高(可能要推翻重来)
问题2:文档驱动太重
  ↓
大量时间花在写文档、审批文档
  ↓
真正写代码的时间被压缩
问题3:不适合需求多变的场景
  ↓
业务环境变化 → 需求变了 → 所有阶段可能要重做

1.2 增量开发(Incremental Development)

核心思想

不一次性做完整个系统,而是分批次、一点一点地做,每次做一个"增量"(Increment),每个增量都是可运行的软件版本。

需求 ──┐
设计 ──┼──→ 增量1(最核心功能)──→ 发布给用户
实现 ──┘
       ↓用户反馈
需求 ──┐
设计 ──┼──→ 增量2(更多功能)──→ 发布给用户
实现 ──┘
       ↓用户反馈
       ...继续...
       ↓
需求 ──┐
设计 ──┼──→ 增量N(完整功能)──→ 最终产品
实现 ──┘
图示

并发活动

概要描述
Outline Description

规格说明
Specification

开发
Development

验证
Validation

初始版本
Initial Version

中间版本
Intermediate Versions

最终版本
Final Version

增量开发的三大优势
优势1:降低需求变更成本
  传统瀑布:改需求 = 重做所有文档 + 重新设计 + 重新编码
  增量开发:改需求 = 只影响当前增量,后续增量可以调整
优势2:更容易获得客户反馈
  传统瀑布:客户要等整个系统完成才能看到
  增量开发:客户每个增量都能看到、试用、提意见
优势3:早期交付有价值的软件
  传统瀑布:等全部做完才能用(可能几年后)
  增量开发:第一个增量完成就能用最核心的功能
增量开发的两个管理问题
问题1:过程不可见
  每次增量变化快,不值得为每个版本做大量文档
  → 管理者难以通过文档评估进度
问题2:系统结构逐渐退化
  每次增量都往系统里"塞"新功能
  → 代码越来越乱(技术债务)
  → 解决方案:定期重构(Refactoring)

1.3 集成与配置(Integration and Configuration)

核心思想

“不重复造轮子”——尽量用现成的组件、系统,把它们配置好、集成起来,就是新系统。

三类可复用的组件

类型 说明 举例
独立应用系统 通用系统,配置后用于特定环境 SAP ERP系统
对象集合/组件包 在框架中集成的组件 Java Spring组件
Web服务 通过互联网远程调用的服务 支付API、地图API

复用导向的开发过程

没有完全匹配

需求规格说明
Requirements Specification

软件发现与评估
Software Discovery & Evaluation

需求细化
Requirements Refinement

是否有现成系统?

配置应用系统
Configure Application System

适配组件并集成
Adapt Components & Integrate

完成系统

搜索满足需求的
现有组件和系统

根据可用组件修改需求
需求与可用组件相互影响

优缺点

方面 优点 缺点
开发量 大幅减少自研代码量 对组件的控制权有限
成本 降低开发成本和风险 需求可能被迫妥协
速度 更快交付软件 系统演化受第三方影响
需求匹配 无法完全满足真实需求

第二节:过程活动详解

2.1 软件规格说明(需求工程)

什么是需求工程?

弄清楚"软件要做什么",并把它严格地写下来。

用户的想法(模糊)
    ↓ 需求工程
精确的需求文档(明确)
需求工程的三个活动

发现问题

需求获取与分析
Elicitation & Analysis

需求规格说明
Requirements Specification

需求验证
Requirements Validation

观察现有系统
访谈用户
任务分析
制作原型

用户需求:高层次
系统需求:详细

检查是否现实
是否一致
是否完整

两种需求层次
用户需求(User Requirements):
  面向:客户和最终用户
  语言:自然语言,避免专业术语
  例:系统应该允许护士查询病人的过敏记录
系统需求(System Requirements):
  面向:系统开发者
  语言:更技术化、更精确
  例:系统应在3秒内返回所有关联该病人ID的过敏记录列表,
       按严重程度降序排列

2.2 软件设计与实现

设计过程的输入输出
输入:
  - 软件需求(要做什么)
  - 平台信息(在什么环境上运行)
  - 数据描述(处理什么样的数据)
四个设计活动:
  ┌─────────────────────────────────────────┐
  │  架构设计  数据库设计  接口设计  组件设计  │
  └─────────────────────────────────────────┘
输出:
  - 系统架构(整体结构图)
  - 数据库设计(数据如何存储)
  - 接口规格(组件间如何通信)
  - 组件描述(每个模块的功能)
四个设计活动详解

设计活动 目标 举例
架构设计 确定系统整体结构,主要组件及关系 分为前端/后端/数据库三层
数据库设计 设计数据结构和存储方式 病人表、就诊记录表的字段和关系
接口设计 定义组件间的接口(必须无歧义) 前端调用后端API的参数和返回格式
组件设计 寻找可复用组件,设计新组件 选用成熟的登录认证库

测试 vs 调试的区别

很多人混淆这两个概念:

测试(Testing):
  目的:发现"有没有缺陷"
  结果:是/否,以及缺陷的表现
调试(Debugging):
  目的:找到缺陷在"哪里",并修复它
  过程:分析程序行为 → 假设原因 → 验证假设 → 修复
关系:
  测试发现问题 → 调试定位和修复问题 → 测试验证修复是否成功

2.3 软件验证(V&V)

三阶段测试过程

组件测试
Component Testing

系统测试
System Testing

客户测试
Customer Testing

开发人员自己测
独立测试每个组件
使用JUnit等工具

测试组件间交互
验证功能和非功能需求
测试涌现属性

用真实数据测试
定制软件:客户参与
产品软件:Beta测试

V模型(计划驱动的测试)

V模型把测试和开发阶段对应起来:

需求规格说明 ──────────────────→ 客户测试
    ↓                                ↑
系统规格说明 ────────────────→ 系统集成测试
    ↓                            ↑
系统设计 ──────────────→ 子系统集成测试
    ↓                        ↑
组件设计 ──────────→ 组件代码和测试

即:左边做什么设计,右边就对应做什么测试,用于验证左边的设计是否正确实现。

2.4 软件演化

演化的本质

软件不是"做完了就结束",而是需要持续改变:

传统观念(错误的):
  软件开发 ────── 维护
  (有趣的)      (无聊的)
现代观念(正确的):
  软件是一个持续演化的过程
  需求变化 → 修改系统 → 需求再变化 → 再修改 → ...

新需求出现

定义系统需求
Define Requirements

评估现有系统
Assess Existing Systems

提出系统变更
Propose Changes

修改系统
Modify Systems

新系统
New System

第三节:应对变化

变化为什么不可避免?

外部原因:
  - 市场竞争 → 需要新功能
  - 法规变化 → 必须合规
  - 技术进步 → 可以用新技术
内部原因:
  - 开始时需求不清楚
  - 用户实际使用后才知道要什么
  - 团队对需求的理解偏差

应对变化的两种策略


策略 含义 方法
变更预期(Change Anticipation) 提前预测可能发生的变化 原型开发
变更容忍(Change Tolerance) 设计系统使得变化容易实现 增量交付

3.1 原型开发(Prototyping)

什么是原型?

原型是软件系统的早期简化版本,目的不是最终使用,而是:

  • 验证需求是否正确
  • 探索设计选项
  • 向用户展示系统外观
原型开发过程

继续改进

确立原型目标
Establish Objectives

定义原型功能
Define Functionality

开发原型
Develop Prototype

评估原型
Evaluate Prototype

产出:原型计划

产出:可执行原型

产出:评估报告

原型的重要决策:放什么,不放什么
通常可以省略:
  - 非功能需求(响应时间、内存)
  - 错误处理和恢复
  - 高可靠性和稳定性要求
  - 完整的文档
重点包含:
  - 用户界面(UI最重要)
  - 核心业务功能
  - 用户最关心的功能
原型的注意事项

重要警告:原型不应该直接变成生产系统!

原型的问题:
  - 为了速度,省略了错误处理
  - 没有完整测试
  - 代码结构混乱
  - 非功能需求没达标
如果强行把原型当产品用:
  → 系统不稳定
  → 难以维护
  → 安全漏洞
  → 技术债务巨大

3.2 增量交付(Incremental Delivery)

核心流程

没完成

完成

定义概要需求
Define Outline Requirements

分配需求到增量
Assign to Increments

设计系统架构
Design Architecture

开发系统增量
Develop Increment

验证增量
Validate Increment

集成增量
Integrate Increment

验证系统
Validate System

系统完成了吗?

部署增量
Deploy Increment

最终系统
Final System

增量交付的四个优势
优势1:早期增量可作为原型
  用户使用真实系统,而不是"玩具"原型
  反馈更真实、更有价值
优势2:客户可以立即获得价值
  最重要的功能先做
  用户不用等全部完成
优势3:容易整合变化
  后续增量可以根据反馈调整
优势4:最重要的功能测试最充分
  先做的增量经历了更多轮测试
增量交付的三个问题
问题1:替换旧系统很困难
  用户需要旧系统的全部功能
  新旧系统数据库可能不兼容
  用户不愿用"半成品"新系统
问题2:公共基础设施难以确定
  哪些功能是所有增量都需要的?
  如果没规划好,后来可能要大改
问题3:与合同机制冲突
  政府、大企业通常要求"完整规格说明"
  增量交付没有完整规格说明直到最后一个增量
  这与传统合同模式不兼容

第四节:过程改进

什么是过程改进?

过程改进就是:理解现有软件开发过程中的问题,然后改进它,让软件质量更好、成本更低、速度更快。

两种改进方法


方法 核心理念 特点
过程成熟度方法 引入规范化过程和最佳实践 系统化、文档化、适合大公司
敏捷方法 减少开销,快速响应变化 轻量级、迭代快、适合小团队

改进循环

测量
Measure

分析
Analyze

变更
Change

测量过程和产品属性
建立基线数据

评估现有过程
识别弱点和瓶颈

提出改进方案
实施并重新测量

能力成熟度模型(CMM/CMMI)

由美国软件工程研究所(SEI)提出,把软件组织的过程成熟度分为5个级别:

第5级:优化(Optimizing)
  用过程和产品数据驱动持续改进
        ↑
第4级:量化管理(Quantitatively Managed)
  用统计和量化方法控制子过程
        ↑
第3级:已定义(Defined)
  组织有标准化过程,项目从中裁剪
        ↑
第2级:已管理(Managed)
  有文档化计划,有资源管理和监控
        ↑
第1级:初始(Initial)
  基本目标满足,过程范围明确
各级别的形象比喻
第1级(初始):
  "凭感觉"开发,全靠个人英雄主义
  一个厉害的程序员走了,项目就完了
第2级(已管理):
  有基本的项目计划和跟踪
  知道"我们在做什么,什么时候完成"
第3级(已定义):
  有公司标准的开发流程
  每个项目按标准流程做,可以适当裁剪
第4级(量化管理):
  用数字说话:缺陷率、测试覆盖率、开发速度
  知道"我们的过程有多好"
第5级(优化):
  持续分析数据,主动改进过程
  始终在变好

综合对比:三大模型选择指南

大量可用

部分可用或否

需求是否明确且稳定?

是否有安全/监管要求?

是否有现成组件可用?

瀑布模型
Waterfall

瀑布模型也可以

集成与配置
Integration & Config

增量开发
Incremental

适合:嵌入式、安全关键、
多方合作大系统

适合:企业系统、
可以基于现有平台

适合:业务系统、
需求多变的产品


场景 推荐模型 原因
汽车防抱死软件 瀑布 嵌入式、安全关键、硬件已定
电商网站 增量 需求会变、要快速上线
企业ERP系统 集成配置 有成熟产品可定制
大学选课系统(替换旧系统) 增量+原型 用户习惯旧系统,需要渐进替换
游戏App 增量+敏捷 需要用户反馈迭代

核心概念速查表


概念 简单解释
软件过程 生产软件的一套有序活动
瀑布模型 阶段顺序执行,每阶段产出文档
增量开发 分批次开发和交付,每次增加功能
集成与配置 复用现有组件,配置集成成新系统
需求工程 弄清楚"要做什么"的过程
原型 早期简化版本,用于验证和探索
增量交付 把做好的增量实际部署给用户使用
重构 改进代码结构而不改变功能
V模型 把测试阶段与开发阶段一一对应
Beta测试 让真实用户试用产品版软件
过程成熟度 组织软件过程的规范化程度
CMMI 软件能力成熟度集成模型,五个级别

练习题参考思路

2.1 为不同系统选择合适的过程模型

  • 汽车防抱死系统(ABS):瀑布模型。嵌入式安全关键系统,需求必须提前明确,硬件接口固定,修改代价极高。
  • 虚拟现实软件维护工具:增量开发。需求难以提前明确,需要用户不断试用和反馈,技术新颖。
  • 大学会计系统(替换现有系统):增量开发+原型。用户习惯旧系统,需要渐进替换,同时了解新需求。
  • 低碳旅行规划系统:增量开发。交互式系统,用户需求会随使用而变化,需要快速迭代。
    2.7 原型直接变生产系统的利弊
  • 优点:节省重新开发时间,马上可用,给用户留下好印象
  • 缺点:原型没有错误处理、性能优化不足、代码结构差难以维护、安全性低、技术债务巨大。政府用的系统尤其不应该这样做,因为数据安全和可靠性要求高。
    2.8 为什么原型不应成为生产系统(给经理的报告要点)
  1. 原型故意省略了非功能需求(响应时间、安全、可靠性)
  2. 代码结构混乱,难以扩展和维护
  3. 没有完整的错误处理,遇到异常会崩溃
  4. 缺少文档,其他工程师无法理解和维护
  5. 长期维护成本将远超重新开发的成本

第3章:敏捷软件开发 — 从零开始的完整理解

目录

  1. 为什么需要敏捷开发?背景与动机
  2. 敏捷方法的核心思想
  3. 敏捷开发技术详解
  4. Scrum — 敏捷项目管理框架
  5. 扩展敏捷方法
  6. 总结与关键点

1. 为什么需要敏捷开发?背景与动机

1.1 传统开发方式的问题

想象你要建一栋楼:先画好所有图纸,批好所有审批,然后开工。这是"计划驱动(Plan-Driven)"的方式。
在软件开发里,这叫瀑布模型(Waterfall)

需求分析 → 系统设计 → 编写代码 → 测试 → 交付
  ↓           ↓          ↓         ↓       ↓
 文档        文档        文档      报告    完成

问题在哪?

  • 从立项到交付可能要 1~2年
  • 交付时,客户的业务需求早就变了
  • 中途发现需求错了,要从头改,代价极高

用一个比喻来理解:你花半年时间按客户描述的"一辆马车"造好了,结果客户说:“哦,我其实想要的是一辆汽车。”

1.2 真实世界的需求:快速变化

现代企业面对的是:

挑战 说明
市场竞争 竞争对手随时可能推出新产品
需求不确定 用户在用了产品之前,自己也不知道想要什么
技术变化快 新技术、新平台层出不穷
快速迭代 需要频繁上线新功能

1.3 敏捷方法的诞生

1990年代末,一批开发者开始探索更灵活的方式。
2001年,他们聚在一起,发布了著名的 《敏捷宣言》(Agile Manifesto)

我们通过实践以及帮助他人实践,探索出更好的软件开发方式。
我们更重视:

  • 个体和交互 胜过 流程和工具
  • 可运行的软件 胜过 详尽的文档
  • 与客户合作 胜过 合同谈判
  • 响应变化 胜过 遵循计划

右边的内容有价值,但我们更重视左边的。

2. 敏捷方法的核心思想

2.1 计划驱动 vs 敏捷开发 的本质区别

【计划驱动方式】
─────────────────────────────────────────────────
阶段1: 需求分析 ──→ 生成"需求规格书"
                              │
阶段2: 系统设计 ──→ 生成"设计文档"
                              │
阶段3: 编码实现 ──→ 生成"源代码"
                              │
阶段4: 测试验证 ──→ 生成"测试报告"
─────────────────────────────────────────────────
特点:每个阶段完成后才能进入下一阶段,阶段之间用文档沟通
【敏捷方式】
─────────────────────────────────────────────────
迭代1(2周): 需求↔设计↔编码↔测试 → 可运行版本
迭代2(2周): 需求↔设计↔编码↔测试 → 可运行版本
迭代3(2周): 需求↔设计↔编码↔测试 → 可运行版本
─────────────────────────────────────────────────
特点:四个活动交叉进行,每隔2~3周就有可演示的成果

2.2 敏捷的五大原则


原则 通俗解释
客户参与 客户不是"甲方爸爸",而是团队成员,全程参与
拥抱变化 需求改了?好的!设计之初就考虑可扩展性
增量交付 每次只开发一小块,交付一小块,而不是一次性交付全部
保持简洁 不要过度设计,解决今天的问题就好
人而非流程 相信团队成员的判断,不要用死规定束缚他们

2.3 敏捷方法最适合的场景

  1. 软件产品开发:比如开发一款手机App,需求会随用户反馈不断调整
  2. 内部定制系统开发:客户能全程配合,且监管要求不多的情况

3. 敏捷开发技术详解

敏捷中最有影响力的具体方法是 极限编程(Extreme Programming,XP),它引入了很多实用技术。

3.1 用户故事(User Stories)

是什么?
不写冗长的"需求规格书",而是用一段通俗语言写出"某类用户在某种情况下想做某件事"的小故事。
例子(处方开药故事卡):

┌─────────────────────────────────────────────────────┐
│  故事名称:开处方                                    │
│                                                     │
│  Kate是一名医生,想给门诊病人开药。                   │
│  她点击"药物"字段,可以选择:                         │
│    - 当前用药(查看并修改剂量)                       │
│    - 新开药物(输入药名前几个字母,系统自动匹配)       │
│    - 处方集(搜索批准的药物列表)                     │
│                                                     │
│  系统始终检查剂量是否在安全范围内。                   │
│  确认后,处方记录到审计数据库。                       │
└─────────────────────────────────────────────────────┘

从故事到任务的分解:

故事:开处方

任务1:修改已开药物的剂量

任务2:处方集选药

任务3:剂量安全检查

查询最大/最小推荐剂量

与处方剂量对比

超出范围则显示错误

优点与缺点:

方面 优点 缺点
可读性 普通人容易理解,不需要懂技术 难以判断故事是否覆盖了所有需求
灵活性 需求变化时,只需增减/修改卡片 有经验的用户可能省略"理所当然"的细节
沟通 促进开发者和客户之间的对话 单张故事可能无法反映真实工作流程的全貌

3.2 重构(Refactoring)

传统观点:设计时就要考虑未来的变化(“设计为变化”)。
XP的观点:别浪费时间猜未来,但要持续整理代码,保持它干净可读。
什么是重构?
重构 = 在不改变程序外部行为的前提下,改善代码的内部结构。
比喻:就像整理房间——东西功能没变,但摆放更整齐、找起来更方便。
常见重构操作举例:

重构前:
─────────────────────────────────────────────────
// 代码中到处重复同样的逻辑
void checkDoseA() {
    // 50行剂量检查代码
}
void checkDoseB() {
    // 同样的50行剂量检查代码(复制粘贴的)
}
─────────────────────────────────────────────────
重构后:
─────────────────────────────────────────────────
// 提取为公共函数,消除重复
bool checkDose(double dose, double minDose, double maxDose) {
    return dose >= minDose && dose <= maxDose;
}
void checkDoseA() { checkDose(...); }
void checkDoseB() { checkDose(...); }
─────────────────────────────────────────────────

为什么要重构?
增量开发有个自然规律:每次修改都会让代码结构变得更乱(就像每次取东西后不整理,房间会越来越乱)。重构就是持续"整理房间"的过程。
重构的局限性:

  • 开发压力大时,重构会被推迟
  • 有些架构级别的问题,代码级别的重构无法解决

3.3 测试驱动开发(Test-First Development / TDD)

核心思想:先写测试,再写代码。
这与传统方式完全相反!

传统方式:
  编写代码 → 编写测试 → 运行测试 → 修复Bug
TDD方式:
  编写测试 → 运行测试(必然失败) → 编写代码 → 再次运行测试(通过)→ 重构

为什么这样做有效?

  1. 写测试时,你必须先想清楚"这个功能应该做什么",也就是先定义好接口和行为规范
  2. 写完代码就能立刻验证,不会"欠债"越来越多
  3. 避免"测试滞后(test-lag)":开发进度跑得比测试快,最后干脆不测了
    剂量检查的测试用例示例:
┌─────────────────────────────────────────────────────┐
│  测试4:剂量检查                                     │
│                                                     │
│  输入:                                             │
│    1. 单次剂量(毫克)                               │
│    2. 每日用药次数                                   │
│                                                     │
│  测试项:                                            │
│    1. 单次剂量正确但频率过高                          │
│    2. 单次剂量过高或过低                              │
│    3. 单次剂量×次数 结果过高或过低                    │
│    4. 单次剂量×次数 在允许范围内                      │
│                                                     │
│  期望输出:OK 或 错误提示(剂量超出安全范围)          │
└─────────────────────────────────────────────────────┘

C++演示:TDD风格的剂量检查

#include <iostream>
#include <string>
#include <cassert>
// ===== 步骤1:先写测试(此时功能函数还不存在)=====
// 剂量检查函数的声明(接口先定好)
std::string checkDose(double singleDose, int timesPerDay,
                      double minDaily, double maxDaily);
// ===== 步骤2:定义测试用例 =====
void runTests() {
    // 测试1:总剂量在安全范围内 → 应返回 "OK"
    assert(checkDose(10.0, 3, 20.0, 40.0) == "OK");
    // 测试2:单次剂量正常,但次数过多,总量超标 → 应返回错误
    assert(checkDose(10.0, 5, 20.0, 40.0) == "ERROR: dose too high");
    // 测试3:单次剂量本身就太低 → 应返回错误
    assert(checkDose(1.0, 3, 20.0, 40.0) == "ERROR: dose too low");
    // 测试4:单次剂量本身就太高 → 应返回错误
    assert(checkDose(50.0, 1, 20.0, 40.0) == "ERROR: dose too high");
    std::cout << "所有测试通过!" << std::endl;
}
// ===== 步骤3:实现功能函数(让测试通过)=====
std::string checkDose(double singleDose, int timesPerDay,
                      double minDaily, double maxDaily) {
    double totalDaily = singleDose * timesPerDay; // 计算每日总剂量
    if (totalDaily < minDaily) {
        return "ERROR: dose too low";   // 总剂量低于最低安全线
    }
    if (totalDaily > maxDaily) {
        return "ERROR: dose too high";  // 总剂量超过最高安全线
    }
    return "OK"; // 在安全范围内
}
int main() {
    runTests(); // 运行所有测试
    return 0;
}

测试驱动开发的问题:

  1. 程序员更喜欢写代码,不喜欢写测试,容易偷懒写不完整的测试
  2. 复杂UI界面的测试很难用单元测试覆盖
  3. 测试数量多不等于覆盖率高,可能存在死角

3.4 结对编程(Pair Programming)

做法:两个程序员坐在同一台电脑前,一起编写代码。一人"开车"(写代码),一人"导航"(审查、思考)。

     程序员A(开车)             程序员B(导航)
     ┌──────────┐               ┌──────────┐
     │          │               │          │
     │  键盘    │←─────────────→│  思考    │
     │  鼠标    │  实时交流      │  审查    │
     │          │               │          │
     └──────────┘               └──────────┘
              ↓
         同一台电脑
              ↓
         同一段代码

三大优点:

  1. 集体所有权:每个人都了解系统的各个部分,不会出现"只有张三懂这块代码"的情况
  2. 隐式代码审查:每行代码至少被两人看过,Bug更难逃过
  3. 促进重构:因为改进代码对自己和搭档都有直接好处,所以更愿意花时间重构
    关于效率的研究结论:
  • 学生实验:结对编程的生产率与两人分开工作相当(错误减少,节省了后期调试时间)
  • 资深程序员实验:有一定生产率损失,质量有所提升,但未能完全补偿损耗
  • 最重要的价值:知识在团队间传播,降低了"某人离职导致项目瘫痪"的风险

4. Scrum — 敏捷项目管理框架

4.1 Scrum 是什么?

Scrum 是一个项目管理框架,它告诉你如何组织团队、如何规划工作,但不规定具体怎么写代码。

比喻:Scrum就像橄榄球比赛的规则手册——它规定了位置、规则、计分方式,但不告诉每个球员具体怎么跑位。

4.2 Scrum 专业术语表


Scrum术语 通俗理解
产品待办列表(Product Backlog) 所有"待做事项"的清单,按优先级排列
冲刺(Sprint) 一个固定时长(2~4周)的开发迭代周期
冲刺待办列表(Sprint Backlog) 本次Sprint要完成的具体任务清单
可交付产品增量(Shippable Increment) Sprint结束后产出的、可以给用户用的软件版本
产品负责人(Product Owner) 代表客户,决定"做什么"和优先级
ScrumMaster 保证Scrum流程顺利运行,类似教练,不是传统意义上的项目经理
开发团队(Development Team) 3~7人的自组织技术团队
速度(Velocity) 团队在一个Sprint能完成多少工作量(经验值)
每日站会(Daily Scrum) 每天约15分钟的同步会议

4.3 Scrum Sprint 完整流程

产品待办列表
Product Backlog

Sprint规划
选择本次任务

Sprint执行
2~4周

每日站会
同步进展

Sprint评审
演示成果

可交付产品增量
Shippable Software

Sprint回顾
改进流程

4.4 Sprint 的详细运作

Sprint规划会(第1天):

产品负责人:         开发团队:
  介绍优先级最高       评估每项任务的工作量
  的需求条目     →     参考上次Sprint的速度
                       决定本次能做多少
                  ↓
             生成Sprint待办列表

每日站会(站着开,限时15分钟):
每个人回答三个问题:

  1. 昨天做了什么?
  2. 今天要做什么?
  3. 有什么障碍?
    Sprint结束(评审+回顾):
评审会议(面向外部):
  演示可运行的软件
  收集客户/利益相关方的反馈
  更新产品待办列表
回顾会议(面向内部):
  我们的工作方式哪里好?
  哪里需要改进?
  下次Sprint怎么做得更好?

4.5 Scrum 的成功之处

为什么用Scrum的团队反馈很好?

  1. 工作被拆成小块,利益相关方可以清楚看到进展
  2. 不稳定的需求不会阻塞整体进度
  3. 团队对所有事情都有可见性,士气和沟通更好
  4. 客户每隔几周就能看到实际运行的软件,没有"最后一刻的大惊喜"
  5. 客户和开发者之间建立信任,大家都相信项目会成功

4.6 分布式 Scrum

Scrum最初是为同一地点的小团队设计的,但现实中团队可能分布在全球。

                   ┌─────────────────────────────────────────┐
                   │            分布式 Scrum                 │
                   └─────────────────────────────────────────┘
  产品负责人(美国)                         开发团队(中国/印度)
  ┌──────────────┐    视频会议/即时通讯    ┌──────────────────┐
  │              │ ←───────────────────→ │                  │
  │ 定义需求     │                        │ ScrumMaster在场  │
  │ 定期访问团队 │                        │ 统一开发环境     │
  └──────────────┘                        │ 持续集成         │
                                          └──────────────────┘
  关键要求:
  ✓ ScrumMaster 与开发团队在一起(了解日常问题)
  ✓ 产品负责人定期访问开发团队(建立信任)
  ✓ 统一的开发环境(所有人看到同样的代码状态)
  ✓ 持续集成(任何时候都能了解产品状态)
  ✓ 实时通讯工具(即时消息、视频通话)

5. 扩展敏捷方法

5.1 扩展面临的两个挑战

扩展挑战
├── 纵向扩展(Scale Up)
│     └── 一个小团队 → 多个大团队协同开发同一个大系统
│
└── 横向扩展(Scale Out)
      └── 几个项目用敏捷 → 整个大公司都用敏捷

5.2 大型系统的6大复杂性特征

大型软件系统
的复杂性

系统之系统
多个子系统集成

棕地开发
与已有老系统交互

系统配置
而非全新开发

监管约束
必须满足法规要求

漫长的采购周期
团队成员来来去去

多元利益相关方
需求互相冲突

通俗解释每一个:

  1. 系统之系统:不是一个独立软件,而是多个系统拼在一起,A组开发的东西要和B组的对接
  2. 棕地开发(Brownfield):不是在空地上建新楼,而是要在已有建筑旁边扩建,还得保证原来的不受影响
  3. 系统配置:很多工作是"把现成零件拼起来"而不是"从零造零件"
  4. 监管约束:航空、医疗、金融软件必须通过监管审查,必须有详细文档
  5. 漫长周期:项目做3年,参与的人换了好几拨,知识传承是难题
  6. 多元利益:医院系统里,护士、医生、管理员、院长对同一个功能有完全不同的要求

5.3 纯敏捷方法在大型项目中的局限


敏捷原则 实际问题
客户全程参与 客户代表还有其他工作,无法全职配合;监管机构更不可能派人蹲点
拥抱变化 多方利益相关者对变化的优先级判断完全不同
增量交付 业务规划和市场部门需要提前几个月知道功能,无法接受"两周后见"
保持简洁 交付压力下,没时间做"值得做但没有立刻价值"的简化
人而非流程 不是每个人都适合敏捷要求的高度协作和持续沟通

5.4 敏捷 + 计划驱动的混合方法

现实中,大型项目往往是两者的混合:

                  纯计划驱动
                      │
                      │  ← 安全关键系统(飞机控制)
                      │  ← 长期合同、法规严格
                      │
              ─────── 混合区域 ───────
                      │  ← 大多数企业软件
                      │  ← 部分使用Scrum
                      │  ← 有一定的前期文档
                      │
                      │  ← App开发
                      │  ← 互联网产品
                  纯敏捷

如何决定用哪种方式? 以下是关键考量因素:

选择方法

系统相关问题

团队相关问题

组织相关问题

系统多大?
越大越需要计划驱动

什么类型?
实时系统需要深入分析

预期寿命多长?
长期系统需要更多文档

是否受外部监管?
受监管则需详细文档

团队技术水平如何?
水平低则需更多指导

团队是否分布式?
分布式需要设计文档

有哪些工具支持?
工具差则需更多文档

合同是否要求详细规格?
有则必须用计划驱动

客户能否增量参与?
不能则无法用纯敏捷

公司文化是什么?
传统工程文化阻力大

5.5 IBM 的敏捷扩展模型(ASM)

IBM 提出了一个三层的扩展模型:

          第三层:规模化敏捷(Agility at Scale)
          ┌─────────────────────────────────────┐
          │  处理:分布式开发、复杂遗留环境、     │
          │        监管合规                      │
          └───────────────┬─────────────────────┘
                          │ 建立在...之上
          第二层:纪律化敏捷交付(Disciplined Agile Delivery)
          ┌─────────────────────────────────────┐
          │  处理:大团队规模、地理分布、法规要求 │
          │  特点:自组织 + 适当治理框架          │
          └───────────────┬─────────────────────┘
                          │ 建立在...之上
          第一层:核心敏捷开发(Core Agile Development)
          ┌─────────────────────────────────────┐
          │  基本的Scrum/XP实践                  │
          │  价值驱动、自组织小团队              │
          └─────────────────────────────────────┘

5.6 大规模敏捷的5个关键做法

做法1:不能完全依赖增量式需求
大系统需要一定量的前期需求工作,用来划分子系统边界和形成合同依据。但细节仍然增量细化。
做法2:多个客户代表
不同的子系统需要不同的产品负责人,他们之间需要持续沟通协调。
做法3:更多前期设计和文档
必须设计软件架构,必须文档化关键的数据库模式、团队分工等。
做法4:跨团队沟通机制

沟通工具:
  ✓ 定期视频会议
  ✓ 频繁的短会(各团队互相更新进度)
  ✓ Wiki / 知识库
  ✓ 即时通讯
  ✓ 邮件
  ✓ 社交软件

做法5:频繁构建与发布对齐
虽然大系统无法做到"任何人提交代码就立刻集成全系统",但要保持频繁构建,并且各团队的发布节奏要对齐。

5.7 多团队 Scrum(Scrum of Scrums)

          ┌─────────────────────────────────────┐
          │           Scrum of Scrums           │
          │  每日从各团队各派一名代表参加的站会   │
          └──────────────┬──────────────────────┘
                         │
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
   Scrum团队A        Scrum团队B        Scrum团队C
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │ PO  + SM │     │ PO  + SM │     │ PO  + SM │
  │ 3~7人   │     │ 3~7人   │     │ 3~7人   │
  └──────────┘     └──────────┘     └──────────┘
      │                 │                 │
   子架构师           子架构师           子架构师
      └─────────────────┴─────────────────┘
                         │
                  架构师委员会
              (协作设计整体架构)

多团队Scrum的4个关键机制:

  1. 角色复制:每个团队都有自己的产品负责人和ScrumMaster
  2. 产品架构师:每个团队选出架构师,共同协作设计整体架构
  3. 发布对齐:各团队的发布时间对齐,确保整体可演示
  4. Scrum of Scrums:每日由各团队代表参加的协调站会

5.8 在大公司推广敏捷的障碍


障碍 说明
管理者的保守心理 项目经理担心"用了新方法,万一失败怎么办?"
现有流程标准冲突 公司规定"所有需求变更必须审批",这与重构矛盾
技能水平参差不齐 敏捷对人的要求高,低技能员工可能拖后腿
文化阻力 有几十年传统工程文化的公司,很难接受"代码比文档重要"

推广策略:
不要强制推行 → 找到热情的先行者 → 做出成功案例 → 让他们去影响其他人 → 在组织内逐步推广

6. 总结与关键点

                     敏捷软件开发 全景图
                     ═══════════════════
  为什么需要?
  ┌─────────────────────────────────────────────────┐
  │  需求快速变化 + 传统瀑布模型太慢 → 需要新方法    │
  └─────────────────────────────────────────────────┘
  核心价值观(敏捷宣言):
  ┌─────────────────────────────────────────────────┐
  │  个体交互 > 流程工具                              │
  │  可运行软件 > 完整文档                            │
  │  客户合作 > 合同谈判                              │
  │  响应变化 > 遵循计划                              │
  └─────────────────────────────────────────────────┘
  XP核心技术:
  ┌───────────┬───────────┬───────────┬───────────┐
  │ 用户故事  │  重构     │  TDD      │ 结对编程  │
  │(需求表达)│(保持清洁)│(先写测试)│(互相审查)│
  └───────────┴───────────┴───────────┴───────────┘
  Scrum框架:
  ┌─────────────────────────────────────────────────┐
  │  产品待办 → Sprint规划 → Sprint执行             │
  │         ← Sprint回顾 ← Sprint评审              │
  └─────────────────────────────────────────────────┘
  扩展问题与解决:
  ┌─────────────────────────────────────────────────┐
  │  小团队适合纯敏捷                                │
  │  大系统需要敏捷 + 计划驱动的混合                 │
  │  分布式团队需要额外的沟通机制                    │
  └─────────────────────────────────────────────────┘

一句话总结每个关键概念:


概念 一句话
敏捷方法 少写文档,多交付可运行的软件,快速响应变化
XP(极限编程) 把好的实践推向极致:更短的迭代、更多的沟通、更严格的测试
用户故事 用"某人想要做某事"的小故事代替长篇需求文档
重构 持续整理代码,不让技术债堆积
测试驱动开发 先写测试,再写代码,保证质量
结对编程 两人共用一台电脑,互相检查,共享知识
Scrum 用Sprint循环、产品待办列表和每日站会管理敏捷项目
扩展敏捷 大型项目需要混合敏捷和计划驱动的方法

第4章:需求工程 — 从零开始的完整理解

目录

  1. 什么是需求?基本概念
  2. 功能需求与非功能需求
  3. 需求工程的整体流程
  4. 需求获取(怎么发现需求)
  5. 需求规格说明(怎么写需求)
  6. 需求验证(怎么检查需求)
  7. 需求变更管理
  8. 总结与关键点

1. 什么是需求?基本概念

1.1 需求的定义

需求(Requirements) 就是"这个软件系统应该做什么、不该做什么、在什么约束条件下工作"的描述。

打个比方:你去餐厅点菜,你对服务员说"我要一份不辣的宫保鸡丁,少油,不加花生"——这就是你对这道菜的"需求"。软件需求也是类似的,只不过描述的是一个软件系统。
需求工程(Requirements Engineering,RE) 就是"找出、分析、记录、检查这些需求"的整个过程。

1.2 需求的两个层次

需求分两个层次,面向不同的读者:

                用户需求(User Requirements)
                ┌────────────────────────────────┐
                │  用自然语言写,给管理层和客户看  │
                │  抽象、高层次、不涉及技术细节    │
                └────────────────────────────────┘
                              │
                              │ 展开和细化
                              ↓
                系统需求(System Requirements)
                ┌────────────────────────────────┐
                │  给开发者和系统架构师看          │
                │  具体、详细、精确定义行为        │
                └────────────────────────────────┘

具体例子(Mentcare心理健康系统):

用户需求(1句话):
  "Mentcare系统应每月生成管理报告,显示当月每家诊所的药品费用。"
                        ↓ 展开为5条系统需求
1.1 每月最后一个工作日,生成药品处方摘要(含费用、诊所信息)
1.2 报告在当天17:30后生成,供打印
1.3 报告按诊所分类,列出药名、处方数、剂量数、总费用
1.4 不同剂量规格(如10mg、20mg)分别生成报告
1.5 药品费用报告仅限授权用户访问

1.3 利益相关者(Stakeholders)是谁?

利益相关者 = 任何与该系统有关联、有合法利益的人。
以Mentcare系统为例:

Mentcare系统的利益相关者
├── 直接用户
│     ├── 医生(评估和治疗病人)
│     ├── 护士(协调诊疗、执行部分治疗)
│     └── 前台(管理预约)
├── 间接用户
│     ├── 病人及家属(信息被记录在系统中)
│     └── 医疗记录人员(确保信息可维护)
└── 管理/监管
      ├── 医疗伦理主管(确保符合伦理准则)
      ├── 医疗管理人员(从系统获取管理数据)
      └── IT人员(安装和维护系统)

2. 功能需求与非功能需求

2.1 功能需求(Functional Requirements)

是什么: 系统"能做什么"的描述——具体功能、输入输出、行为。
Mentcare系统的功能需求例子:

1. 用户可以搜索所有诊所的预约列表
2. 系统每天为每家诊所生成当日预约病人名单
3. 每位使用系统的员工用8位工号唯一标识

功能需求的问题:歧义性
需求写得不清楚会导致争议。例如:

模糊需求:
  "用户应能搜索所有诊所的预约列表"
开发者的理解A(省事的实现):
  用户先选择诊所,再在该诊所内搜索
客户的真实意图:
  直接输入病人姓名,系统在所有诊所中搜索
→ 结果:双方争执,延误交付,增加成本

理想的功能需求应该做到:

  • 完整性(Completeness):所有用户需要的功能都有定义
  • 一致性(Consistency):需求之间没有矛盾

实际上,大型系统几乎不可能做到完全完整且一致,因为不同利益相关者的需求天然存在冲突。

2.2 非功能需求(Non-functional Requirements)

是什么: 系统"有多好"的约束——不是具体功能,而是整体质量特性。

比喻:功能需求是"这辆车能载人",非功能需求是"这辆车时速要达到120km,油耗不超过8L/100km,噪音低于60分贝"。
非功能需求比功能需求更关键!

  • 功能需求没实现 → 某个功能用不了,用户可以绕过
  • 非功能需求没实现 → 整个系统可能无法使用
    例如:飞机控制软件可靠性不达标 → 无法取得适航证 → 整个系统作废

2.3 非功能需求的分类树

非功能需求

产品需求
Product Requirements

组织需求
Organizational Requirements

外部需求
External Requirements

效率需求
性能/空间

可靠性需求
故障率/可用性

安全性需求

可用性需求
易用性

环境需求
操作环境

运营需求
使用方式

开发需求
编程语言/标准

监管需求
法规符合性

法律需求
隐私法/版权

伦理需求
用户可接受性

三类非功能需求的具体例子(Mentcare系统):

产品需求(可用性):
  "系统在正常工作时间(周一至周五,08:30–17:30)对所有诊所可用。
   在正常工作时间内,每天宕机时间不超过5秒。"
组织需求(身份认证):
  "Mentcare系统用户应使用医疗机构身份卡进行身份验证。"
外部需求(隐私法规):
  "系统应按照HStan-03-2006-priv标准中规定的病人隐私条款实施。"

2.4 非功能需求的常见问题:目标 vs 可测量需求

问题: 客户往往用模糊的"目标"表达非功能需求。

模糊目标(无法测试):
  "系统应该对医护人员易于使用,
   并以减少用户错误的方式组织。"
可测量需求(可以客观验证):
  "医护人员经过2小时培训后应能使用所有系统功能。
   培训后,有经验的用户每小时使用中平均错误不超过2次。"

可量化的非功能需求度量指标:

属性 度量指标
速度 每秒处理的事务数;用户/事件响应时间;屏幕刷新时间
大小 占用内存(MB);ROM芯片数量
易用性 培训时长;帮助页面数量
可靠性 平均故障时间(MTTF);不可用概率;故障发生率
健壮性 故障后重启时间;导致故障的事件比例;故障时数据损坏概率
可移植性 平台相关代码比例;目标系统数量

3. 需求工程的整体流程

3.1 需求工程的三大活动

需求获取(Elicitation)
  与利益相关者交互,发现需求
          ↓
需求规格说明(Specification)
  把需求转化为标准格式的文档
          ↓
需求验证(Validation)
  检查需求文档是否准确定义了客户真正想要的系统

这三个活动不是线性顺序,而是螺旋迭代的:

发现问题,返回修改

可行性研究

用户需求获取

用户需求规格说明

系统需求获取

系统需求规格说明与建模

需求验证
原型/评审

系统需求文档

3.2 可行性研究(Feasibility Study)

在正式开始需求工程之前,先做一个简短的可行性评估,回答三个问题:

1. 这个系统是否符合组织的整体目标?
2. 能否在预算和进度内用现有技术实现?
3. 能否与现有系统集成?
如果任何一个答案是"否" → 很可能不应该继续这个项目

4. 需求获取(怎么发现需求)

4.1 需求获取为什么困难?

5大难点:
┌─────────────────────────────────────────────────────┐
│ 1. 利益相关者自己也不清楚想要什么                    │
│    "我想要个更好的系统"——但具体什么是"更好"?        │
├─────────────────────────────────────────────────────┤
│ 2. 利益相关者用行业术语,需求工程师听不懂            │
│    医生说"SOAP笔记",工程师以为是肥皂...             │
├─────────────────────────────────────────────────────┤
│ 3. 不同利益相关者需求冲突                            │
│    护士想要简化输入,安全主管想要详细记录             │
├─────────────────────────────────────────────────────┤
│ 4. 政治因素干扰                                      │
│    某经理要求某功能,是为了增加自己在组织中的影响力   │
├─────────────────────────────────────────────────────┤
│ 5. 业务环境动态变化                                  │
│    分析过程中,公司被收购了,需求全变了...            │
└─────────────────────────────────────────────────────┘

4.2 需求获取的四步循环

下一轮迭代

1.需求发现与理解
与利益相关者交互

2.需求分类与组织
将散乱需求归类

3.需求优先级与协商
解决冲突

4.需求文档化
写成文档/白板/Wiki

4.3 技术一:访谈(Interviewing)

两种访谈形式:

封闭式访谈(Closed)          开放式访谈(Open)
─────────────────────         ─────────────────────
预先设计好问题清单             没有固定议程
按顺序逐一提问                 自由探讨各种话题
适合收集特定信息               适合理解整体工作背景

实际中通常混合使用。
访谈的技巧:

  1. 保持开放心态:不要先入为主地认为"需求应该是这样的"
  2. 使用"跳板问题":不要直接说"告诉我你想要什么"(太空泛),而是:
    • 展示一个原型系统,请用户评价
    • 描述一个使用场景,请用户确认是否符合实际
      访谈的局限性:
难以通过访谈获得的信息:
  × 领域专业知识(医生觉得"理所当然"的事不会主动说)
  × 组织内部的真实权力结构(面子问题,不愿透露)
  × 实际工作流程(与官方规定有差距,不愿承认)

4.4 技术二:民族志(Ethnography)

是什么: 需求分析师像"人类学家"一样,深入到用户的工作环境中,长期观察用户如何实际工作,而不只是听他们说。

比喻:与其问厨师"你是怎么炒菜的",不如直接站在厨房旁边看他炒一天。
为什么有效?
人们往往无法清楚描述自己熟悉的工作,但观察者可以发现他们自己意识不到的隐性需求。
民族志的两个特别发现场景:

场景1:实际工作 vs 规定流程
  规定:空管员必须打开飞行冲突预警系统
  实际:预警太敏感,频繁误报,空管员悄悄把它关掉
  → 真实需求:需要一个敏感度可调节的预警系统
场景2:团队协作意识
  实际:空管员会偷瞄邻扇区的工作状态,预测进入自己扇区的飞机数量
  → 真实需求:系统应允许一个扇区的管制员看到相邻扇区的情况

民族志结合原型开发的流程:

民族志分析
观察用户工作

开发初始原型

原型评估
用户试用

汇报会议
讨论发现的问题

有针对性的民族志
专门观察问题点

民族志的局限:

  • 只关注现有工作方式,对创新帮助有限
  • 苹果开发iPhone时不是研究现有手机怎么用,而是重新想象手机应该是什么

4.5 技术三:故事与场景(Stories & Scenarios)

故事(Story) = 高层次的叙述性描述,说明某人如何使用系统完成某项任务
场景(Scenario) = 更结构化、更详细的交互描述
故事示例(iLearn数字学习平台):

┌────────────────────────────────────────────────────────────┐
│  课堂中的照片分享                                           │
│                                                            │
│  Jack是苏格兰乌拉浦地区一所小学的老师。他决定让班级         │
│  做一个关于当地渔业的项目。学生需要收集老照片,           │
│  记录家人的回忆。Jack需要一个照片分享网站,让学生          │
│  上传照片并互相评论。                                       │
│                                                            │
│  Jack发邮件给教师群,有人推荐了KidsTakePics——一个          │
│  允许教师审核内容的照片分享网站。由于该网站未与            │
│  iLearn认证服务集成,他单独建立了账号,然后在             │
│  iLearn设置中将其添加到班级学生的可见服务中。              │
└────────────────────────────────────────────────────────────┘

场景示例(更详细的结构化描述):

┌────────────────────────────────────────────────────────────┐
│  场景:向KidsTakePics上传照片                              │
│                                                            │
│  初始状态:                                                │
│    用户已登录系统,照片保存在平板或笔记本上               │
│                                                            │
│  正常流程:                                                │
│    1. 用户选择要上传的照片                                 │
│    2. 选择存储项目名称,可选输入关键词                     │
│    3. 系统自动命名(用户名+本地文件名)                    │
│    4. 上传完成,系统发邮件给审核员,屏幕显示等待审核提示   │
│                                                            │
│  出错情况:                                                │
│    - 项目没有审核员 → 自动邮件给管理员,提示用户可能有延迟 │
│    - 同名照片已存在 → 询问覆盖/重命名/取消                 │
│                                                            │
│  其他并发活动:审核员可能正在登录,同步审核                │
│                                                            │
│  结束状态:照片已上传,状态为"等待审核",仅用户和审核员可见│
└────────────────────────────────────────────────────────────┘

故事 vs 场景 的区别:

方面 故事 场景
形式 叙述性文字,像讲故事 结构化模板,有固定字段
细节 高层次,大局观 详细,包含异常处理
用途 发起讨论,获取初步需求 细化需求,发现边界情况
受众 广大用户社区 开发团队和系统设计者

5. 需求规格说明(怎么写需求)

5.1 四种写需求的方式


方式 说明 适用场景
自然语言句子 编号的自然语言语句,每句一个需求 用户需求,适合所有读者
结构化自然语言 按标准模板填写,字段化 系统需求,更规范
图形化符号(UML) 用例图、顺序图等 描述交互流程
数学规格说明 基于有限状态机、集合论等 安全关键系统(客户难以审核)

5.2 自然语言写需求的5条规范

  1. 统一格式:所有需求用相同的结构写,减少遗漏
  2. 区分"必须"和"应该"
    • shall(必须)= 强制性需求
    • should(应该)= 可选的期望需求
  3. 高亮关键词:用粗体、斜体或颜色标注重要部分
  4. 避免技术术语:普通读者看不懂"模块"、"架构"等词
  5. 附上原因:每条需求说明为什么需要它,是谁提出的
    示例(胰岛素泵软件需求):
3.2  系统**应(shall)**每10分钟测量一次血糖,并在需要时输送胰岛素。
     (血糖变化较慢,更频繁的测量没有必要;更长的间隔可能导致血糖过高。)
3.6  系统**应(shall)**每分钟运行一次自检程序,需要测试的条件和对应
     操作在表1中定义。(自检程序可以发现硬件和软件问题,提醒用户正常
     操作可能无法进行。)

5.3 结构化规格说明(表单式写法)

用标准表格模板来写需求,减少歧义:
胰岛素泵需求的表单写法示例:

┌──────────────────────────────────────────────────────────────┐
│ 编号:胰岛素泵/控制软件/SRS/3.3.2                            │
├──────────┬───────────────────────────────────────────────────┤
│ 功能     │ 计算胰岛素剂量:安全血糖水平                      │
├──────────┼───────────────────────────────────────────────────┤
│ 描述     │ 当前血糖在安全范围(3~7单位)内时,计算应输送的    │
│          │ 胰岛素剂量                                        │
├──────────┼───────────────────────────────────────────────────┤
│ 输入     │ 当前血糖读数r2,前两次读数r0和r1                  │
├──────────┼───────────────────────────────────────────────────┤
│ 来源     │ r2来自传感器,r0/r1来自内存                       │
├──────────┼───────────────────────────────────────────────────┤
│ 输出     │ CompDose——应输送的胰岛素剂量                      │
├──────────┼───────────────────────────────────────────────────┤
│ 动作     │ 见下方逻辑说明(参见表格4.14)                    │
├──────────┼───────────────────────────────────────────────────┤
│ 前置条件 │ 胰岛素储液器中至少有最大单次剂量的胰岛素          │
├──────────┼───────────────────────────────────────────────────┤
│ 后置条件 │ r0被r1替换,r1被r2替换                           │
├──────────┼───────────────────────────────────────────────────┤
│ 副作用   │ 无                                                │
└──────────┴───────────────────────────────────────────────────┘

5.4 表格化复杂逻辑

当有多种条件对应不同行为时,用表格比文字清晰得多。
胰岛素计算逻辑(对应上面表单中的"动作"部分):
r 0 , r 1 , r 2 r_0, r_1, r_2 r0,r1,r2 分别为前前次、前次、本次血糖读数, CompDose \text{CompDose} CompDose 为计算出的胰岛素剂量。

条件 动作
血糖下降( r 2 < r 1 r_2 < r_1 r2<r1 CompDose = 0 \text{CompDose} = 0 CompDose=0
血糖平稳( r 2 = r 1 r_2 = r_1 r2=r1 CompDose = 0 \text{CompDose} = 0 CompDose=0
血糖上升,但增速减慢( ( r 2 − r 1 ) < ( r 1 − r 0 ) (r_2-r_1) < (r_1-r_0) (r2r1)<(r1r0) CompDose = 0 \text{CompDose} = 0 CompDose=0
血糖上升,且增速不减( r 2 > r 1 r_2>r_1 r2>r1 ( r 2 − r 1 ) ≥ ( r 1 − r 0 ) (r_2-r_1) \geq (r_1-r_0) (r2r1)(r1r0) CompDose = round ( r 2 − r 1 4 ) \text{CompDose} = \text{round}\left(\frac{r_2-r_1}{4}\right) CompDose=round(4r2r1),若结果为0则取最小剂量

用C++演示上述剂量计算逻辑:

#include <iostream>
#include <cmath>    // 提供 round() 函数
#include <algorithm> // 提供 max()
// 胰岛素泵的剂量计算函数
// 参数说明:
//   r0: 前前次血糖读数
//   r1: 前次血糖读数
//   r2: 当前血糖读数
//   minDose: 系统能输送的最小剂量(避免剂量四舍五入后变为0)
// 返回值:应输送的胰岛素剂量(0表示不输送)
double computeInsulinDose(double r0, double r1, double r2, double minDose) {
    // 情况1:血糖在下降(当前 < 前次)→ 不需要胰岛素
    if (r2 < r1) {
        return 0.0;
    }
    // 情况2:血糖平稳(当前 == 前次)→ 不需要胰岛素
    if (r2 == r1) {
        return 0.0;
    }
    // 情况3:血糖在上升(r2 > r1),但增速在减缓
    // 本次增量 = r2 - r1,上次增量 = r1 - r0
    // 如果本次增量 < 上次增量,说明上升速度在变慢
    double currentIncrease = r2 - r1;  // 本次变化量
    double previousIncrease = r1 - r0; // 上次变化量
    if (currentIncrease < previousIncrease) {
        return 0.0; // 增速减慢,暂不需要额外胰岛素
    }
    // 情况4:血糖上升,且增速稳定或加快 → 需要计算剂量
    // 公式:CompDose = round((r2 - r1) / 4)
    double compDose = std::round(currentIncrease / 4.0);
    // 如果计算结果四舍五入后为0,使用最小剂量
    if (compDose == 0.0) {
        compDose = minDose;
    }
    return compDose;
}
int main() {
    double minDose = 0.5; // 假设最小剂量为0.5单位
    // 测试案例1:血糖下降(6 → 5.5 → 5)
    double dose1 = computeInsulinDose(6.0, 5.5, 5.0, minDose);
    std::cout << "案例1(血糖下降):剂量 = " << dose1 << std::endl; // 期望:0
    // 测试案例2:血糖平稳(5 → 5 → 5)
    double dose2 = computeInsulinDose(5.0, 5.0, 5.0, minDose);
    std::cout << "案例2(血糖平稳):剂量 = " << dose2 << std::endl; // 期望:0
    // 测试案例3:血糖上升但增速减慢(4 → 5.5 → 6,增量从1.5降到0.5)
    double dose3 = computeInsulinDose(4.0, 5.5, 6.0, minDose);
    std::cout << "案例3(增速减慢):剂量 = " << dose3 << std::endl; // 期望:0
    // 测试案例4:血糖快速上升(4 → 5 → 7,增量从1升到2)
    double dose4 = computeInsulinDose(4.0, 5.0, 7.0, minDose);
    // 计算:currentIncrease = 7-5 = 2,CompDose = round(2/4) = round(0.5) = 1
    std::cout << "案例4(快速上升):剂量 = " << dose4 << std::endl; // 期望:1
    // 测试案例5:血糖缓慢上升(5 → 5.5 → 6,增量从0.5到0.5,稳定)
    double dose5 = computeInsulinDose(5.0, 5.5, 6.0, minDose);
    // 计算:currentIncrease = 0.5,CompDose = round(0.5/4) = round(0.125) = 0 → 取最小剂量
    std::cout << "案例5(缓慢上升):剂量 = " << dose5 << std::endl; // 期望:0.5(最小剂量)
    return 0;
}

5.5 用例图(Use Cases)

用例图用图形方式描述"谁能用系统做什么":

Mentcare系统用例图:
   医疗前台                          医生
   ┌──────┐                          ┌──────┐
   │      │──→ [注册病人]            │      │──→ [设置会诊]
   └──────┘                          └──────┘
                                           │
   护士                              [编辑记录]
   ┌──────┐                               │
   │      │──→ [查看记录]           [查看记录]
   └──────┘        ↑                      ↑
                   │                      │
   管理员          └──────────────────────┘
   ┌──────┐
   │      │──→ [导出统计数据]
   │      │──→ [生成报告]
   └──────┘

用例(Use Case)的标准文字描述示例:

用例名称:设置会诊(Setup Consultation)
描述:
  允许两个或更多医生在不同办公室同时查看同一病人记录。
  发起者从在线医生的下拉菜单中选择参与人员。
  病人记录显示在所有参与者的屏幕上,但只有发起者可以编辑记录。
  同时创建一个文本聊天窗口,用于协调操作。
  语音通话需要另行通过电话安排。

5.6 软件需求文档(SRS)的标准结构

需求文档的推荐章节结构(基于IEEE标准):

需求文档结构
├── 前言(版本历史,预期读者)
├── 引言(系统目的,与其他系统的关系,业务目标)
├── 术语表(技术术语定义,不假设读者背景)
├── 用户需求定义(自然语言+图表,面向客户)
├── 系统架构(高层次架构概述,模块分布)
├── 系统需求规格说明(详细功能/非功能需求)
├── 系统模型(对象模型、数据流图、语义数据模型)
├── 系统演化(未来变化的预判,帮助设计者避免过度约束)
├── 附录(硬件规格、数据库描述等)
└── 索引(字母索引、图表索引、功能索引)

需求文档的读者群体:

读者 使用方式
系统客户 检查需求是否符合自身需要,提出变更
项目经理 用于投标报价和制定开发计划
系统工程师 理解要开发什么系统
测试工程师 基于需求制定验收测试
维护工程师 理解系统各部分之间的关系

6. 需求验证(怎么检查需求)

6.1 为什么需求验证至关重要?

错误发现得越晚,修复代价越大:
需求阶段发现错误   → 修改文档,代价:低
设计阶段发现错误   → 修改设计+文档,代价:中
编码阶段发现错误   → 修改代码+设计+文档,代价:高
测试阶段发现错误   → 修改代码+重新测试,代价:很高
上线后发现错误     → 修改代码+重新部署+影响用户,代价:极高

6.2 验证时要做的5类检查


检查类型 检查内容 示例问题
有效性检查 需求是否反映用户真实需要? “这个功能用户真的需要吗?还是分析员猜的?”
一致性检查 需求之间有没有矛盾? “A处说数据只读,B处说可以编辑,矛盾!”
完整性检查 是否覆盖了所有需要的功能和约束? “登录功能写了,注销功能忘了写”
现实性检查 在预算/进度内能否实现? “要求1ms响应时间,但技术上做不到”
可验证性检查 是否能写出测试来验证这条需求? "易于使用"无法测试;"错误率<2次/小时"可以测试

6.3 三种验证技术

技术1:需求评审
一组人(客户方+开发方)坐在一起,逐条审查需求文档,找出错误和不一致。
技术2:原型验证
做一个可运行的原型系统,让真实用户试用,通过实际操作验证需求是否准确。
技术3:测试用例生成
为每条需求预先写出测试用例。如果一条需求很难写出测试用例,说明这条需求本身有问题,需要重新考虑。

这实际上就是TDD(测试驱动开发)中"先写测试"的基础思想。

7. 需求变更管理

7.1 需求为什么总在变?

需求变更的三大根源:
1. 外部环境变化
   ├── 新的硬件/平台出现
   ├── 法规修订(必须合规)
   └── 与新系统集成的需求
2. 付钱的人 ≠ 使用的人
   ├── 客户基于预算设定了某些约束
   └── 上线后真实用户反馈完全不同
3. 多方利益相关者的博弈
   ├── 需求是各方妥协的结果
   └── 实际使用后发现原有优先级不合理

7.2 需求变更管理流程

批准

拒绝

发现问题
或变更请求

问题分析
变更规格说明
确认请求是否有效

变更分析与成本估算
评估影响范围和代价

是否批准?

变更实施
更新需求文档/设计/代码

记录拒绝原因
反馈给请求者

更新后的需求文档

7.3 变更管理的三个阶段

阶段1:问题分析和变更规格说明
收到变更请求后,先分析这个变更是否真的有必要,然后请求者根据分析结果提交更具体的变更提案。
阶段2:变更分析和成本估算

  • 评估这个变更会影响哪些其他需求(追溯性)
  • 估算修改需求文档、设计、代码的工作量和成本
  • 决定是否实施这个变更
    阶段3:变更实施
    按批准的方案修改需求文档,必要时修改设计和实现。

重要警告: 紧急情况下可能先改代码后改文档,但这非常危险!一定要尽快补上需求文档的更新,否则文档和代码将越来越不同步。

7.4 需求管理规划的四个决策


决策事项 说明
需求标识 每条需求有唯一ID,便于追踪和引用
变更管理流程 定义如何评估变更的影响和成本
追溯性策略 记录需求与需求之间、需求与设计之间的关联关系
工具支持 选择合适的需求管理工具(小系统用Excel,大系统用DOORS)

7.5 持久需求 vs 易变需求

持久需求(Enduring Requirements)
  └── 与组织核心业务活动相关
  └── 变化缓慢
  └── 例:"系统应记录所有病人的诊疗记录"
易变需求(Volatile Requirements)
  └── 与辅助性工作流程相关
  └── 容易随组织变化而变化
  └── 例:"报告应按照当前ISO格式生成"(标准可能更新)

8. 总结与关键点

               需求工程 全景总结
               ═══════════════════
  什么是需求?
  ┌────────────────────────────────────────────────┐
  │  用户需求(抽象,给管理层看)                   │
  │  系统需求(详细,给开发者看)                   │
  └────────────────────────────────────────────────┘
  需求类型:
  ┌──────────────────┬─────────────────────────────┐
  │ 功能需求          │ 非功能需求                  │
  │ 系统做什么        │ 系统做得有多好              │
  │ 具体功能行为      │ 性能/可靠性/安全/易用性等   │
  └──────────────────┴─────────────────────────────┘
  需求工程过程(螺旋迭代):
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │ 需求获取  │ →  │ 需求规格  │ →  │ 需求验证  │
  │ 访谈     │    │ 自然语言  │    │ 评审     │
  │ 民族志   │    │ 结构化    │    │ 原型     │
  │ 故事/场景│    │ 用例图   │    │ 测试用例 │
  └──────────┘    └──────────┘    └──────────┘
  需求变更管理:
  ┌────────────────────────────────────────────────┐
  │  问题识别 → 分析成本 → 批准/拒绝 → 实施变更    │
  └────────────────────────────────────────────────┘

一句话总结每个关键概念:


概念 一句话
用户需求 管理层和客户看的,用通俗语言写,描述"想要什么"
系统需求 开发者看的,详细精确,描述"具体怎么做"
功能需求 系统能做什么具体功能
非功能需求 系统性能/质量约束,往往比功能需求更关键
需求获取 通过访谈、民族志、故事场景等方式发现需求
需求规格说明 将需求用自然语言、结构化模板或UML图正式记录
需求验证 通过评审、原型、测试用例检查需求是否正确完整
需求变更管理 对所有变更请求进行系统化的影响分析和成本评估

第5章:系统建模 — 从零开始的完整理解

目录

  1. 什么是系统建模?
  2. 上下文模型(Context Models)
  3. 交互模型(Interaction Models)
  4. 结构模型(Structural Models)
  5. 行为模型(Behavioral Models)
  6. 模型驱动架构(MDA)
  7. 总结与关键点

1. 什么是系统建模?

1.1 核心概念

系统建模就是用图形符号画出软件系统的"示意图",帮助人们理解、讨论、设计和记录软件。

比喻:建筑师在盖楼之前会画平面图、立面图、结构图。每张图展示建筑的不同侧面。软件建模也是一样——用不同的图展示软件的不同方面。
模型 vs 完整描述:

完整描述(Representation):保留所有信息
  → 把书从英文翻译成中文,信息量不变
模型(Model/Abstraction):有意省略细节,突出重点
  → 把书的要点做成PPT幻灯片,只保留最关键的内容

模型是一种抽象,不是完整的系统描述。它故意简化,目的是让人更容易理解。

1.2 建模的三种用途

用途1:促进讨论
  ─────────────────────────────────────────────
  不要求完整,也不要求严格符合符号规范
  只要能引发有价值的讨论就好
  (敏捷建模中常用这种方式)
用途2:记录现有系统
  ─────────────────────────────────────────────
  不需要完整(只需覆盖关键部分)
  但必须正确(符号使用准确,描述无误)
用途3:生成系统实现
  ─────────────────────────────────────────────
  必须完整且正确
  模型将被工具自动转换为代码
  (模型驱动开发,MDD)

1.3 四种建模视角

一个系统可以从四个角度来描述:

软件系统

上下文视角
Context
系统在环境中的位置

交互视角
Interaction
系统与外部/内部的交互

结构视角
Structure
系统的静态组织结构

行为视角
Behavior
系统对事件的动态响应

1.4 UML——统一建模语言

UML(Unified Modeling Language) 是软件建模的国际标准语言,共有13种图表类型。
本章重点介绍最常用的5种:

图类型 用途 所属视角
活动图(Activity Diagram) 展示流程中的活动和数据流 上下文/行为
用例图(Use Case Diagram) 展示系统与外部参与者的交互 交互
顺序图(Sequence Diagram) 展示对象间交互的时序 交互
类图(Class Diagram) 展示系统中的类和关联关系 结构
状态图(State Diagram) 展示系统对事件的状态响应 行为

2. 上下文模型(Context Models)

2.1 什么是上下文模型?

上下文模型回答一个问题:“这个系统的边界在哪里?它与哪些外部系统打交道?”
在开始设计之前,必须先划定系统边界:

  • 哪些功能在这个系统里实现?
  • 哪些功能由其他系统承担?
  • 这个系统依赖哪些外部系统?

2.2 Mentcare系统的上下文模型

Mentcare是一个心理健康病人信息系统。它需要与多个外部系统交互:

                    ┌──────────────────────────────────────────────┐
                    │              Mentcare系统的外部环境            │
                    └──────────────────────────────────────────────┘
   «system»                «system»              «system»
  管理报告系统  ─────────  Mentcare  ──────────  病人记录系统
                           系统
                        ╱  │  │  ╲
                       ╱   │  │   ╲
          «system»   ╱    │   │    ╲   «system»
         预约系统 ──╯     │    │     ╲── 统计系统
                         │    │
                  «system»    «system»
                  入院系统     处方系统

上下文模型的局限: 只显示"有哪些外部系统",不显示关系的类型(是共享数据?是单向输入?是双向通信?)。所以通常还需要配合活动图来补充说明。

2.3 活动图:展示业务流程

UML活动图用于展示系统在某个业务流程中的角色。
活动图符号说明:

● ─── 实心圆 = 流程起点
◎ ─── 双圆 = 流程终点
┌──┐
│  │ ─── 圆角矩形 = 活动(具体子流程)
└──┘
─── 箭头 = 控制流(从一个活动到下一个)
─ ─── 粗横线 = 活动协调(上方所有活动完成后才能继续,或同时触发多个活动)
[条件] = 方括号内是守卫条件(决定走哪条分支)

强制收押病人的业务流程(活动图):

             ●
             │
             ↓
     ┌─────────────┐
     │  确认收押决定 │  ← Mentcare系统参与
     └──────┬──────┘
            │
      ┌─────┴─────┐
      │[危险]      │[不危险]
      ↓            ↓
┌──────────┐  ┌──────────┐
│查找安全场所│  │通知病人权利│ ← Mentcare系统参与
└────┬─────┘  └────┬─────┘
     │              │
 ┌───┴──┐           │
 │[有空位]│[没有空位]  │
 ↓       ↓           ↓
转送  转送      ┌────────────┐
安全  警察      │    并发活动  │
医院  局       ├────────────┤
              │通知社会服务 │
              │通知家属    │  ← Mentcare系统参与
              │更新登记册  │ ← 入院系统参与
              └─────┬──────┘
                    │
                    ↓
              ┌──────────┐
              │  收入医院  │
              └──────────┘
                    │
                    ↓
                    ◎

活动协调规则:

  • 粗横线之前的多条分支 → 所有都完成才能继续(合并同步)
  • 粗横线之后分出多条 → 可以并发执行(并行分叉)

3. 交互模型(Interaction Models)

交互模型用于描述谁和谁交互、怎么交互。分为两种:

用例图(Use Case Diagram)
  └── 粗粒度,展示系统与外部参与者之间有哪些类型的交互
顺序图(Sequence Diagram)
  └── 细粒度,展示一次具体交互中各对象之间消息传递的时序

3.1 用例图(Use Case Diagram)

用例图符号说明:

小人(棍人)= 参与者(Actor),可以是人,也可以是外部系统
椭圆       = 用例(Use Case),代表一类交互
连线       = 参与者参与该用例

Mentcare系统中"医疗前台"相关的用例:

                医疗前台(Medical Receptionist)
                          │
              ┌───────────┼───────────┐
              │           │           │
              ↓           ↓           ↓
         (注册病人)   (查看病人信息) (数据传输)
              │           │           │
         (注销病人)   (联系病人)

"数据传输"用例的表格描述:

┌──────────────────────────────────────────────────────────────┐
│  用例名称:数据传输(Transfer Data)                          │
├─────────┬────────────────────────────────────────────────────┤
│  参与者  │ 医疗前台、病人记录系统(PRS)                      │
├─────────┼────────────────────────────────────────────────────┤
│  描述    │ 前台可将Mentcare中的数据传输到卫生局维护的通用      │
│         │ 病人记录数据库。传输内容可以是更新的个人信息        │
│         │ (地址、电话等)或病人诊断治疗摘要。               │
├─────────┼────────────────────────────────────────────────────┤
│  数据    │ 病人个人信息、治疗摘要                             │
├─────────┼────────────────────────────────────────────────────┤
│  触发条件│ 医疗前台发出命令                                   │
├─────────┼────────────────────────────────────────────────────┤
│  响应    │ 确认PRS已更新                                     │
├─────────┼────────────────────────────────────────────────────┤
│  备注    │ 前台必须有访问病人信息和PRS的安全权限              │
└─────────┴────────────────────────────────────────────────────┘

3.2 顺序图(Sequence Diagram)

顺序图展示在某个用例实例中,各对象之间按时间顺序传递消息的过程。
顺序图符号说明:

对象/参与者列在顶部,各自往下画一条垂直虚线(生命线)
消息 = 带标签的箭头(从左到右表示调用,从右到左表示返回)
生命线上的矩形框 = 该对象正在活跃(参与计算)的时间段
[条件] = 守卫条件(决定走哪个分支)
alt框 = 替代选择框(类似if-else)

示例1:查看病人信息(View Patient Information)

医疗前台      P:PatientInfo      AS:Authorization      D:Mentcare-DB
   │                │                    │                   │
   │  ViewInfo(PID) │                    │                   │
   │───────────────→│                    │                   │
   │                │   report(Info,PID,UID)                 │
   │                │──────────────────────────────────────→│
   │                │         authorize(Info,UID)            │
   │                │                    │←──────────────────│
   │                │                    │  authorization    │
   │                │                    │──────────────────→│
   │   ┌─────────────────────────────────────────────────────│
   │   │ alt                                                  │
   │   │  [authorization OK]                                  │
   │   │                    Patient info                      │
   │   │                │←──────────────────────────────────│
   │←──│─────────────── Patient info(显示在屏幕上)          │
   │   │                                                      │
   │   │  [authorization fail]                                │
   │   │                    Error(no access)                  │
   │   │                │←──────────────────────────────────│
   │←──│─────────────── Error                                │
   │   └─────────────────────────────────────────────────────│

读懂这个图的步骤:

  1. 前台调用 P(PatientInfo对象)的 ViewInfo 方法,传入病人ID
  2. P 向数据库 D 查询数据,传入前台的用户ID(用于权限检查)
  3. D 请求授权系统 AS 验证权限
  4. 若授权通过 → 返回病人信息并显示;若失败 → 返回错误信息
    示例2:数据传输(Transfer Data)——包含对象创建
医疗前台    P:PatientInfo    AS:Authorization    D:Mentcare-DB    PRS
   │              │                 │                  │           │
   │  login()     │                 │                  │           │
   │─────────────────────────────────────────────────────────────→│
   │              │                 │                  │           │
   │  ┌───────────────────────────────────────────────────────────│
   │  │ alt                                                        │
   │  │                                                            │
   │  │  [sendInfo] updateInfo()                                   │
   │  │              │   authorize(TF,UID)  │                      │
   │  │              │───────────────────→  │                      │
   │  │              │    authorization     │                      │
   │  │              │←───────────────────  │                      │
   │  │              │                      │  update(PID)         │
   │  │              │──────────────────────────────────────────→  │
   │  │              │                      │  update OK           │
   │  │              │←──────────────────────────────────────────  │
   │  │                                                            │
   │  │  [sendSummary] UpdateSummary()                             │
   │  │              │   authorize(TF,UID)  │                      │
   │  │              │───────────────────→  │                      │
   │  │              │    authorization     │                      │
   │  │              │←───────────────────  │                      │
   │  │              │      summarize(UID)  │                      │
   │  │              │─────────────────────→│   :summary(新建对象)│
   │  │              │                      │─────────────────────→│
   │  │              │                      │       update OK      │
   │  │              │←─────────────────────│                      │
   │  └───────────────────────────────────────────────────────────│
   │  logout()                                                     │
   │─────────────────────────────────────────────────────────────→│

关键点:

  • alt 框表示两个选项:直接传输个人信息,或创建摘要后传输
  • :summary 是在顺序图执行过程中动态创建的新对象

4. 结构模型(Structural Models)

结构模型展示系统的静态组织结构:系统由哪些组件构成,组件之间有什么关系。

4.1 类图(Class Diagram)

类图是面向对象系统建模的核心工具。
类图的基本构成:

┌─────────────────────────┐
│       类名(ClassName)  │  ← 第一行:类的名字
├─────────────────────────┤
│  属性名 : 类型           │  ← 第二区域:属性(数据)
│  属性名 : 类型           │
├─────────────────────────┤
│  方法名()               │  ← 第三区域:操作/方法(行为)
│  方法名(参数) : 返回类型 │
└─────────────────────────┘

关联关系的多重性(Multiplicity):

1       = 恰好1个
0..1    = 0个或1个(可选)
1..*    = 1个或多个(至少1个)
*       = 0个或多个(任意数量)
1..4    = 1到4个

Mentcare系统的类图:

                        病人记录
                       ┌────────┐
                  1    │Patient │ 1
病人 ─────────────────→│Record  │←─────────────── 咨询
(Patient)             └────────┘             (Consultation)
   │                                               │
   │ 1..*  diagnosed with  1..*                   │ 1..*  involves  1..4
   ↓                                              ↓
病情                                           治疗/处方
(Condition)                              (Treatment/Medication)
病人 ─── attends ──→ 诊所(Hospital)
病人 ─── referred-by ──→ 全科医生(General Practitioner)
病人 ─── referred-to 1..* ──→ 医生(Doctor)
医生 ─── prescribes ──→ 药物(Medication)

Consultation类的详细表示:

┌──────────────────────────────┐
│         Consultation         │
├──────────────────────────────┤
│  Doctors                     │
│  Date                        │
│  Time                        │
│  Clinic                      │
│  Reason                      │
│  Medication prescribed       │
│  Treatment prescribed        │
│  Voice notes                 │
│  Transcript                  │
│  ...                         │
├──────────────────────────────┤
│  New()                       │
│  Prescribe()                 │
│  RecordNotes()               │
│  Transcribe()                │
│  ...                         │
└──────────────────────────────┘

4.2 泛化(Generalization)——继承关系

泛化 = 面向对象中的继承,用于描述"是一种"(is-a)关系。

比喻:松鼠和老鼠都是啮齿动物,所以它们共享啮齿动物的特征(有门牙),又各自有自己的特点。
医生类的泛化层次(Generalization Hierarchy):

Doctor
医生
─────────────
Name
Phone
Email
─────────────
register
de-register

General Practitioner
全科医生
─────────────
Practice
Address

Hospital Doctor
医院医生
─────────────
Staff Number
Pager Number

Trainee Doctor
实习医生

Qualified Doctor
执业医生

Consultant
顾问医生

继承规则:

  • 子类继承父类的所有属性和操作
  • 子类可以添加自己特有的属性和操作
  • 例如:所有Doctor都有Name和Phone;只有Hospital Doctor才有Staff Number

4.3 聚合(Aggregation)——"整体-部分"关系

聚合 = 描述"由…组成"的关系,用菱形标记。

病人记录(Patient Record)
    ├── 1个 病人(Patient)       ← 个人信息
    └── 多个 咨询记录(Consultation)← 每次就诊的记录
UML表示:
┌──────────────┐     1     ┌──────────┐
│ PatientRecord│◇─────────→│ Patient  │
└──────────────┘           └──────────┘
       │ 1
       │
       │ 1..*
┌──────────────┐
│ Consultation │
└──────────────┘
◇ 旁边的菱形 = 聚合关系,表示"PatientRecord包含Patient和Consultation"

4.4 C++代码演示:类图到代码的映射

#include <iostream>
#include <string>
#include <vector>
// ===== 基类:医生(Doctor)=====
// 对应类图中 Doctor 节点
class Doctor {
protected:
    std::string name;      // 姓名(所有医生共有)
    std::string phone;     // 电话(所有医生共有)
    std::string email;     // 邮箱(所有医生共有)
public:
    Doctor(const std::string& n, const std::string& p, const std::string& e)
        : name(n), phone(p), email(e) {}
    // 注册到 Mentcare 系统(所有医生共有的操作)
    virtual void registerDoctor() {
        std::cout << name << " 注册到系统" << std::endl;
    }
    // 从系统注销(所有医生共有的操作)
    virtual void deregisterDoctor() {
        std::cout << name << " 从系统注销" << std::endl;
    }
    virtual void showInfo() const {
        std::cout << "医生:" << name << "  电话:" << phone << std::endl;
    }
    virtual ~Doctor() {}
};
// ===== 子类:全科医生(General Practitioner)=====
// 继承 Doctor,添加自己特有的属性
class GeneralPractitioner : public Doctor {
private:
    std::string practiceName;   // 诊所名称(全科医生特有)
    std::string practiceAddress; // 诊所地址(全科医生特有)
public:
    GeneralPractitioner(const std::string& n, const std::string& p,
                        const std::string& e, const std::string& pn,
                        const std::string& pa)
        : Doctor(n, p, e), practiceName(pn), practiceAddress(pa) {}
    void showInfo() const override {
        Doctor::showInfo(); // 先显示父类信息
        std::cout << "  诊所:" << practiceName
                  << "  地址:" << practiceAddress << std::endl;
    }
};
// ===== 子类:医院医生(Hospital Doctor)=====
// 继承 Doctor,添加员工编号和寻呼机
class HospitalDoctor : public Doctor {
protected:
    std::string staffNumber;  // 员工编号(医院医生特有)
    std::string pagerNumber;  // 寻呼机号(医院医生特有)
public:
    HospitalDoctor(const std::string& n, const std::string& p,
                   const std::string& e, const std::string& sn,
                   const std::string& pgr)
        : Doctor(n, p, e), staffNumber(sn), pagerNumber(pgr) {}
    void showInfo() const override {
        Doctor::showInfo();
        std::cout << "  员工编号:" << staffNumber
                  << "  寻呼机:" << pagerNumber << std::endl;
    }
};
// ===== 子类:顾问医生(Consultant)=====
// 继承 HospitalDoctor,是资深医生,有完整决策权
class Consultant : public HospitalDoctor {
private:
    std::string specialization; // 专科方向(顾问医生特有)
public:
    Consultant(const std::string& n, const std::string& p,
               const std::string& e, const std::string& sn,
               const std::string& pgr, const std::string& spec)
        : HospitalDoctor(n, p, e, sn, pgr), specialization(spec) {}
    void showInfo() const override {
        HospitalDoctor::showInfo();
        std::cout << "  专科:" << specialization << std::endl;
    }
};
// ===== 聚合示例:病人记录(PatientRecord)=====
// PatientRecord 包含(聚合)病人信息和多条咨询记录
struct Consultation {
    std::string date;
    std::string doctorName;
    std::string reason;
    std::string treatment;
};
class PatientRecord {
private:
    std::string patientName;         // 病人姓名
    std::string patientID;           // 病人ID
    std::vector<Consultation> consultations; // 多条咨询记录(1..*)
public:
    PatientRecord(const std::string& name, const std::string& id)
        : patientName(name), patientID(id) {}
    // 添加一次咨询记录
    void addConsultation(const Consultation& c) {
        consultations.push_back(c);
    }
    // 显示所有记录
    void showRecord() const {
        std::cout << "病人:" << patientName
                  << "(ID:" << patientID << ")" << std::endl;
        std::cout << "咨询记录(共" << consultations.size() << "次):" << std::endl;
        for (const auto& c : consultations) {
            std::cout << "  日期:" << c.date
                      << "  医生:" << c.doctorName
                      << "  原因:" << c.reason << std::endl;
        }
    }
};
int main() {
    // 演示泛化(继承)
    GeneralPractitioner gp("李医生", "010-1234", "li@clinic.com",
                           "和平诊所", "北京市朝阳区");
    Consultant consultant("王顾问", "010-5678", "wang@hospital.com",
                          "H001", "P-100", "心理科");
    std::cout << "=== 全科医生信息 ===" << std::endl;
    gp.showInfo();
    std::cout << "\n=== 顾问医生信息 ===" << std::endl;
    consultant.showInfo();
    // 演示聚合
    std::cout << "\n=== 病人记录 ===" << std::endl;
    PatientRecord record("张三", "P-2024-001");
    record.addConsultation({"2024-01-15", "王顾问", "焦虑症", "药物治疗"});
    record.addConsultation({"2024-02-20", "王顾问", "复诊", "继续用药"});
    record.showRecord();
    return 0;
}

5. 行为模型(Behavioral Models)

行为模型描述系统运行时如何响应输入和事件的动态过程。
有两种触发方式:

数据驱动(Data-driven)
  └── 当某些数据到达时,触发处理
  └── 例:账单系统收到通话记录数据后,开始计算费用
事件驱动(Event-driven)
  └── 当某些事件发生时,触发状态转换
  └── 例:微波炉门关上(事件)→ 允许启动(状态变化)

5.1 数据驱动建模(Data-driven Modeling)

活动图可以用来表示数据流图(DFD),展示数据在处理步骤间的流转。
胰岛素泵的数据处理流程:

血糖传感器
    │
    │ 传感器原始数据
    ↓
┌─────────────────┐
│  获取传感器读数   │
│ (Get Sensor Value)│
└────────┬────────┘
         │ 原始传感器数据
         ↓
┌─────────────────┐
│  计算血糖水平     │
│(Compute Sugar Level)│
└────────┬────────┘
         │ 血糖水平(数值)
         ↓
┌─────────────────┐
│  计算胰岛素需求   │
│(Calculate Insulin)│
└────────┬────────┘
         │ 胰岛素需求量
         ↓
┌─────────────────┐
│  计算泵控制命令   │
│(Calculate Pump Commands)│
└────────┬────────┘
         │ 泵控制命令
         ↓
┌─────────────────┐
│  控制泵          │
│(Control Pump)   │
└────────┬────────┘
         │
         ↓
      胰岛素泵

活动图 vs 顺序图 处理数据时的比较:

角度 活动图 顺序图
突出 操作/处理步骤 对象/数据
适合 数据流转过程 对象间的通信
受众 非技术人员更直觉 工程师更偏好

5.2 事件驱动建模(Event-driven Modeling)——状态图

状态图展示系统在不同状态(State)之间,因事件(Event)触发而发生的转换(Transition)
状态图符号说明:

圆角矩形  = 状态(系统当前所处的情形)
带标签箭头 = 状态转换(由某事件触发)
do:xxx    = 在该状态下持续执行的动作
●         = 初始状态
◎         = 终止状态

微波炉控制软件的状态图(简化):

按全功率键

按半功率键

按半功率键

按全功率键

按数字键

按数字键

开门

关门

关门

开门

按开始键

计时结束

开门

按取消键

Waiting

FullPower

HalfPower

SetTime

Disabled

Enabled

Operation

微波炉各状态的详细说明:

状态 描述
Waiting(等待) 炉子等待输入,显示屏显示当前时间
Half Power(半功率) 功率设为300瓦,显示"Half power"
Full Power(全功率) 功率设为600瓦,显示"Full power"
Set Time(设置时间) 根据用户输入设置烹饪时间,动态更新显示
Disabled(禁用) 出于安全原因禁止操作,炉内灯亮,显示"Not ready"
Enabled(就绪) 可以启动,炉内灯灭,显示"Ready to cook"
Operation(运行中) 正在烹饪,倒计时显示;完成后蜂鸣5秒,显示"Cooking complete"

触发状态转换的事件:

事件 说明
Half power 用户按半功率键
Full power 用户按全功率键
Timer 用户按计时键
Number 用户按数字键
Door open 炉门开关未关闭
Door closed 炉门开关已关闭
Start 用户按开始键
Cancel 用户按取消键

5.3 超状态(Superstate):处理状态爆炸

当系统状态很多时,用超状态(Superstate)来隐藏细节——高层看它是一个状态,展开后是一组子状态。
Operation超状态的展开:

Operation(超状态)
├── Checking(检查中): 检查系统状态
│     ├── 若发现故障 → Alarm(报警)
│     └── 若正常 → Cook(烹饪中)
│
├── Cook(烹饪中): 运行微波发生器
│     ├── 超时 → Done(完成)
│     └── 开门 → Disabled(禁用)
│
├── Done(完成): 蜂鸣器响5秒
│     └── 超时 → Waiting(等待)
│
└── Alarm(报警): 显示故障信息
      └── → Disabled(禁用)

5.4 C++演示:状态机实现

#include <iostream>
#include <string>
// ===== 微波炉状态枚举 =====
enum class OvenState {
    WAITING,      // 等待输入
    HALF_POWER,   // 半功率已选择
    FULL_POWER,   // 全功率已选择
    SET_TIME,     // 设置烹饪时间
    DISABLED,     // 禁用(门开着)
    ENABLED,      // 就绪(可启动)
    OPERATION     // 正在烹饪
};
// ===== 微波炉事件枚举 =====
enum class OvenEvent {
    HALF_POWER_BTN,  // 按半功率键
    FULL_POWER_BTN,  // 按全功率键
    NUMBER_KEY,      // 按数字键
    DOOR_OPEN,       // 开门
    DOOR_CLOSE,      // 关门
    START_BTN,       // 按开始键
    CANCEL_BTN,      // 按取消键
    TIMER_DONE       // 计时结束
};
// ===== 简单微波炉状态机 =====
class MicrowaveOven {
private:
    OvenState currentState;  // 当前状态
    int cookingTime;         // 烹饪时间(秒)
    int powerLevel;          // 功率(瓦)
    // 获取状态的名称字符串(用于显示)
    std::string stateName(OvenState s) const {
        switch (s) {
            case OvenState::WAITING:    return "WAITING(等待)";
            case OvenState::HALF_POWER: return "HALF_POWER(半功率)";
            case OvenState::FULL_POWER: return "FULL_POWER(全功率)";
            case OvenState::SET_TIME:   return "SET_TIME(设置时间)";
            case OvenState::DISABLED:   return "DISABLED(禁用)";
            case OvenState::ENABLED:    return "ENABLED(就绪)";
            case OvenState::OPERATION:  return "OPERATION(运行中)";
            default:                    return "UNKNOWN";
        }
    }
public:
    MicrowaveOven() : currentState(OvenState::WAITING),
                      cookingTime(0), powerLevel(0) {}
    // 处理事件,触发状态转换
    // 这是状态机的核心:根据当前状态和输入事件,决定下一个状态
    void handleEvent(OvenEvent event) {
        OvenState nextState = currentState; // 默认保持当前状态
        switch (currentState) {
            case OvenState::WAITING:
                if (event == OvenEvent::HALF_POWER_BTN) {
                    powerLevel = 300;
                    nextState = OvenState::HALF_POWER;
                } else if (event == OvenEvent::FULL_POWER_BTN) {
                    powerLevel = 600;
                    nextState = OvenState::FULL_POWER;
                }
                break;
            case OvenState::HALF_POWER:
                if (event == OvenEvent::FULL_POWER_BTN) {
                    powerLevel = 600;
                    nextState = OvenState::FULL_POWER;
                } else if (event == OvenEvent::NUMBER_KEY) {
                    cookingTime = 60; // 简化:假设按数字键设置60秒
                    nextState = OvenState::SET_TIME;
                }
                break;
            case OvenState::FULL_POWER:
                if (event == OvenEvent::HALF_POWER_BTN) {
                    powerLevel = 300;
                    nextState = OvenState::HALF_POWER;
                } else if (event == OvenEvent::NUMBER_KEY) {
                    cookingTime = 60;
                    nextState = OvenState::SET_TIME;
                }
                break;
            case OvenState::SET_TIME:
                if (event == OvenEvent::DOOR_OPEN) {
                    nextState = OvenState::DISABLED; // 开门→禁用(安全规则)
                } else if (event == OvenEvent::DOOR_CLOSE) {
                    nextState = OvenState::ENABLED;  // 关门→就绪
                }
                break;
            case OvenState::DISABLED:
                if (event == OvenEvent::DOOR_CLOSE) {
                    nextState = OvenState::ENABLED;  // 关门才能重新就绪
                }
                break;
            case OvenState::ENABLED:
                if (event == OvenEvent::DOOR_OPEN) {
                    nextState = OvenState::DISABLED;
                } else if (event == OvenEvent::START_BTN) {
                    nextState = OvenState::OPERATION; // 按开始→开始烹饪
                }
                break;
            case OvenState::OPERATION:
                if (event == OvenEvent::DOOR_OPEN) {
                    nextState = OvenState::DISABLED;  // 烹饪中开门→立刻禁用
                } else if (event == OvenEvent::CANCEL_BTN) {
                    nextState = OvenState::WAITING;   // 取消→回到等待
                } else if (event == OvenEvent::TIMER_DONE) {
                    std::cout << "  >>> 烹饪完成!蜂鸣器响起..." << std::endl;
                    nextState = OvenState::WAITING;   // 完成→回到等待
                }
                break;
        }
        // 若状态发生了变化,打印转换日志
        if (nextState != currentState) {
            std::cout << "状态转换:" << stateName(currentState)
                      << " → " << stateName(nextState) << std::endl;
            currentState = nextState;
        } else {
            std::cout << "(在当前状态 " << stateName(currentState)
                      << " 中,该事件无效)" << std::endl;
        }
    }
    // 获取当前状态(用于测试)
    OvenState getState() const { return currentState; }
};
int main() {
    MicrowaveOven oven;
    std::cout << "=== 微波炉操作模拟 ===" << std::endl;
    std::cout << "\n--- 正常烹饪流程 ---" << std::endl;
    oven.handleEvent(OvenEvent::FULL_POWER_BTN); // 选全功率
    oven.handleEvent(OvenEvent::NUMBER_KEY);      // 设置时间
    oven.handleEvent(OvenEvent::DOOR_CLOSE);      // 关门
    oven.handleEvent(OvenEvent::START_BTN);       // 开始烹饪
    oven.handleEvent(OvenEvent::TIMER_DONE);      // 烹饪完成
    std::cout << "\n--- 安全测试:烹饪中开门 ---" << std::endl;
    oven.handleEvent(OvenEvent::FULL_POWER_BTN);
    oven.handleEvent(OvenEvent::NUMBER_KEY);
    oven.handleEvent(OvenEvent::DOOR_CLOSE);
    oven.handleEvent(OvenEvent::START_BTN);
    oven.handleEvent(OvenEvent::DOOR_OPEN); // 烹饪中开门→应该禁用
    return 0;
}

6. 模型驱动架构(MDA)

6.1 什么是模型驱动架构?

模型驱动架构(Model-Driven Architecture,MDA) 的核心思想:

与其写代码,不如画模型——然后让工具自动把模型变成代码。
这是**模型驱动工程(MDE)**的具体实现方式,由OMG(对象管理组)提出。

6.2 MDA的三层模型

人工辅助转换

自动转换

代码生成器

计算无关模型
CIM
Computation Independent Model
领域概念模型
例:安全CIM、病人记录CIM

平台无关模型
PIM
Platform Independent Model
用UML描述系统行为
不涉及具体平台

平台相关模型
PSM
Platform Specific Model
针对特定平台的实现
例:J2EE版、.NET版

可执行代码
Java程序 / C#程序

三层模型的通俗解释:

CIM(领域模型)
  ─────────────────────────────────────────────
  描述业务概念,与技术无关
  例:"病人有诊断记录"、"医生有处方权"
  类似于:建筑师的概念草图
PIM(平台无关模型)
  ─────────────────────────────────────────────
  用UML精确描述系统行为,但不管用什么语言实现
  例:类图、状态图、顺序图
  类似于:建筑师的标准设计图纸
PSM(平台相关模型)
  ─────────────────────────────────────────────
  针对具体平台(Java/J2EE/.NET)的实现细节
  从PIM自动生成
  类似于:针对不同施工队的施工图

6.3 MDA的转换流程

         领域专家参与              人工介入
              ↓                      ↓
CIM ─────────────────→ PIM ─────────────────→ PSM ──→ 可执行代码
         CIM→PIM                 PIM→PSM
        (仍是研究问题)         (工具已较成熟)

同一个PIM生成多个平台版本:

              ┌─────────────────────────────────────┐
              │         平台无关模型(PIM)           │
              │  (只需维护这一个模型)               │
              └──────────────┬──────────────────────┘
                             │
              ┌──────────────┴──────────────┐
              │                             │
    J2EE转换器                         .NET转换器
              │                             │
              ↓                             ↓
    J2EE专用模型(PSM)            .NET专用模型(PSM)
              │                             │
    Java代码生成器                    C#代码生成器
              │                             │
              ↓                             ↓
         Java程序                       C#程序

6.4 MDA为什么没有成为主流?


原因 说明
CIM→PIM转换还是研究问题 从业务概念到技术模型的自动转换非常困难,需要人工干预
适合讨论的模型≠适合实现的模型 用于讨论的UML图往往过于简化,无法直接生成高质量代码
实现不是最大问题 需求工程、安全性、遗留系统集成等问题更难,MDA帮不上忙
平台独立性价值有限 大多数系统运行在Windows/Linux,平台几十年不换,MDA的优势不明显
敏捷方法兴起 敏捷强调"可运行软件",与MDA的"大量前期建模"理念冲突
工具依赖 需要特殊工具,且工具提供商可能倒闭,企业不愿依赖

MDA适合的场景:

  • 包含硬件和软件的系统产品(汽车、航空等)
  • 软件生命周期很长,需要跨多代平台迁移
  • 应用领域已被充分理解,可以形式化为CIM

7. 总结与关键点

         系统建模 全景总结
         ═══════════════════
  建模的四个视角:
  ┌──────────┬──────────┬──────────┬──────────┐
  │ 上下文    │ 交互     │ 结构     │ 行为     │
  │ 系统边界  │ 谁与谁   │ 系统由   │ 系统如何 │
  │ 外部关系  │ 如何交互 │ 什么组成 │ 响应事件 │
  └──────────┴──────────┴──────────┴──────────┘
  对应的主要图类型:
  ┌──────────┬──────────┬──────────┬──────────┐
  │活动图     │用例图    │类图      │状态图    │
  │(流程)   │(外部交互)│(结构)  │(事件响应)│
  │          │顺序图    │泛化/聚合  │活动图    │
  │          │(时序)  │          │(数据流) │
  └──────────┴──────────┴──────────┴──────────┘
  MDA三层模型:
  CIM(业务概念)→ PIM(平台无关设计)→ PSM(平台相关)→ 代码
  当前状态:PIM→PSM→代码工具较成熟,CIM→PIM仍需人工

一句话总结每种图:


图类型 一句话
上下文图(活动图) 这个系统在整个业务流程中处于什么位置,与哪些外部系统打交道
用例图 谁能对这个系统做什么操作(粗粒度的交互概览)
顺序图 在一次具体操作中,各对象按什么顺序传递消息(细粒度时序)
类图 系统里有哪些类,它们有什么属性和方法,相互之间有什么关系
状态图 系统在不同状态间怎么转换,什么事件触发什么转换
MDA 用模型代替代码,从高层抽象模型自动生成可执行代码

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐