彻底搞懂面向对象分析(OOA)
一、先搞懂:面向对象分析到底是什么?
你不用记复杂定义 ——OOA 的核心就是 “搞清楚系统要做什么”,并把这个 “需求” 变成一个 “看得见、能沟通” 的模型。
比如开发 ATM,OOA 要解决的问题是:
- 这个系统里有哪些 “关键角色 / 东西”(比如用户、储蓄卡、银行系统)?
- 这些角色之间会发生什么互动(比如用户插卡、银行验证密码)?
- 系统最终要实现什么功能(比如取钱、打印凭条)?
官方点说,OOA 的三大核心工作是:
- 理解:吃透用户需求(比如银行说 “密码错 3 次锁卡”);
- 表达:把需求变成 “对象模型、动态模型、功能模型”(后文重点讲);
- 验证:确认模型没遗漏、没矛盾(比如 “锁卡后还能不能查余额?”)。
二、OOA 的基本过程:3 个子模型 + 5 个层次
这是 OOA 的 “骨架”,先记住一句话:先确定 “有什么”(对象模型),再看 “会发生什么”(动态模型),最后明确 “要做什么”(功能模型),5 个层次则是建模的 “步骤顺序”。
1. 3 个子模型:像剥洋葱一样理解系统
子模型 | 核心问题 | 类比(ATM 系统) |
对象模型 | 系统里 “有什么” | 桌椅、员工(对应:用户、储蓄卡、ATM 机) |
动态模型 | 系统 “会发生什么” | 顾客点餐流程(对应:插卡→输密码→取钱) |
功能模型 | 系统 “要做什么” | 厨房做菜步骤(对应:验证卡→扣余额→吐钱) |
2. 5 个层次:建模的 “先后顺序”
按这个顺序做,绝对不会乱:
- 主题层:把系统拆成 “大模块”(比如 ATM 分 “用户交互模块”“银行通信模块”);
- 类与对象层:每个模块里有哪些 “关键角色”(比如 “用户交互模块” 里有 ATM 机、用户);
- 结构层:这些角色之间的关系(比如 “用户持有储蓄卡”“储蓄卡关联银行系统”);
- 属性层:每个角色的 “特征”(比如储蓄卡有 “卡号、余额、有效期”);
- 服务层:每个角色能 “做什么”(比如 ATM 机能 “验证密码、吐钱”)。
三、第一步:写清楚 “需求陈述”—— 别让需求变成 “罗生门”
需求是 OOA 的起点,写差了后面全错。比如银行说 “用户能取钱”,这就是典型的 “烂需求”—— 没说取多少钱、密码错了怎么办、要不要打凭条。
1. 需求陈述 3 个要点
- 无歧义:把 “模糊词” 变 “精确描述”
坏例子:“用户能取钱”
好例子:“储蓄卡用户插入有效卡片,输入正确密码后,可提取 100~20000 元(步长 100 元)”;
- 无遗漏:别漏 “异常场景”
比如必须写 “密码连续错误 3 次,系统锁定卡片并吐出”“余额不足时提示‘请重新输入金额’”;
- 无矛盾:前后说法一致
不能前面写 “密码错 3 次锁卡”,后面写 “错 5 次锁卡”。
2. ATM 系统需求陈述(实战示例)
1. 用户范围:持有本行储蓄卡的个人用户;
2. 核心功能:
a. 插卡验证:系统读取卡号,向银行确认卡片是否有效(未挂失、未过期);
b. 密码验证:用户输入6位密码,银行验证正确性,错3次锁卡;
c. 取款操作:验证通过后,用户输入100~20000元(步长100),系统向银行请求扣款,扣款成功后吐钱;
d. 凭条打印:用户可选择是否打印凭条,凭条包含卡号后4位、金额、时间;
3. 异常处理:
a. 卡片无效(过期/挂失):提示“卡片无法使用”并吐卡;
b. 余额不足:提示“余额不足”并返回金额输入界面;
c. 吐钞失败:提示“交易失败”,向银行发送“撤销扣款”请求。
四、核心任务 1:建立 “对象模型”—— 确定系统里的 “人和物”
对象模型是 OOA 的基石,简单说就是 “画一张图,展示系统里的关键角色、关系和特征”。按以下 6 步来:
步骤 1:确立 “类与对象”—— 从需求里抓 “名词”
- 方法:把需求里的 “名词 / 名词短语” 列出来,排除 “非问题域” 的词;
- ATM 实战:
需求里的名词:用户、储蓄卡、ATM 机、银行系统、密码、金额、凭条、卡号、余额
排除非问题域:密码(是储蓄卡的属性,不是独立类)、金额(是取款事件的参数,不是类)
最终类:User(用户)、DebitCard(储蓄卡)、ATM(ATM 机)、BankSystem(银行系统)、Receipt(凭条)。
步骤 2:确立 “关联”—— 类之间的 “关系”
- 方法:用 “动词” 描述类之间的联系,比如 “持有”“关联”“生成”;
- ATM 实战(用 UML 类图表示,文字版如下):
- User ←“持有”→ DebitCard(一个用户可持有多张卡,一张卡属一个用户);
- DebitCard ←“关联”→ BankSystem(一张卡对应一个银行系统);
- ATM ←“生成”→ Receipt(一台 ATM 可生成多个凭条);
- ATM ←“通信”→ BankSystem(ATM 向银行发送请求)。
步骤 3:划分 “主题”—— 给类 “分组”
- 目的:避免类太多看着乱,按 “业务模块” 分组;
- ATM 实战:
- 主题 1:用户交互组(User、ATM、Receipt);
- 主题 2:银行数据组(DebitCard、BankSystem)。
步骤 4:确定 “属性”—— 类的 “特征”
- 方法:只选 “问题域需要的特征”,排除无关的(比如储蓄卡的 “颜色” 不用管);
- ATM 实战(代码示例:Java 类的属性定义):
// 储蓄卡类:只保留问题域需要的属性
public class DebitCard {
private String cardId; // 卡号(必需,用于识别)
private double balance; // 余额(必需,判断能否取款)
private String expiryDate;// 有效期(必需,验证卡片是否有效)
private boolean isLocked; // 是否锁定(必需,处理密码错误场景)
// 排除无关属性:cardColor(颜色)、material(材质)
}
步骤 5:识别 “继承关系”—— 减少重复代码
- 方法:找 “同类事物的共性与个性”,比如 “用户” 里有 “储蓄卡用户”“信用卡用户”;
- ATM 实战(代码示例:继承):
// 父类:用户(共性)
public class User {
private String name; // 姓名(所有用户都有)
private String idCard; // 身份证号(所有用户都有)
// 共性方法:登录(输入身份信息)
public boolean login(String idInfo) {
// 验证身份的通用逻辑
return true;
}
}
// 子类:储蓄卡用户(个性)
public class DebitCardUser extends User {
private List<DebitCard> debitCards; // 储蓄卡用户特有:持有多张储蓄卡
// 个性方法:绑定储蓄卡(父类没有)
public void bindCard(DebitCard card) {
this.debitCards.add(card);
}
}
步骤 6:反复修改 —— 模型不是 “一次性成品”
- 例子:一开始漏了Receipt(凭条)类,后来看需求里有 “打印凭条”,就补充进去;
- 原则:每次修改后,回头对照需求,确认没偏离。
五、核心任务 2:建立 “动态模型”—— 描述系统 “会发生什么”
动态模型关注 “事件和状态变化”,比如 “插卡” 这个动作会让储蓄卡从 “未插入” 变成 “已插入”。按以下 5 步来:
步骤 1:编写 “脚本”—— 模拟 “正常 / 异常场景”
脚本就是 “一步步描述用户和系统的互动”,先写正常场景,再补异常场景。
- ATM 正常脚本(取款流程):
1. 用户(触发者)插入储蓄卡到ATM(目标)→ 参数:卡号信息;
2. ATM(触发者)向银行系统(目标)发送“验证卡片”请求→ 参数:卡号;
3. 银行系统(触发者)返回“卡片有效”→ 参数:有效信号;
4. ATM(触发者)提示用户输密码→ 参数:无;
5. 用户(触发者)输入密码→ 参数:6位密码;
6. ATM(触发者)向银行系统(目标)发送“验证密码”请求→ 参数:卡号+密码;
7. 银行系统(触发者)返回“密码正确”→ 参数:通过信号;
8. 用户(触发者)输入取款金额(比如2000)→ 参数:2000;
9. ATM(触发者)向银行系统(目标)发送“扣款请求”→ 参数:卡号+2000;
10. 银行系统(触发者)返回“扣款成功”→ 参数:成功信号+更新后余额;
11. ATM(触发者)吐钱→ 参数:2000元现金;
12. 用户(触发者)选择“打印凭条”→ 参数:打印指令;
13. ATM(触发者)生成凭条并吐出→ 参数:凭条;
14. ATM(触发者)吐卡→ 参数:储蓄卡;
15. 用户(触发者)取卡→ 流程结束。
- ATM 异常脚本(密码错 3 次):
1. 步骤1~5同上;
2. 银行系统返回“密码错误”(第1次)→ ATM提示“密码错误,还有2次机会”;
3. 用户再输错2次→ 银行系统返回“密码错3次,锁定卡片”;
4. ATM提示“卡片已锁定,请联系银行”并吐卡;
5. 流程结束。
步骤 2:设想 “用户界面”—— 绑定 “动作和事件”
不用画复杂界面,只要明确 “用户操作” 对应 “哪个事件”:
- ATM 界面元素:插卡口、密码键盘、金额输入键、“打印凭条” 按钮;
- 对应关系:按 “金额输入键”→ 触发 “输入取款金额” 事件;按 “打印凭条” 按钮→ 触发 “请求打印凭条” 事件。
步骤 3:画 “事件跟踪图”—— 按 “时间轴” 展示互动
用 “时间轴 + 角色” 的方式,清晰看事件发生顺序(文字简化版):
时间轴→ 1 2 3 4 5 6
User | 插卡 | | 输密码 | 输金额 | 选打印 | 取卡
ATM | | 发验证 | 提示输密| 发扣款 | 吐钱打条| 吐卡
BankSys | | 返有效 | 返正确 | 返成功 | |
步骤 4:画 “状态图”—— 描述单个类的 “状态变化”
状态图只关注 “一个类” 的状态,比如DebitCard(储蓄卡)的状态变化:
初始状态→ 未插入 →(触发事件:插入)→ 已插入 →(触发事件:密码正确)→ 已验证
已验证 →(触发事件:输入金额并确认)→ 交易中 →(触发事件:扣款成功)→ 交易完成
交易完成 →(触发事件:吐卡)→ 已退出 → 最终状态
// 异常分支:已插入 →(触发事件:密码错3次)→ 已锁定 →(触发事件:吐卡)→ 已退出
步骤 5:审查动态模型 —— 确保 “无遗漏”
- 检查点 1:每个脚本里的事件,是否都在状态图里有对应?
比如 “密码错 3 次” 事件,在DebitCard的状态图里有 “已插入→已锁定” 的转换;
- 检查点 2:是否覆盖所有异常场景?
比如 “吐钞失败” 事件,是否在ATM的状态图里有 “交易中→交易失败→吐卡” 的处理。
六、核心任务 3:建立 “功能模型”—— 描述系统 “要做什么”
功能模型用 “数据流图(DFD)” 表示,关注 “数据怎么流动、怎么处理”,比如 “卡号” 从用户到 ATM,再到银行,最后返回 “有效 / 无效” 信号。
步骤 1:画 “基本系统模型图”—— 确定 “系统边界”
先明确 “哪些是系统内的,哪些是外部的”:
- 外部实体:User(用户)、BankSystem(银行系统);
- 系统边界:ATM 系统(我们要开发的部分);
- 处理框:ATM 核心处理(最顶层的功能);
- 数据流:
外部→系统:用户操作指令(插卡、输密码、输金额)、银行响应(有效 / 无效、成功 / 失败);
系统→外部:ATM 提示(输密码、余额不足)、吐钱、吐卡、凭条。
步骤 2:画 “功能级数据流图”—— 拆分 “顶层功能”
把 “ATM 核心处理” 拆成具体的小功能,比如:
1. 卡验证处理:
输入:卡号(来自插卡);
输出:卡状态(有效/无效,到提示模块)、有效卡号(到密码验证处理);
2. 密码验证处理:
输入:有效卡号(来自卡验证)、用户输入的密码(来自用户);
输出:密码状态(正确/错误,到提示模块)、验证通过信号(到取款处理);
3. 取款处理:
输入:验证通过信号(来自密码验证)、取款金额(来自用户);
输出:扣款请求(到银行系统)、扣款结果(到吐钱模块);
4. 凭条打印处理:
输入:交易信息(卡号后4位、金额、时间,来自取款处理)、打印指令(来自用户);
输出:凭条(到用户)。
步骤 3:描述 “处理框功能”—— 只说 “做什么”,不说 “怎么做”
- 坏例子(写了算法):“取款处理:计算吐钞的 100 元张数(金额 / 100),控制电机转动吐钞”;
- 好例子(只说功能):“取款处理:接收用户输入的取款金额,向银行系统发送扣款请求,接收扣款结果后,触发吐钞模块执行吐钞操作”。
七、最后一步:定义 “服务”—— 给类加 “能做的事”
服务就是类的 “方法”,比如ATM类的verifyPassword()(验证密码)方法。按以下 4 个来源找服务:
1. 常规行为:类 “天生该有的能力”
- 比如DebitCard的getBalance()(获取余额)、isValid()(判断是否有效);
- 代码示例:
public class DebitCard {
// 常规服务:获取余额
public double getBalance() {
return this.balance;
}
// 常规服务:判断卡片是否有效(未过期、未锁定)
public boolean isValid() {
return !this.isLocked && LocalDate.now().isBefore(LocalDate.parse(this.expiryDate));
}
}
2. 从事件导出:动态模型里的 “事件” 对应服务
- 比如 “插卡事件”→ ATM的handleCardInsert()(处理插卡)服务;
- “密码验证事件”→ ATM的verifyPassword()(验证密码)服务;
- 代码示例:
public class ATM {
private BankSystem bankSystem; // 依赖银行系统
// 从“密码验证事件”导出的服务
public boolean verifyPassword(DebitCard card, String password) {
// 调用银行系统的验证接口
return bankSystem.checkPassword(card.getCardId(), password);
}
}
3. 与数据流图对应:处理框对应服务
- 比如数据流图里的 “取款处理”→ ATM的executeWithdrawal()(执行取款)服务;
- 代码示例:
public class ATM {
// 与“取款处理”框对应的服务
public boolean executeWithdrawal(DebitCard card, double amount) {
// 1. 向银行发送扣款请求
boolean deductSuccess = bankSystem.deductBalance(card.getCardId(), amount);
// 2. 扣款成功则吐钱
if (deductSuccess) {
this.dispenseCash(amount); // 调用吐钱服务
return true;
}
return false;
}
}
4. 利用继承减少冗余:子类复用父类服务
- 比如User类的login()(登录)服务,DebitCardUser子类直接继承,不用重写;
- 若子类有特殊需求,再重写(比如CreditCardUser的登录需要额外验证信用卡有效期)。
八、小结:小白学好 OOA 的 3 个关键
- 别被术语吓住:OOA 不是 “玄学”,核心就是 “理解需求→建 3 个模型”,全程用 ATM 这样的生活案例对照;
- 按步骤来:需求→对象模型→动态模型→功能模型→服务,一步错了回头改,别跳步;
- 多练手:看完这篇,试着用 “网购系统”(用户、商品、订单、支付系统)练一遍,马上就熟了。
记住:好的分析模型,是用户、开发、产品都能看懂的 “沟通工具”,不是只有程序员才懂的 “天书”。只要你能把 “系统要做什么” 说清楚、画明白,就是合格的 OOA!
还想看更多干货,关注同名公众昊“奈奈聊成长”!!!