软件工程学习:软件工程导论
1. 为什么需要软件工程?
软件无处不在
现代世界完全依赖软件:国家基础设施、金融系统、医疗、娱乐、手机……可以说,没有软件,现代社会无法运转。
软件的特殊性
软件和桥梁、建筑不同:
实体工程(如桥梁):
受材料物理特性限制
受自然规律约束
复杂度有自然上限
软件工程:
没有物理约束
理论上可以无限复杂
越复杂越难理解、越贵
这是一把双刃剑——自由度大,但失控风险也大。
软件失败的两大根本原因
- 系统越来越复杂:需求不断增长,旧方法跟不上
- 没有使用软件工程方法:很多公司"随手写代码",导致软件昂贵且不可靠
2. 什么是专业软件开发?
业余 vs 专业的本质区别
| 对比项 | 业余编程 | 专业软件开发 |
|---|---|---|
| 使用者 | 只有自己 | 其他用户 |
| 开发者 | 个人 | 团队 |
| 文档 | 不需要 | 必须有 |
| 维护 | 不需要 | 全生命周期维护 |
| 质量要求 | 能跑就行 | 需满足非功能属性 |
软件 = 程序 + 更多东西
很多人误以为软件只是代码,实际上:
软件 = 程序代码
+ 文档(用户手册、设计文档)
+ 配置文件
+ 支持网站
+ 库文件
两类软件产品
关键区别:谁控制需求规格?
- 通用产品:开发商说了算,遇到问题可以改方向
- 定制产品:客户说了算,开发商必须按规格做
3. 好软件的四个核心属性
| 属性 | 含义 | 举例 |
|---|---|---|
| 可接受性(Acceptability) | 用户能理解和使用,与其他系统兼容 | 界面直观,文件格式通用 |
| 可靠性和安全性(Dependability & Security) | 不崩溃、不被攻击、不造成损失 | 银行系统7×24不宕机 |
| 效率(Efficiency) | 不浪费内存和CPU | 响应快、资源占用少 |
| 可维护性(Maintainability) | 能随需求变化而修改 | 代码结构清晰,易于扩展 |
4. 软件工程是什么?
定义拆解
软件工程 = 工程纪律 + 覆盖软件生产全过程
两个关键词:
- 工程纪律:在约束(预算、时间)内找到可行解,不追求完美
- 全过程:从需求到维护,不只是编码
软件过程的四个基本活动
- 规格说明:客户和工程师定义要做什么
- 开发:设计和编程
- 验证:检查是否满足客户需求
- 演化:随需求变化修改软件
软件工程 vs 计算机科学 vs 系统工程
计算机科学(Computer Science)
↓ 提供理论基础
软件工程(Software Engineering)
↓ 是其中的一部分
系统工程(System Engineering)
= 硬件 + 软件 + 流程 + 部署
5. 软件工程面临的四大挑战
6. 八类应用系统及其特点
| 类型 | 例子 | 核心关注点 |
|---|---|---|
| 独立应用 | PC上的Word、手机App | 功能完整性 |
| 交互式事务系统 | 电商网站、网银 | 数据一致性、并发 |
| 嵌入式控制系统 | 汽车ABS、微波炉 | 实时性、安全性 |
| 批处理系统 | 电话账单、工资系统 | 吞吐量、准确性 |
| 娱乐系统 | 游戏 | 用户体验、响应速度 |
| 建模仿真系统 | 气候模型 | 计算性能 |
| 数据采集分析 | 传感器网络、大数据 | 可靠性、实时性 |
| 系统之系统 | 企业ERP | 集成复杂度 |
7. 互联网对软件工程的影响
互联网改变了软件的组织方式:
互联网之前:
单机程序 → 本地运行 → 本地通信
互联网之后:
高度分布式 → 全球部署 → 基于服务
四个主要变化:
- 软件复用成为主流:先找现成的,再自己写
- 增量开发成为标准:边做边改,不可能一次把需求定死
- 服务化架构(SOA):软件组件 = 独立Web服务
- 富界面技术出现:AJAX、HTML5让浏览器变成应用平台
**软件即服务(SaaS)**的兴起:
传统模式:买软件光盘 → 安装到电脑 → 本地运行
现代模式:订阅服务 → 云端运行 → 浏览器访问
例:Office 365、Google Docs、Adobe CC
8. 软件工程伦理
为什么工程师需要关注伦理?
软件工程师拥有巨大影响力——代码可以影响数百万人的生活、安全和隐私。
四个核心职业责任
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:胰岛素泵控制系统(嵌入式系统)
问题背景:
糖尿病患者 → 胰腺无法产生足够胰岛素
传统方式:手动检测血糖 + 手动注射
自动化方案:
血糖传感器 → 控制器计算剂量 → 微型泵注射胰岛素
核心挑战:这是安全关键系统
- 血糖过低(胰岛素太多)→ 短期:大脑失能、昏迷、死亡
- 血糖过高(胰岛素太少)→ 长期:眼、肾、心脏损伤
两个必须满足的高级需求:
- 系统在需要时必须能供应胰岛素(可用性)
- 系统必须可靠地输送正确剂量(准确性)
案例2:精神卫生信息系统(Mentcare)
这是一个信息系统,管理精神病患者的就诊记录。
系统架构:
[Mentcare客户端] [Mentcare客户端] [Mentcare客户端]
| | |
+------------------+------------------+
|
[网络 Network]
|
[Mentcare服务器]
|
[患者数据库]
两个主要目的:
- 生成管理报告(评估绩效)
- 为医疗人员提供患者信息(支持治疗)
特殊挑战:精神病患者的特殊性
患者可能:
- 错过预约
- 弄丢处方
- 忘记指示
- 无固定住所
- 对自己或他人有危险(需要强制住院)
法律冲突:隐私 vs 可用性
隐私最大化 → 只保存一份数据副本
可用性最大化 → 保存多份副本(服务器故障时可用)
两者存在矛盾,需要设计权衡
案例3:野外气象站(传感器数据采集)
背景:政府在偏远地区部署数百个气象站,
用于气候变化监测和天气预报
系统组成:
[气象站系统] ←卫星通信→ [数据管理和存档系统]
↕ ↕
[站点维护系统] ←远程控制→ (可远程更新软件)
特殊挑战(因为无人值守、偏远部署):
1. 电池供电,太阳能/风能充电
2. 恶劣环境(极端温度、动物破坏)
3. 卫星带宽窄 → 必须本地预处理数据
4. 必须支持远程重配置
5. 必须能自我监控故障并上报
这使得软件远比功能看起来复杂。
案例4:学校数字学习环境(iLearn)
这是一个支持系统,为3-18岁学生提供数字化学习环境。
架构(三层服务):
浏览器用户界面
|
[配置服务层]
|
+----+----+----+
| | | |
工具服务 应用服务 实用服务
工具服务(Utility Services):
认证、存储、搜索、日志监控
应用服务(Application Services):
邮件、视频会议、报纸档案、VLE
配置服务(Configuration Services):
管理哪些服务对哪些用户可用
两种服务集成方式:
集成服务(Integrated):
提供API → 其他服务可以直接调用 → 无需重新认证
例:统一认证服务
独立服务(Independent):
只通过浏览器访问 → 服务间数据只能靠复制粘贴
→ 可能需要重复登录
10. 知识结构总览
11. 章节核心要点总结
| 要点 | 说明 |
|---|---|
| 软件工程的范围 | 不只是编码,包括规格、开发、验证、演化全过程 |
| 软件的完整含义 | 程序 + 文档 + 配置 + 库 + 支持网站 |
| 好软件的标准 | 可接受性、可靠安全性、效率、可维护性 |
| 不存在万能方法 | 不同系统类型需要不同的工程技术 |
| 共同基础 | 过程管理、可靠性、需求工程、软件复用 |
| 工程师的责任 | 不只是技术,还有对社会和职业的伦理责任 |
| 职业准则 | ACM/IEEE的八条原则提供行为指导 |
12. 练习题参考思路
1.1 专业软件不只是程序本身
专业软件还包括:用户文档(如何使用)、设计文档(如何构建)、配置文件、测试套件、支持网站。这些对于软件被他人使用和维护至关重要。
1.2 通用 vs 定制软件的关键区别
通用软件的规格由开发商控制,可以根据市场反馈调整。定制软件的规格由客户控制,开发商必须严格遵守。对用户而言:通用软件可能不完全符合需求,但价格低;定制软件更合身,但价格高。
1.3 为什么软件工程长期更便宜
前期投入软件工程方法(规格说明、设计、测试)可以:
- 早期发现错误(修复成本低)
- 减少测试和质量保障成本
- 降低长期维护成本
- 避免返工
不使用软件工程方法的代价:测试成本高、维护困难、需求变更时改动代价巨大。
1.4 伦理问题举例 - 收集用户数据时不充分告知(隐私问题)
- 算法歧视(公平性问题)
- 开发监控软件(自由 vs 安全)
- 知道系统有安全漏洞但不报告(公众安全问题)
1.5 不同应用类型需要不同技术 - 嵌入式系统:需要实时操作系统、硬件接口编程、严格验证
- 游戏:需要图形引擎、物理仿真、快速原型迭代
- 信息系统:需要数据库设计、事务处理、访问控制
- 批处理系统:需要吞吐量优化、错误恢复机制
软件工程第二章:软件过程详细解读
引言:什么是软件过程?
软件过程就是一套有组织的活动集合,目标是生产出软件系统。
打个比方:就像盖房子有"打地基→砌墙→盖屋顶→装修"这样的步骤,软件开发也需要一套有序的步骤。但软件比房子复杂得多,因为需求会变、技术会变、人员会变。
所有软件过程都必须包含这四个核心活动:
第一节:三大过程模型
总览对比
1.1 瀑布模型(Waterfall Model)
什么是瀑布模型?
就像瀑布从上往下流,每个阶段完成后才能进入下一阶段,不能回头。
阶段1:需求分析
↓(瀑布流下)
阶段2:系统和软件设计
↓
阶段3:实现和单元测试
↓
阶段4:集成和系统测试
↓
阶段5:运行和维护
五个阶段详解
瀑布模型的核心思想
理想情况:每个阶段结束时产出一个"签批文档",下一阶段才能开始。
现实情况:各阶段之间需要反馈,就像这样:
需求分析 ←───────────────── 发现需求有问题
↓
系统设计 ←────────── 发现设计有问题
↓
实现阶段 ←── 发现代码无法实现设计
↓
测试阶段 ──→ 发现各种问题 ──→ 反馈给上面各阶段
什么时候用瀑布模型?
| 适用场景 | 原因 |
|---|---|
| 嵌入式系统(如汽车软件) | 硬件已定型,软件功能必须提前明确 |
| 安全关键系统(如医疗、航空) | 需要完整文档做安全分析,后期改动代价极高 |
| 多公司合作的大型系统 | 需要完整规格说明来协调各方独立开发 |
瀑布模型的问题
问题1:需求冻结过早
↓
用户看到"完整系统"才发现不是自己想要的
↓
这时候修改代价极高(可能要推翻重来)
问题2:文档驱动太重
↓
大量时间花在写文档、审批文档
↓
真正写代码的时间被压缩
问题3:不适合需求多变的场景
↓
业务环境变化 → 需求变了 → 所有阶段可能要重做
1.2 增量开发(Incremental Development)
核心思想
不一次性做完整个系统,而是分批次、一点一点地做,每次做一个"增量"(Increment),每个增量都是可运行的软件版本。
需求 ──┐
设计 ──┼──→ 增量1(最核心功能)──→ 发布给用户
实现 ──┘
↓用户反馈
需求 ──┐
设计 ──┼──→ 增量2(更多功能)──→ 发布给用户
实现 ──┘
↓用户反馈
...继续...
↓
需求 ──┐
设计 ──┼──→ 增量N(完整功能)──→ 最终产品
实现 ──┘
图示
增量开发的三大优势
优势1:降低需求变更成本
传统瀑布:改需求 = 重做所有文档 + 重新设计 + 重新编码
增量开发:改需求 = 只影响当前增量,后续增量可以调整
优势2:更容易获得客户反馈
传统瀑布:客户要等整个系统完成才能看到
增量开发:客户每个增量都能看到、试用、提意见
优势3:早期交付有价值的软件
传统瀑布:等全部做完才能用(可能几年后)
增量开发:第一个增量完成就能用最核心的功能
增量开发的两个管理问题
问题1:过程不可见
每次增量变化快,不值得为每个版本做大量文档
→ 管理者难以通过文档评估进度
问题2:系统结构逐渐退化
每次增量都往系统里"塞"新功能
→ 代码越来越乱(技术债务)
→ 解决方案:定期重构(Refactoring)
1.3 集成与配置(Integration and Configuration)
核心思想
“不重复造轮子”——尽量用现成的组件、系统,把它们配置好、集成起来,就是新系统。
三类可复用的组件
| 类型 | 说明 | 举例 |
|---|---|---|
| 独立应用系统 | 通用系统,配置后用于特定环境 | SAP ERP系统 |
| 对象集合/组件包 | 在框架中集成的组件 | Java Spring组件 |
| Web服务 | 通过互联网远程调用的服务 | 支付API、地图API |
复用导向的开发过程
优缺点
| 方面 | 优点 | 缺点 |
|---|---|---|
| 开发量 | 大幅减少自研代码量 | 对组件的控制权有限 |
| 成本 | 降低开发成本和风险 | 需求可能被迫妥协 |
| 速度 | 更快交付软件 | 系统演化受第三方影响 |
| 需求匹配 | — | 无法完全满足真实需求 |
第二节:过程活动详解
2.1 软件规格说明(需求工程)
什么是需求工程?
弄清楚"软件要做什么",并把它严格地写下来。
用户的想法(模糊)
↓ 需求工程
精确的需求文档(明确)
需求工程的三个活动
两种需求层次
用户需求(User Requirements):
面向:客户和最终用户
语言:自然语言,避免专业术语
例:系统应该允许护士查询病人的过敏记录
系统需求(System Requirements):
面向:系统开发者
语言:更技术化、更精确
例:系统应在3秒内返回所有关联该病人ID的过敏记录列表,
按严重程度降序排列
2.2 软件设计与实现
设计过程的输入输出
输入:
- 软件需求(要做什么)
- 平台信息(在什么环境上运行)
- 数据描述(处理什么样的数据)
四个设计活动:
┌─────────────────────────────────────────┐
│ 架构设计 数据库设计 接口设计 组件设计 │
└─────────────────────────────────────────┘
输出:
- 系统架构(整体结构图)
- 数据库设计(数据如何存储)
- 接口规格(组件间如何通信)
- 组件描述(每个模块的功能)
四个设计活动详解
| 设计活动 | 目标 | 举例 |
|---|---|---|
| 架构设计 | 确定系统整体结构,主要组件及关系 | 分为前端/后端/数据库三层 |
| 数据库设计 | 设计数据结构和存储方式 | 病人表、就诊记录表的字段和关系 |
| 接口设计 | 定义组件间的接口(必须无歧义) | 前端调用后端API的参数和返回格式 |
| 组件设计 | 寻找可复用组件,设计新组件 | 选用成熟的登录认证库 |
测试 vs 调试的区别
很多人混淆这两个概念:
测试(Testing):
目的:发现"有没有缺陷"
结果:是/否,以及缺陷的表现
调试(Debugging):
目的:找到缺陷在"哪里",并修复它
过程:分析程序行为 → 假设原因 → 验证假设 → 修复
关系:
测试发现问题 → 调试定位和修复问题 → 测试验证修复是否成功
2.3 软件验证(V&V)
三阶段测试过程
V模型(计划驱动的测试)
V模型把测试和开发阶段对应起来:
需求规格说明 ──────────────────→ 客户测试
↓ ↑
系统规格说明 ────────────────→ 系统集成测试
↓ ↑
系统设计 ──────────────→ 子系统集成测试
↓ ↑
组件设计 ──────────→ 组件代码和测试
即:左边做什么设计,右边就对应做什么测试,用于验证左边的设计是否正确实现。
2.4 软件演化
演化的本质
软件不是"做完了就结束",而是需要持续改变:
传统观念(错误的):
软件开发 ────── 维护
(有趣的) (无聊的)
现代观念(正确的):
软件是一个持续演化的过程
需求变化 → 修改系统 → 需求再变化 → 再修改 → ...
第三节:应对变化
变化为什么不可避免?
外部原因:
- 市场竞争 → 需要新功能
- 法规变化 → 必须合规
- 技术进步 → 可以用新技术
内部原因:
- 开始时需求不清楚
- 用户实际使用后才知道要什么
- 团队对需求的理解偏差
应对变化的两种策略
| 策略 | 含义 | 方法 |
|---|---|---|
| 变更预期(Change Anticipation) | 提前预测可能发生的变化 | 原型开发 |
| 变更容忍(Change Tolerance) | 设计系统使得变化容易实现 | 增量交付 |
3.1 原型开发(Prototyping)
什么是原型?
原型是软件系统的早期简化版本,目的不是最终使用,而是:
- 验证需求是否正确
- 探索设计选项
- 向用户展示系统外观
原型开发过程
原型的重要决策:放什么,不放什么
通常可以省略:
- 非功能需求(响应时间、内存)
- 错误处理和恢复
- 高可靠性和稳定性要求
- 完整的文档
重点包含:
- 用户界面(UI最重要)
- 核心业务功能
- 用户最关心的功能
原型的注意事项
重要警告:原型不应该直接变成生产系统!
原型的问题:
- 为了速度,省略了错误处理
- 没有完整测试
- 代码结构混乱
- 非功能需求没达标
如果强行把原型当产品用:
→ 系统不稳定
→ 难以维护
→ 安全漏洞
→ 技术债务巨大
3.2 增量交付(Incremental Delivery)
核心流程
增量交付的四个优势
优势1:早期增量可作为原型
用户使用真实系统,而不是"玩具"原型
反馈更真实、更有价值
优势2:客户可以立即获得价值
最重要的功能先做
用户不用等全部完成
优势3:容易整合变化
后续增量可以根据反馈调整
优势4:最重要的功能测试最充分
先做的增量经历了更多轮测试
增量交付的三个问题
问题1:替换旧系统很困难
用户需要旧系统的全部功能
新旧系统数据库可能不兼容
用户不愿用"半成品"新系统
问题2:公共基础设施难以确定
哪些功能是所有增量都需要的?
如果没规划好,后来可能要大改
问题3:与合同机制冲突
政府、大企业通常要求"完整规格说明"
增量交付没有完整规格说明直到最后一个增量
这与传统合同模式不兼容
第四节:过程改进
什么是过程改进?
过程改进就是:理解现有软件开发过程中的问题,然后改进它,让软件质量更好、成本更低、速度更快。
两种改进方法
| 方法 | 核心理念 | 特点 |
|---|---|---|
| 过程成熟度方法 | 引入规范化过程和最佳实践 | 系统化、文档化、适合大公司 |
| 敏捷方法 | 减少开销,快速响应变化 | 轻量级、迭代快、适合小团队 |
改进循环
能力成熟度模型(CMM/CMMI)
由美国软件工程研究所(SEI)提出,把软件组织的过程成熟度分为5个级别:
第5级:优化(Optimizing)
用过程和产品数据驱动持续改进
↑
第4级:量化管理(Quantitatively Managed)
用统计和量化方法控制子过程
↑
第3级:已定义(Defined)
组织有标准化过程,项目从中裁剪
↑
第2级:已管理(Managed)
有文档化计划,有资源管理和监控
↑
第1级:初始(Initial)
基本目标满足,过程范围明确
各级别的形象比喻
第1级(初始):
"凭感觉"开发,全靠个人英雄主义
一个厉害的程序员走了,项目就完了
第2级(已管理):
有基本的项目计划和跟踪
知道"我们在做什么,什么时候完成"
第3级(已定义):
有公司标准的开发流程
每个项目按标准流程做,可以适当裁剪
第4级(量化管理):
用数字说话:缺陷率、测试覆盖率、开发速度
知道"我们的过程有多好"
第5级(优化):
持续分析数据,主动改进过程
始终在变好
综合对比:三大模型选择指南
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 汽车防抱死软件 | 瀑布 | 嵌入式、安全关键、硬件已定 |
| 电商网站 | 增量 | 需求会变、要快速上线 |
| 企业ERP系统 | 集成配置 | 有成熟产品可定制 |
| 大学选课系统(替换旧系统) | 增量+原型 | 用户习惯旧系统,需要渐进替换 |
| 游戏App | 增量+敏捷 | 需要用户反馈迭代 |
核心概念速查表
| 概念 | 简单解释 |
|---|---|
| 软件过程 | 生产软件的一套有序活动 |
| 瀑布模型 | 阶段顺序执行,每阶段产出文档 |
| 增量开发 | 分批次开发和交付,每次增加功能 |
| 集成与配置 | 复用现有组件,配置集成成新系统 |
| 需求工程 | 弄清楚"要做什么"的过程 |
| 原型 | 早期简化版本,用于验证和探索 |
| 增量交付 | 把做好的增量实际部署给用户使用 |
| 重构 | 改进代码结构而不改变功能 |
| V模型 | 把测试阶段与开发阶段一一对应 |
| Beta测试 | 让真实用户试用产品版软件 |
| 过程成熟度 | 组织软件过程的规范化程度 |
| CMMI | 软件能力成熟度集成模型,五个级别 |
练习题参考思路
2.1 为不同系统选择合适的过程模型
- 汽车防抱死系统(ABS):瀑布模型。嵌入式安全关键系统,需求必须提前明确,硬件接口固定,修改代价极高。
- 虚拟现实软件维护工具:增量开发。需求难以提前明确,需要用户不断试用和反馈,技术新颖。
- 大学会计系统(替换现有系统):增量开发+原型。用户习惯旧系统,需要渐进替换,同时了解新需求。
- 低碳旅行规划系统:增量开发。交互式系统,用户需求会随使用而变化,需要快速迭代。
2.7 原型直接变生产系统的利弊 - 优点:节省重新开发时间,马上可用,给用户留下好印象
- 缺点:原型没有错误处理、性能优化不足、代码结构差难以维护、安全性低、技术债务巨大。政府用的系统尤其不应该这样做,因为数据安全和可靠性要求高。
2.8 为什么原型不应成为生产系统(给经理的报告要点)
- 原型故意省略了非功能需求(响应时间、安全、可靠性)
- 代码结构混乱,难以扩展和维护
- 没有完整的错误处理,遇到异常会崩溃
- 缺少文档,其他工程师无法理解和维护
- 长期维护成本将远超重新开发的成本
第3章:敏捷软件开发 — 从零开始的完整理解
目录
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 敏捷方法最适合的场景
- 软件产品开发:比如开发一款手机App,需求会随用户反馈不断调整
- 内部定制系统开发:客户能全程配合,且监管要求不多的情况
3. 敏捷开发技术详解
敏捷中最有影响力的具体方法是 极限编程(Extreme Programming,XP),它引入了很多实用技术。
3.1 用户故事(User Stories)
是什么?
不写冗长的"需求规格书",而是用一段通俗语言写出"某类用户在某种情况下想做某件事"的小故事。
例子(处方开药故事卡):
┌─────────────────────────────────────────────────────┐
│ 故事名称:开处方 │
│ │
│ Kate是一名医生,想给门诊病人开药。 │
│ 她点击"药物"字段,可以选择: │
│ - 当前用药(查看并修改剂量) │
│ - 新开药物(输入药名前几个字母,系统自动匹配) │
│ - 处方集(搜索批准的药物列表) │
│ │
│ 系统始终检查剂量是否在安全范围内。 │
│ 确认后,处方记录到审计数据库。 │
└─────────────────────────────────────────────────────┘
从故事到任务的分解:
优点与缺点:
| 方面 | 优点 | 缺点 |
|---|---|---|
| 可读性 | 普通人容易理解,不需要懂技术 | 难以判断故事是否覆盖了所有需求 |
| 灵活性 | 需求变化时,只需增减/修改卡片 | 有经验的用户可能省略"理所当然"的细节 |
| 沟通 | 促进开发者和客户之间的对话 | 单张故事可能无法反映真实工作流程的全貌 |
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方式:
编写测试 → 运行测试(必然失败) → 编写代码 → 再次运行测试(通过)→ 重构
为什么这样做有效?
- 写测试时,你必须先想清楚"这个功能应该做什么",也就是先定义好接口和行为规范
- 写完代码就能立刻验证,不会"欠债"越来越多
- 避免"测试滞后(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;
}
测试驱动开发的问题:
- 程序员更喜欢写代码,不喜欢写测试,容易偷懒写不完整的测试
- 复杂UI界面的测试很难用单元测试覆盖
- 测试数量多不等于覆盖率高,可能存在死角
3.4 结对编程(Pair Programming)
做法:两个程序员坐在同一台电脑前,一起编写代码。一人"开车"(写代码),一人"导航"(审查、思考)。
程序员A(开车) 程序员B(导航)
┌──────────┐ ┌──────────┐
│ │ │ │
│ 键盘 │←─────────────→│ 思考 │
│ 鼠标 │ 实时交流 │ 审查 │
│ │ │ │
└──────────┘ └──────────┘
↓
同一台电脑
↓
同一段代码
三大优点:
- 集体所有权:每个人都了解系统的各个部分,不会出现"只有张三懂这块代码"的情况
- 隐式代码审查:每行代码至少被两人看过,Bug更难逃过
- 促进重构:因为改进代码对自己和搭档都有直接好处,所以更愿意花时间重构
关于效率的研究结论:
- 学生实验:结对编程的生产率与两人分开工作相当(错误减少,节省了后期调试时间)
- 资深程序员实验:有一定生产率损失,质量有所提升,但未能完全补偿损耗
- 最重要的价值:知识在团队间传播,降低了"某人离职导致项目瘫痪"的风险
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 完整流程
4.4 Sprint 的详细运作
Sprint规划会(第1天):
产品负责人: 开发团队:
介绍优先级最高 评估每项任务的工作量
的需求条目 → 参考上次Sprint的速度
决定本次能做多少
↓
生成Sprint待办列表
每日站会(站着开,限时15分钟):
每个人回答三个问题:
- 昨天做了什么?
- 今天要做什么?
- 有什么障碍?
Sprint结束(评审+回顾):
评审会议(面向外部):
演示可运行的软件
收集客户/利益相关方的反馈
更新产品待办列表
回顾会议(面向内部):
我们的工作方式哪里好?
哪里需要改进?
下次Sprint怎么做得更好?
4.5 Scrum 的成功之处
为什么用Scrum的团队反馈很好?
- 工作被拆成小块,利益相关方可以清楚看到进展
- 不稳定的需求不会阻塞整体进度
- 团队对所有事情都有可见性,士气和沟通更好
- 客户每隔几周就能看到实际运行的软件,没有"最后一刻的大惊喜"
- 客户和开发者之间建立信任,大家都相信项目会成功
4.6 分布式 Scrum
Scrum最初是为同一地点的小团队设计的,但现实中团队可能分布在全球。
┌─────────────────────────────────────────┐
│ 分布式 Scrum │
└─────────────────────────────────────────┘
产品负责人(美国) 开发团队(中国/印度)
┌──────────────┐ 视频会议/即时通讯 ┌──────────────────┐
│ │ ←───────────────────→ │ │
│ 定义需求 │ │ ScrumMaster在场 │
│ 定期访问团队 │ │ 统一开发环境 │
└──────────────┘ │ 持续集成 │
└──────────────────┘
关键要求:
✓ ScrumMaster 与开发团队在一起(了解日常问题)
✓ 产品负责人定期访问开发团队(建立信任)
✓ 统一的开发环境(所有人看到同样的代码状态)
✓ 持续集成(任何时候都能了解产品状态)
✓ 实时通讯工具(即时消息、视频通话)
5. 扩展敏捷方法
5.1 扩展面临的两个挑战
扩展挑战
├── 纵向扩展(Scale Up)
│ └── 一个小团队 → 多个大团队协同开发同一个大系统
│
└── 横向扩展(Scale Out)
└── 几个项目用敏捷 → 整个大公司都用敏捷
5.2 大型系统的6大复杂性特征
通俗解释每一个:
- 系统之系统:不是一个独立软件,而是多个系统拼在一起,A组开发的东西要和B组的对接
- 棕地开发(Brownfield):不是在空地上建新楼,而是要在已有建筑旁边扩建,还得保证原来的不受影响
- 系统配置:很多工作是"把现成零件拼起来"而不是"从零造零件"
- 监管约束:航空、医疗、金融软件必须通过监管审查,必须有详细文档
- 漫长周期:项目做3年,参与的人换了好几拨,知识传承是难题
- 多元利益:医院系统里,护士、医生、管理员、院长对同一个功能有完全不同的要求
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个关键机制:
- 角色复制:每个团队都有自己的产品负责人和ScrumMaster
- 产品架构师:每个团队选出架构师,共同协作设计整体架构
- 发布对齐:各团队的发布时间对齐,确保整体可演示
- Scrum of Scrums:每日由各团队代表参加的协调站会
5.8 在大公司推广敏捷的障碍
| 障碍 | 说明 |
|---|---|
| 管理者的保守心理 | 项目经理担心"用了新方法,万一失败怎么办?" |
| 现有流程标准冲突 | 公司规定"所有需求变更必须审批",这与重构矛盾 |
| 技能水平参差不齐 | 敏捷对人的要求高,低技能员工可能拖后腿 |
| 文化阻力 | 有几十年传统工程文化的公司,很难接受"代码比文档重要" |
推广策略:
不要强制推行 → 找到热情的先行者 → 做出成功案例 → 让他们去影响其他人 → 在组织内逐步推广
6. 总结与关键点
敏捷软件开发 全景图
═══════════════════
为什么需要?
┌─────────────────────────────────────────────────┐
│ 需求快速变化 + 传统瀑布模型太慢 → 需要新方法 │
└─────────────────────────────────────────────────┘
核心价值观(敏捷宣言):
┌─────────────────────────────────────────────────┐
│ 个体交互 > 流程工具 │
│ 可运行软件 > 完整文档 │
│ 客户合作 > 合同谈判 │
│ 响应变化 > 遵循计划 │
└─────────────────────────────────────────────────┘
XP核心技术:
┌───────────┬───────────┬───────────┬───────────┐
│ 用户故事 │ 重构 │ TDD │ 结对编程 │
│(需求表达)│(保持清洁)│(先写测试)│(互相审查)│
└───────────┴───────────┴───────────┴───────────┘
Scrum框架:
┌─────────────────────────────────────────────────┐
│ 产品待办 → Sprint规划 → Sprint执行 │
│ ← Sprint回顾 ← Sprint评审 │
└─────────────────────────────────────────────────┘
扩展问题与解决:
┌─────────────────────────────────────────────────┐
│ 小团队适合纯敏捷 │
│ 大系统需要敏捷 + 计划驱动的混合 │
│ 分布式团队需要额外的沟通机制 │
└─────────────────────────────────────────────────┘
一句话总结每个关键概念:
| 概念 | 一句话 |
|---|---|
| 敏捷方法 | 少写文档,多交付可运行的软件,快速响应变化 |
| XP(极限编程) | 把好的实践推向极致:更短的迭代、更多的沟通、更严格的测试 |
| 用户故事 | 用"某人想要做某事"的小故事代替长篇需求文档 |
| 重构 | 持续整理代码,不让技术债堆积 |
| 测试驱动开发 | 先写测试,再写代码,保证质量 |
| 结对编程 | 两人共用一台电脑,互相检查,共享知识 |
| Scrum | 用Sprint循环、产品待办列表和每日站会管理敏捷项目 |
| 扩展敏捷 | 大型项目需要混合敏捷和计划驱动的方法 |
第4章:需求工程 — 从零开始的完整理解
目录
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 非功能需求的分类树
三类非功能需求的具体例子(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 需求获取的四步循环
4.3 技术一:访谈(Interviewing)
两种访谈形式:
封闭式访谈(Closed) 开放式访谈(Open)
───────────────────── ─────────────────────
预先设计好问题清单 没有固定议程
按顺序逐一提问 自由探讨各种话题
适合收集特定信息 适合理解整体工作背景
实际中通常混合使用。
访谈的技巧:
- 保持开放心态:不要先入为主地认为"需求应该是这样的"
- 使用"跳板问题":不要直接说"告诉我你想要什么"(太空泛),而是:
- 展示一个原型系统,请用户评价
- 描述一个使用场景,请用户确认是否符合实际
访谈的局限性:
难以通过访谈获得的信息:
× 领域专业知识(医生觉得"理所当然"的事不会主动说)
× 组织内部的真实权力结构(面子问题,不愿透露)
× 实际工作流程(与官方规定有差距,不愿承认)
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条规范
- 统一格式:所有需求用相同的结构写,减少遗漏
- 区分"必须"和"应该":
shall(必须)= 强制性需求should(应该)= 可选的期望需求
- 高亮关键词:用粗体、斜体或颜色标注重要部分
- 避免技术术语:普通读者看不懂"模块"、"架构"等词
- 附上原因:每条需求说明为什么需要它,是谁提出的
示例(胰岛素泵软件需求):
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) (r2−r1)<(r1−r0)) | 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) (r2−r1)≥(r1−r0)) | CompDose = round ( r 2 − r 1 4 ) \text{CompDose} = \text{round}\left(\frac{r_2-r_1}{4}\right) CompDose=round(4r2−r1),若结果为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章:系统建模 — 从零开始的完整理解
目录
- 什么是系统建模?
- 上下文模型(Context Models)
- 交互模型(Interaction Models)
- 结构模型(Structural Models)
- 行为模型(Behavioral Models)
- 模型驱动架构(MDA)
- 总结与关键点
1. 什么是系统建模?
1.1 核心概念
系统建模就是用图形符号画出软件系统的"示意图",帮助人们理解、讨论、设计和记录软件。
比喻:建筑师在盖楼之前会画平面图、立面图、结构图。每张图展示建筑的不同侧面。软件建模也是一样——用不同的图展示软件的不同方面。
模型 vs 完整描述:
完整描述(Representation):保留所有信息
→ 把书从英文翻译成中文,信息量不变
模型(Model/Abstraction):有意省略细节,突出重点
→ 把书的要点做成PPT幻灯片,只保留最关键的内容
模型是一种抽象,不是完整的系统描述。它故意简化,目的是让人更容易理解。
1.2 建模的三种用途
用途1:促进讨论
─────────────────────────────────────────────
不要求完整,也不要求严格符合符号规范
只要能引发有价值的讨论就好
(敏捷建模中常用这种方式)
用途2:记录现有系统
─────────────────────────────────────────────
不需要完整(只需覆盖关键部分)
但必须正确(符号使用准确,描述无误)
用途3:生成系统实现
─────────────────────────────────────────────
必须完整且正确
模型将被工具自动转换为代码
(模型驱动开发,MDD)
1.3 四种建模视角
一个系统可以从四个角度来描述:
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 │
│ └─────────────────────────────────────────────────────│
读懂这个图的步骤:
- 前台调用
P(PatientInfo对象)的ViewInfo方法,传入病人ID P向数据库D查询数据,传入前台的用户ID(用于权限检查)D请求授权系统AS验证权限- 若授权通过 → 返回病人信息并显示;若失败 → 返回错误信息
示例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;只有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(等待) | 炉子等待输入,显示屏显示当前时间 |
| 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(领域模型)
─────────────────────────────────────────────
描述业务概念,与技术无关
例:"病人有诊断记录"、"医生有处方权"
类似于:建筑师的概念草图
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 | 用模型代替代码,从高层抽象模型自动生成可执行代码 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)