面向对象建模
面向对象建模 — 书籍摘要
Section titled “面向对象建模 — 书籍摘要”书籍一:Object Design: Roles, Responsibilities, and Collaborations
Section titled “书籍一:Object Design: Roles, Responsibilities, and Collaborations”作者: Rebecca Wirfs-Brock & Alan McKean 出版: 2003 (Addison-Wesley)
对象设计的本质是将职责(responsibilities)分配给对象,并定义对象之间如何协作(collaborate)来实现系统行为。Responsibility-Driven Design(职责驱动设计,RDD)不从数据结构或继承层级入手,而是将对象视为自治的、智能的代理,在一个社区中扮演角色(roles)。好的对象设计源于思考对象做什么(它们的职责)以及它们与谁合作(它们的协作关系),而不是过早地决定它们是什么(它们的数据)。
1. Responsibility-Driven Design(职责驱动设计,RDD)
Section titled “1. Responsibility-Driven Design(职责驱动设计,RDD)”RDD 是本书的核心框架。它提供了一种替代 Data-Driven Design(数据驱动设计)的方法——后者从建模实体关系图开始,把行为放在最后考虑。在 RDD 中:
- 职责是对象承担的义务:它知道什么、它做什么、它做什么决策。
- 职责分为两类:
- 行为职责(Doing responsibilities):执行计算、创建其他对象、控制/协调活动等。
- 认知职责(Knowing responsibilities):了解私有数据、关联对象,或能推导或计算的内容。
- 设计的核心问题始终是:“谁应该负责这件事?“——而不是”这个对象持有什么数据?”
作者认为,基于职责的思维方式会带来更高内聚、更低耦合的对象,因为你是根据概念上合理的逻辑来分配行为,而不是根据数据的邻近关系。
2. 对象角色与 Stereotypes(原型)
Section titled “2. 对象角色与 Stereotypes(原型)”对象在设计中扮演角色(roles)。角色是一组相关的职责,对象在特定协作场景中承担这些职责。一个对象类可以在不同场景中扮演多个角色。
本书引入了对象原型(object stereotypes)——面向对象系统中常见的角色典型类别:
| Stereotype(原型) | 描述 |
|---|---|
| Information Holder(信息持有者) | 知道并提供信息(例如,Sensor 知道它当前的读数) |
| Structurer(结构管理者) | 维护对象间的关系以及这些关系的信息(例如,Catalog 组织 Product 条目) |
| Service Provider(服务提供者) | 执行工作并提供计算服务(例如,TaxCalculator) |
| Coordinator(协调者) | 响应事件并将任务委派给其他对象(例如,TransactionCoordinator) |
| Controller(控制者) | 做出决策并指挥其他对象的行为(例如,ApplicationController) |
| Interfacer(接口者) | 在系统不同部分之间转换信息和请求(例如,DatabaseGateway、UI 组件) |
这些原型并非严格的分类,而是思维工具。一个对象可以同时表现出多种原型的特征,但试图承担太多角色的对象通常意味着设计问题。这些原型帮助设计者识别一个对象是否正在变成无所不做的”上帝对象”(god object)。
示例: ShoppingCart 可能是一个 Structurer(维护商品集合及数量),同时也部分是一个 Information Holder(知道总价)。如果它还开始处理支付流程,就说明它承担了太多角色。
3. CRC 卡片
Section titled “3. CRC 卡片”CRC(Class-Responsibility-Collaboration,类-职责-协作)卡片是一种轻量级、可触摸的设计技术。每张索引卡代表一个候选类,分为三部分:
- 类名(顶部)
- 职责(左列)
- 协作者(右列——该类为履行职责所需合作的其他类)
这项技术最初由 Ward Cunningham 和 Kent Beck 开发。Wirfs-Brock 和 McKean 将其扩展为核心的设计探索工具:
- CRC 会议是团队设计工作坊,设计者扮演对象角色,手持卡片,推演场景。
- 索引卡的物理限制强制简洁——如果一张卡写不下所有职责,说明这个对象的职责可能太多了。
- CRC 卡片刻意保持非正式且可丢弃的特点,鼓励探索和快速迭代。
- 在推演场景时,参与者拿起一张卡片”成为”那个对象,然后问自己:“作为这个对象,我知道什么?我能做什么?我需要跟谁对话?”
书中的实用建议: 在提交任何类图或代码之前,尽早使用 CRC 卡片。它们是对话工具,不是文档工具。
4. 协作模式
Section titled “4. 协作模式”协作描述了对象之间如何交互以完成工作。本书识别了几种协作方式:
- 委托(Delegation):对象将请求传递给更适合处理的另一个对象。这是最基本的协作模式。委托方不做实际工作,它只知道谁能做。
- 转发(Forwarding):类似于委托,但转发方不承担额外职责——它只是原封不动地传递请求。
- 中介(Mediation):中介对象协调其他彼此不应直接了解的对象之间的交互。
- 协作链(Collaboration chains):请求通过一连串对象传递,每个对象贡献部分结果。
作者强调,好的协作应遵循 “告知,不要询问”(tell, don’t ask)风格(与 Law of Demeter / 迪米特法则相关):不要从对象中抽取数据然后在外部做决策,而是告诉对象要做什么,让它自己决定怎么做。这样可以保持封装性并分散智能。
示例: 不要写 if (account.getBalance() > amount) { account.setBalance(account.getBalance() - amount); },而应写 account.withdraw(amount),让 Account 自己决定如何处理,包括验证和副作用。
5. 设计流程
Section titled “5. 设计流程”本书描述了一个迭代式、探索式的设计流程:
- 探索问题域:从领域概念、用户故事或用例中识别候选对象。
- 分配职责:对每个场景,确定哪个对象应该处理哪个行为。
- 识别协作关系:确定每个对象需要与哪些其他对象合作。
- 精炼与整合:寻找职责重叠的对象,拆分过大的对象,合并过于琐碎的对象。
- 定义契约:明确公共接口——每个对象承诺向其协作者提供什么服务。
- 迭代:随着设计演进,重新审视先前的决策。
这明确不是瀑布流程。作者强调持续精炼,以及在理解加深时重新安排职责的意愿。
6. 对象契约与可靠性
Section titled “6. 对象契约与可靠性”对象的契约(contract)定义了它承诺为客户端做什么,以及它对客户端的要求是什么。这个概念与 Bertrand Meyer 的 Design by Contract(契约式设计)相关,但以更非正式的方式呈现:
- 前置条件(Preconditions):调用服务之前必须为真的条件。
- 后置条件(Postconditions):服务完成后对象保证为真的条件。
- 不变量(Invariants):关于对象状态始终为真的条件。
契约有助于在协作对象之间建立信任,并明确错误处理的责任:如果前置条件被违反,这是谁的错?
7. 为灵活性而设计
Section titled “7. 为灵活性而设计”作者讨论了如何设计能适应变化的系统:
- 组合优于继承(Composition over inheritance):优先通过协作对象组装行为,而不是构建深层继承树。继承在父类和子类之间创建了紧耦合。
- 热点与变化点(Hotspots and variation points):识别设计中最可能发生变化的位置(热点),并围绕它们设计抽象。在这些位置使用接口、抽象类或策略对象。
- 间接层(Indirection):插入中介对象以解耦系统各部分。每一层间接都增加了灵活性,但也增加了复杂性——要审慎使用。
- 可插拔行为(Pluggable behavior):设计对象时使其行为可以通过插入不同的协作者来改变(本质上就是 Strategy pattern / 策略模式)。
书中的比喻: 设计应该像一个组织良好的车间,每个工具都在它的位置上,每个工人都知道自己的工作。如果有新类型的任务到来,你应该能引入一个专家而无需重组整个车间。
8. Neighborhood(邻域)与子系统
Section titled “8. Neighborhood(邻域)与子系统”随着系统增长,作者主张将对象组织成邻域(neighborhoods)——紧密协作的对象集群,形成一个内聚的子系统。核心思想:
- 每个邻域有明确的目的和清晰的边界。
- 邻域间的通信应通过 Interfacer 或 Coordinator 对象进行,而不是内部对象之间的直接耦合。
- 这是职责驱动设计中”模块”或”包”的对应概念。
9. 可靠的协作
Section titled “9. 可靠的协作”本书讨论了协作出错时该怎么办:
- 对象应该优雅地失败,并清晰地传达错误。
- 防御性设计:在邻域的边界处验证输入,在邻域内部信任对象。
- 信任区域(trust regions)的概念:在邻域内部,对象之间可以更加信任。在边界处,则应进行更严格的检查。
10. 常见陷阱
Section titled “10. 常见陷阱”- 上帝对象 / Blob 类(God objects / Blob classes):一个对象积累了太多职责。症状:一个所有东西都依赖的巨大类。
- 贫血对象(Anemic objects):持有数据但没有行为的对象——本质上是美化的数据结构。这违反了 RDD 的核心原则。
- Feature Envy(特性依恋):一个对象不断深入另一个对象的数据来做决策。行为应该移到拥有数据的对象中。
- 过早的层级结构(Premature hierarchy):在理解问题之前就构建深层继承树。继承应该是被发现的,而不是被强加的。
- 消息链 / 火车残骸(Message chains / Train Wrecks):
a.getB().getC().doSomething()——违反了 Law of Demeter(迪米特法则),造成脆弱的耦合。 - 忽视协作设计:只关注单个类的设计而不考虑对象之间如何交互,会导致笨重且紧耦合的系统。
本书哲学总结
Section titled “本书哲学总结”设计就是做选择。好的设计将智能分散到一个对象社区中,每个对象都有清晰的职责和定义良好的协作关系。问题从来不是”这个对象持有什么数据?“而是”这个对象扮演什么角色?“
书籍二:Object-Oriented Methods: A Foundation, UML Edition
Section titled “书籍二:Object-Oriented Methods: A Foundation, UML Edition”作者: James Martin & James Odell 出版: 1998 (Prentice Hall)
面向对象的基础建立在少数几个深层概念原则之上——分类(classification)、组合(composition)、泛化(generalization)和关联(association)——这些原则反映了人类自然组织世界知识的方式。面向对象方法不仅仅是一种编程技术,更是一种对现实建模的方式,可以从高层业务分析一路应用到具体实现。本书为面向对象提供了严谨的概念基础,并将这些原则映射到 UML 表示法。
Martin 和 Odell 从分析优先的视角切入,认为先把概念模型做对比急于跳到实现结构更重要。他们仔细区分了被建模的现实世界概念与用来表示它们的软件构造。
1. 对象与类型
Section titled “1. 对象与类型”- 对象(object)是一个事物——一个概念、抽象或实体——具有明确的边界和身份,封装了状态和行为。
- 类型(type)或类(class)定义了一组共享公共特征的对象。类型与编程语言中的类不同——它们是概念性的范畴。
- 本书强调类型(概念层面)与类(实现层面)的区别。类型描述某物是什么;类描述它如何构建。
核心思想: 一个对象可以同时是多个类型的实例。一个人可以同时是 Employee(员工)、Customer(客户)和 Shareholder(股东)。这种多重分类(multiple classification)是人类思维的自然特征,即使某些编程语言使其难以实现。
2. 分类与泛化
Section titled “2. 分类与泛化”分类是根据共同属性将对象归入类型的行为。本书以不同寻常的严谨度对待它:
- 分类(Classification)vs. 泛化(Generalization):分类是将单个对象分配到类型中。泛化定义的是类型之间的关系(超类型-子类型层级)。
- 子类型化(Subtyping)意味着子类型的每个实例也是超类型的实例。
Penguin(企鹅)是一种Bird(鸟)。这就是”is-a”(是一种)关系。 - 多重继承(Multiple inheritance):一个类型可以有多个超类型。
FlyingCar(飞行汽车)可能既是Car又是Aircraft。本书承认这带来的复杂性,但将其视为合理的建模概念。 - 动态分类(Dynamic classification):对象可以随时间改变其类型。
Caterpillar(毛毛虫)变成Butterfly(蝴蝶)。今天是Employee的Person明天可能变成Retiree(退休人员)。大多数面向对象编程语言不直接支持这一点,但它是分析师应该了解的现实现象。[解读:这个概念预见了后来实践中更常见的模式,如 State pattern(状态模式)或基于角色的建模。] - Powertype(幂类型):一种其实例本身就是类型的类型。例如,
Species(物种)是Animal(动物)的 powertype——Species的每个实例(如Homo sapiens、Canis lupus)定义了一种动物类型。这是一个高级且较为抽象的概念,本书对此做了详细阐述。
分类方案:
- 分区(Partitioning):将一个类型划分为互斥的子类型(例如,
Person分为Male和Female)。 - 重叠分类(Overlapping classification):子类型不互斥(例如,一辆
Vehicle可以同时是LuxuryVehicle和ElectricVehicle)。
3. 组合(整体-部分关系)
Section titled “3. 组合(整体-部分关系)”组合(composition)是对象由其他对象构成的关系。本书区分了几种形式:
- 部件-整体组合(Component-Integral composition):部分物理包含在整体中(例如,汽车中的发动机)。移除部分会损害整体。
- 材料组合(Material composition):整体由某种材料构成(例如,桌子由木头制成)。材料在整体中失去了自身的身份。
- 份额组合(Portion composition):将同质量分成份额(例如,一块饼的切片)。
- 地点-区域组合(Place-Area composition):由子区域组成的地理区域(例如,一个国家由多个州组成)。
- 成员-群组组合(集合)(Member-Group composition / Collection):由成员组成的群组(例如,由球员组成的团队)。成员保持各自的身份。
- 成员-伙伴组合(Member-Partnership composition):成员在其中扮演特定角色的合作关系。
作者强调的关键区别在于组合(强所有权——部分不能独立存在)和聚合(aggregation,弱关联——部分有独立的生命周期)。在 UML 中,组合用实心菱形表示,聚合用空心菱形表示。
传播(Propagation):整体的属性传播到其部分,或反之。例如,移动汽车就移动了它的发动机。销毁文档就销毁了它的段落。作者指出,传播行为取决于组合的类型。
4. 关联与关系
Section titled “4. 关联与关系”关联(association)表示对象之间有意义的连接:
- 二元关联(Binary associations):两个类型之间的关联(例如,
Person works-for Company)。 - N元关联(N-ary associations):三个或更多类型之间的关联(例如,连接
Doctor、Patient和Drug的Prescription)。 - 多重性(基数)(Multiplicity / cardinality):一个类型的多少个实例可以与另一个类型的一个实例相关联(1, 0..1, , 1..)。
- 角色(Roles):关联的每一端都有一个角色名,阐明对象如何参与其中(例如,在
Employment关联中,Person扮演employee角色,Company扮演employer角色)。 - 限定关联(Qualified associations):通过限定符(qualifier)精炼的关联,在众多相关对象中进行选择(例如,
Bank有多个Account,通过accountNumber限定)。 - 关联类(Association classes):当关联本身具有属性或行为时(例如,
Employment有startDate和salary——这些属于关系本身,不属于任何一方参与者)。
5. 对象状态与行为
Section titled “5. 对象状态与行为”- 状态(State):对象在某一时间点的条件,由其属性值和关联决定。状态可以用状态图(state diagrams / statecharts)来建模。
- 事件(Events):在特定时间点发生的事情,触发状态转换。
- 操作(Operations):对象提供的服务。本书区分了:
- 查询操作(Query operations):不改变状态
- 修改操作(Modifier operations):改变状态
- 方法(Methods):操作的实现。一个操作在不同类型中可以有不同的方法(多态性)。
状态建模被视为理解对象生命周期——从创建、经过各种有效状态到销毁——的必要手段。
6. 规则与约束
Section titled “6. 规则与约束”本书赋予业务规则(business rules)和约束(constraints)作为一等建模元素的重要地位:
- 不变约束(Invariant constraints):必须始终成立(例如,储蓄账户的
Account余额不能为负)。 - 前置条件与后置条件:如 Design by Contract 所述——操作前/后必须成立的条件。
- 推导规则(Derivation rules):定义如何计算派生属性或关联(例如,
age由dateOfBirth和当前日期推导得出)。 - 刺激-响应规则(Stimulus-response rules):当事件 X 发生时,必须执行动作 Y(例如,“当库存降至阈值以下时,生成补货请求”)。
作者强调,规则应在分析阶段被捕获,而不是隐藏在代码中。它们是概念模型的关键组成部分。
7. UML 表示法与图
Section titled “7. UML 表示法与图”虽然本书写于 UML 最终标准化之前,但它将其概念框架映射到了 UML 表示法上:
- 类图(Class diagrams):展示类型、关联、泛化层级和组合。
- 对象图(Object diagrams):展示某一时间点的具体实例及其链接。
- 状态图(Statecharts)(State diagrams):展示对象生命周期和状态转换。
- 交互图(序列图与协作图)(Interaction diagrams):展示对象之间如何交换消息。
- 活动图(Activity diagrams):展示工作流和流程。
- 用例图(Use case diagrams):从用户视角捕获功能需求。
作者将 UML 视为表达更深层概念思想的表示系统,而非思想本身。他们警告不要将表示法与概念混淆。
8. 分析模式与可重用抽象
Section titled “8. 分析模式与可重用抽象”本书介绍了几种可重用的建模模式:
- Party pattern(参与方模式):
Person和Organization的通用抽象,两者都可以扮演Customer、Supplier等角色。 - Accountability pattern(责任关系模式):建模各方之间的责任关系(例如,组织层级、汇报结构)。
- Quantity pattern(数量模式):带计量单位的数量建模(例如,
Money有amount和currency)。 - Range pattern(范围模式):值范围的建模(例如,带有起止日期的
DateRange)。
[解读:这些模式与 Martin Fowler 的 Analysis Patterns(1997)有显著重叠,Fowler 明确表示受到了 Odell 的影响。]
9. 建模层次
Section titled “9. 建模层次”本书清晰地区分了三个层次:
- 概念层(分析)(Conceptual level / Analysis):按原样建模问题域——类型代表现实世界的概念,关联代表现实中的关系。不涉及实现细节。
- 规约层(设计)(Specification level / Design):定义软件组件的接口和契约。类型成为带有指定操作的类。
- 实现层(Implementation level):实际的代码、数据结构、算法。
作者强烈主张,分析应首先在概念层完成,不受实现关切的干扰。过早引入实现构造(指针、外键、存取器方法)会污染模型,使其更难理解。
10. 映射到实现
Section titled “10. 映射到实现”本书讨论了从分析到设计和实现的转换:
- 泛化到代码的映射:单继承可以直接映射;多重继承可能需要接口、委托或重构。
- 关联的映射:在实现中,关联变为引用、指针、集合或连接表。概念模型比代码直接表达的内容更丰富。
- 组合的映射:强组合意味着生命周期管理(整体被销毁时,部分也被销毁)。聚合则不然。
- 动态分类的映射:由于大多数语言缺乏这种动态类型化能力,可使用 State pattern(状态模式)、Role Object(角色对象)或委托等模式。
11. 过程与方法论指导
Section titled “11. 过程与方法论指导”本书主张迭代与增量的方法:
- 先建立一个广泛范围的模型,然后深入特定领域。
- 通过工作坊和评审与领域专家验证模型。
- 模型是沟通工具——在分析阶段必须让非程序员也能理解。
- 随着理解的加深不断精炼模型;没有模型在第一次就能完善。
12. 常见陷阱
Section titled “12. 常见陷阱”- 混淆类型与实例:将
January(一月)建模为Month(月份)的子类型,而非实例。 - 混淆泛化与组合:“汽车有一个发动机”是组合,不是继承。“跑车是一种汽车”是泛化。
- 过度分类:在简单属性就能解决时创建过多子类型(例如,创建
MaleCustomer和FemaleCustomer子类型,而不是用gender属性)。 - 忽略多重分类:在现实是多维度的情况下,强制使用单一分类层级。
- 过早的实现思维:在分析模型中添加数据库 ID、getter/setter 方法或实现数据类型。
- 忽视约束和规则:让业务规则隐含不明,留待”编码时再弄清楚”。
- 把地图当成领土:UML 图不是设计——它是设计的表示。将表示法误认为实质会导致肤浅的建模。
本书哲学总结
Section titled “本书哲学总结”面向对象建模是一门关于概念、范畴和关系的精确思考学科。现实世界是复杂的——对象扮演多重角色,随时间改变其本质,并存在于关系的网络之中。好的面向对象模型在概念层忠实捕捉这种复杂性,然后再为实现进行简化。表示法服务于概念,绝非反过来。
| 维度 | Wirfs-Brock & McKean | Martin & Odell |
|---|---|---|
| 主要关注点 | 设计——将行为分配给对象 | 分析——建模领域概念 |
| 核心问题 | ”谁负责这件事?" | "有哪些概念?它们之间有什么关系?“ |
| 关键技术 | CRC 卡片和角色扮演场景 | 严谨的分类和组合分类法 |
| 对象观 | 具有角色和职责的自治代理 | 概念模型中类型的实例 |
| 灵活性机制 | Stereotypes(原型)、委托、可插拔协作者 | Multiple classification(多重分类)、Dynamic classification(动态分类)、Powertypes(幂类型) |
| 表示法 | 非正式(卡片、图表) | 正式(UML) |
| 目标读者 | 设计者和开发者 | 分析师、架构师和建模者 |
| 与代码的关系 | 紧密——设计决策直接映射到实现 | 抽象——概念模型先于实现 |
| 共同点 | 两者都拒绝数据优先的思维方式;都强调行为、关系和迭代精炼 |
这两本书很好地互相补充:Martin & Odell 提供了理解领域的概念基础和精确词汇,而 Wirfs-Brock & McKean 提供了将这种理解转化为可工作设计的实用技术。结合使用,它们形成了从分析到设计的完整流程。