设计模式课程第5讲(比较大的实例)—— CAD/CAM + 电子商务综合案例
主讲:王亚沙(北京大学软件研究所) 课程:0C107 设计模式 来源:田浩然上传的资料
内容概览
本讲通过两个完整的大型案例,演示如何综合运用多种设计模式解决真实系统设计问题,并介绍了变化分析矩阵(Variation Analysis Matrix) 这一系统化的需求分析方法。
三大主题:
- CAD/CAM 工件信息抽取模块—— Bridge + Facade + Adapter 三模式组合
- 变化分析矩阵—— 一种系统化的共性/变化性分析方法
- 电子商务订单处理系统—— Strategy + Abstract Factory 双模式综合
一、CAD/CAM 系统案例
问题背景
系统由三部分组成:
- CAD/CAM 系统(已有系统,存在 V1、V2 等多个版本,未来可能还有 V3、V4)
- 专家系统(已有系统,稳定不变,修改难度巨大)
- 工件信息抽取模块(需要开发的中间模块)
核心难点:
- 多套 CAD/CAM 系统并存,数据格式各异
- 专家系统接口要求稳定,不能随 CAD 系统变化而变化
- 需要在多版本 CAD 系统与稳定的专家系统之间建立桥梁
CAD/CAM 存储的模型
5 种几何特征(feature):
| 特征 | 加工工具 |
|---|---|
| hole(孔) | 钻头 |
| cutout(冲切) | 冲床 |
| slot(槽) | 铣刀 |
| special(特殊形状) | 冲床 |
| irregular(不规则形) | 多种工具组合 |
关键术语
| 术语 | 英文 | 含义 |
|---|---|---|
| 几何特征 | Geometry | 钣金件的形状描述:各特征的位置、尺寸、外形 |
| 零件 | Part | 钣金件本身,需要存储其几何信息 |
| 数据集/模型 | Dataset / model | CAD/CAM 数据库中存储零件几何信息的记录集 |
| 数控机床/数控集 | NC machine / NC set | 数控机床,由计算机程序控制切割头,程序由命令组成 |
直观 OO 设计的问题
最初的设计思路:
- 按形状分类(hole / cutout / slot / special / irregular)
- 每种形状再按 V1 / V2 系统分类
- V1 中 feature 功能委派给一个 V1System 方法聚合体对象
- V2 中 feature 功能委派给一组 OOGxx 对象
问题:类爆炸(5 种形状 × N 个系统版本),且每新增一个 CAD 系统版本就要新增一整套类。
用模式方法改进设计
采用 Alexander《建筑永恒之道》提出的一次一模式方法:
- 找出所有可用的模式
- 按”为其他模式提供背景”的顺序排列
- 逐个应用模式,每一步扩展设计
- 重复直到全部模式应用完毕
- 添加细节
最终设计:三模式组合
最终设计运用了三个核心设计模式,按层次从底到顶:
专家系统(稳定接口)
│
▼
┌───────────┐
│ Facade │ ← 为专家系统提供统一、简单、稳定的高层接口
└─────┬─────┘
│
┌─────▼─────┐
│ Adapter │ ← 将不同 CAD 系统的接口适配成统一接口
└─────┬─────┘
│
┌─────▼─────┐
│ Bridge │ ← 将"特征种类"与"CAD系统版本"两个维度解耦
└───────────┘
│
CAD/CAM 系统(多版本)
1. Bridge 模式—— 核心解耦
将两个独立变化的维度(特征种类 × CAD 系统版本)分离:
- 抽象层(Abstraction):Feature 层次结构(HoleFeature、CutoutFeature、SlotFeature…)
- 实现层(Implementor):CADSystem 层次结构(V1System、V2System、V3System…)
两个维度可以独立扩展:
- 新增特征 → 只需新增 Feature 子类
- 新增 CAD 版本 → 只需新增 CADSystem 子类
- 避免了类爆炸(5 × N → 5 + N)
2. Adapter 模式—— 接口适配
为每种 CAD 系统创建适配器,将各异的原生接口转换为 Bridge 模式中 Implementor 定义的统一接口:
- V1Adapter:封装 V1 系统的方法聚合体接口
- V2Adapter:封装 V2 系统的 OOG 对象组接口
- V3Adapter:未来新增 V3 时只需加一个适配器
3. Facade 模式—— 统一入口
为专家系统提供一个简单、稳定、高层的统一接口,屏蔽底层 Bridge + Adapter 的复杂性:
- 专家系统只与 Facade 交互
- 底层新增 CAD 系统或修改实现,不影响专家系统
- 符合”专家系统稳定不变”的约束
设计收益
| 维度 | 直观设计 | 模式组合设计 |
|---|---|---|
| 类数量 | 5 × N(类爆炸) | 5 + N(线性增长) |
| 新增 CAD 版本 | 新增 5 个类 | 新增 1 个 Adapter 类 |
| 专家系统影响 | 可能受接口变化影响 | Facade 层隔离,完全不受影响 |
| 可测试性 | 各版本耦合难测试 | 可独立测试各层 |
| 可维护性 | 改动影响面大 | 单一职责,改动局部化 |
二、变化分析矩阵
为什么需要变化分析矩阵
“当我们精心设计了一组概念后,常常发现仍然存在特殊情况不符合上述概念。”
- 现实世界的变化点极多,人脑无法全部记住和分辨
- OO 设计需要做共性与变化性分析:
- 共性:变化发生的地点(抽象层)
- 变化:共性概念的具体实例(实现层)
- 客户通常谈得很具体,不习惯在概念层次表达
- 客户说”总是”通常表示”通常”,说”从不”通常表示”很少”
矩阵结构
变化1(实现A) 变化2(实现B) 变化3(实现C)
共性概念1 具体实现 具体实现 具体实现
共性概念2 具体实现 具体实现 具体实现
共性概念3 具体实现 具体实现 具体实现
- 行:一个变化点(共性概念),以及实现这个变化点的多种具体情况
- 列:一个具体的场景/实现方案
使用步骤
- 从一个具体场景开始:找到该场景中最重要的特性,填入矩阵
- 扩展新场景:继续处理其他情况,每种情况独立分析,扩展矩阵
- 补充新概念:用新的变化点(行)扩展矩阵
- 横向归纳(按行):从每行中识别共性,抽象出策略/接口
- 纵向归纳(按列):从每列中识别一致性约束,抽象出工厂/族
- 匹配设计模式:
- 行 → Strategy 模式(封装算法族)
- 列 → Abstract Factory 模式(保证产品族一致性)
- 得出高层设计
与客户沟通的经验法则
对于非常具体的问题,客户详细的回答一般是可信的;但他们一般性的回答却不可信。
- 客户非常了解问题域
- 客户不习惯在概念层次表达,谈得很具体
- “总是” = 通常,“从不” = 很少
- 追问具体场景,不要轻信概括性陈述
三、电子商务系统案例
问题需求
美国电子商务公司的订单处理系统,需支持多国家/地区的销售订单:
| 需求项 | 美国 | 加拿大 |
|---|---|---|
| 运费计算 | UPS 费率 | 联邦快递海运费率 |
| 地址验证 | 美国邮政规则 | 加拿大邮政规则 |
| 税费计算 | 州税 + 地税 | GST 国税 + PST 地税 |
| 支付货币 | 美元 | 加元 |
| 发货方式 | 美国邮政 | 联邦快递 |
后续可能扩展到德国、英国等更多国家。
应用变化分析矩阵:八步法
第一步:填入第一个场景(美国)
| 共性概念 | 美国客户 |
|---|---|
| 计算运费 | 使用 UPS 费率 |
| 验证地址 | 使用美国邮政的规则 |
| 计算税费 | 美国州税和地税 |
| 支付货币 | 美元 |
第二步:填入第二个场景(加拿大)
| 共性概念 | 美国客户 | 加拿大客户 |
|---|---|---|
| 计算运费 | 使用 UPS 费率 | 使用联邦快运的海运费率 |
| 验证地址 | 使用美国邮政的规则 | 使用加拿大邮政的规则 |
| 计算税费 | 美国州税和地税 | 使用 GST 和 PST |
| 支付货币 | 美元 | 加元 |
第三步:扩展场景和特征(加入德国)
| 共性概念 | 美国客户 | 加拿大客户 | 德国客户 |
|---|---|---|---|
| 计算运费 | UPS 费率 | 联邦快运海运费率 | 德国快运海运费率 |
| 验证地址 | 美国邮政规则 | 加拿大邮政规则 | 德国邮政规则 |
| 计算税费 | 州税+地税 | GST + PST | 德国 VAT |
| 支付货币 | 美元 | 加元 | 德国马克 |
| 日期格式 | mm/dd/yyyy | dd/mm/yyyy | dd/mm/yyyy |
| 最大重量 | 30kg | — | — |
第四步:按行识别规律(抽象策略)
每一行代表一个变化点,可用策略封装:
| 共性概念 | 抽象 |
|---|---|
| 计算运费 | ShippingStrategy 接口,封装运费计算规则 |
| 验证地址 | AddressValidator 接口,封装地址验证规则 |
| 计算税费 | TaxStrategy 接口,封装税费计算规则 |
| 支付货币 | Money 对象,包含货币名称和总量,自动转换 |
| 日期 | Time 对象,按用户 locale 提供不同显示格式 |
| 最大重量 | WeightLimitStrategy 接口,封装限重规则 |
→ 这些行对应 Strategy 模式。
第五步:按列识别约束(产品族)
每一列的对象必须配套使用(美国的运费算法必须配美国的地址验证、美国的税制、美元):
- 美国产品族:USShipping + USAddressValidator + USTax + USD
- 加拿大产品族:CAShipping + CAAddressValidator + CATax + CAD
- 德国产品族:DEShipping + DEAddressValidator + DETax + DEM
→ 这些列对应 Abstract Factory 模式。
第六步:匹配设计模式(行视角)
每一行 → Strategy 模式:
- 封装”做同一件事的不同方法”
- 算法族可独立于使用它的客户端而变化
第七步:匹配设计模式(列视角)
每一列 → Abstract Factory 模式:
- 保证同一产品族中对象的一致性(美国的工厂生产的全是美国规则的对象)
- 切换产品族只需切换工厂
第八步:综合得出高层设计
OrderProcessor(订单处理器)
│
▼
┌──────────────────┐
│ OrderConfigFactory ← Abstract Factory 接口
└────────┬─────────┘
│
┌─────┴──────┬──────────┐
▼ ▼ ▼
USFactory CAFactory DEFactory
│ │ │
▼ ▼ ▼
ShippingStrategy ← Strategy 接口
AddressValidator ← Strategy 接口
TaxStrategy ← Strategy 接口
Money / Time ← 值对象
设计收益
- 新增一个国家:新增一个具体工厂类 + 各策略的具体实现类,不影响现有代码(OCP)
- 新增一种策略(如新增”退货政策”):在抽象工厂接口加一个创建方法,各具体工厂实现,符合 OCP 但需改所有工厂(权衡点)
- 运行时切换国家:只需切换工厂对象
- 一致性保证:同一工厂生产的所有对象天然配套,不会出现”美国运费 + 加拿大税制”的错误组合
核心要点总结
- 设计模式不是孤立使用的:真实项目中常以组合形式出现
- 一次一模式方法:按”背景提供顺序”逐步应用模式,而非一次性全上
- Bridge + Adapter + Facade 层次:
- Bridge 负责两个维度的解耦(最底层)
- Adapter 负责接口适配(中间层)
- Facade 负责为客户端提供统一入口(最顶层)
- 变化分析矩阵:从具体场景出发,按行/列两个维度归纳,自动推导出 Strategy + Abstract Factory 的组合
- 横向看策略,纵向看工厂:矩阵的行是 Strategy,列是 Abstract Factory
- 客户沟通法则:信具体、不信概括;信场景、不信结论