跳转到内容

面向对象建模


书籍一: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(知道总价)。如果它还开始处理支付流程,就说明它承担了太多角色。


CRC(Class-Responsibility-Collaboration,类-职责-协作)卡片是一种轻量级、可触摸的设计技术。每张索引卡代表一个候选类,分为三部分:

  • 类名(顶部)
  • 职责(左列)
  • 协作者(右列——该类为履行职责所需合作的其他类)

这项技术最初由 Ward Cunningham 和 Kent Beck 开发。Wirfs-Brock 和 McKean 将其扩展为核心的设计探索工具:

  • CRC 会议是团队设计工作坊,设计者扮演对象角色,手持卡片,推演场景。
  • 索引卡的物理限制强制简洁——如果一张卡写不下所有职责,说明这个对象的职责可能太多了。
  • CRC 卡片刻意保持非正式且可丢弃的特点,鼓励探索和快速迭代。
  • 在推演场景时,参与者拿起一张卡片”成为”那个对象,然后问自己:“作为这个对象,我知道什么?我能做什么?我需要跟谁对话?”

书中的实用建议: 在提交任何类图或代码之前,尽早使用 CRC 卡片。它们是对话工具,不是文档工具。


协作描述了对象之间如何交互以完成工作。本书识别了几种协作方式:

  • 委托(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 自己决定如何处理,包括验证和副作用。


本书描述了一个迭代式、探索式的设计流程:

  1. 探索问题域:从领域概念、用户故事或用例中识别候选对象。
  2. 分配职责:对每个场景,确定哪个对象应该处理哪个行为。
  3. 识别协作关系:确定每个对象需要与哪些其他对象合作。
  4. 精炼与整合:寻找职责重叠的对象,拆分过大的对象,合并过于琐碎的对象。
  5. 定义契约:明确公共接口——每个对象承诺向其协作者提供什么服务。
  6. 迭代:随着设计演进,重新审视先前的决策。

这明确不是瀑布流程。作者强调持续精炼,以及在理解加深时重新安排职责的意愿。


对象的契约(contract)定义了它承诺为客户端做什么,以及它对客户端的要求是什么。这个概念与 Bertrand Meyer 的 Design by Contract(契约式设计)相关,但以更非正式的方式呈现:

  • 前置条件(Preconditions):调用服务之前必须为真的条件。
  • 后置条件(Postconditions):服务完成后对象保证为真的条件。
  • 不变量(Invariants):关于对象状态始终为真的条件。

契约有助于在协作对象之间建立信任,并明确错误处理的责任:如果前置条件被违反,这是谁的错?


作者讨论了如何设计能适应变化的系统:

  • 组合优于继承(Composition over inheritance):优先通过协作对象组装行为,而不是构建深层继承树。继承在父类和子类之间创建了紧耦合。
  • 热点与变化点(Hotspots and variation points):识别设计中最可能发生变化的位置(热点),并围绕它们设计抽象。在这些位置使用接口、抽象类或策略对象。
  • 间接层(Indirection):插入中介对象以解耦系统各部分。每一层间接都增加了灵活性,但也增加了复杂性——要审慎使用。
  • 可插拔行为(Pluggable behavior):设计对象时使其行为可以通过插入不同的协作者来改变(本质上就是 Strategy pattern / 策略模式)。

书中的比喻: 设计应该像一个组织良好的车间,每个工具都在它的位置上,每个工人都知道自己的工作。如果有新类型的任务到来,你应该能引入一个专家而无需重组整个车间。


随着系统增长,作者主张将对象组织成邻域(neighborhoods)——紧密协作的对象集群,形成一个内聚的子系统。核心思想:

  • 每个邻域有明确的目的和清晰的边界。
  • 邻域间的通信应通过 InterfacerCoordinator 对象进行,而不是内部对象之间的直接耦合。
  • 这是职责驱动设计中”模块”或”包”的对应概念。

本书讨论了协作出错时该怎么办:

  • 对象应该优雅地失败,并清晰地传达错误。
  • 防御性设计:在邻域的边界处验证输入,在邻域内部信任对象。
  • 信任区域(trust regions)的概念:在邻域内部,对象之间可以更加信任。在边界处,则应进行更严格的检查。

  • 上帝对象 / Blob 类(God objects / Blob classes):一个对象积累了太多职责。症状:一个所有东西都依赖的巨大类。
  • 贫血对象(Anemic objects):持有数据但没有行为的对象——本质上是美化的数据结构。这违反了 RDD 的核心原则。
  • Feature Envy(特性依恋):一个对象不断深入另一个对象的数据来做决策。行为应该移到拥有数据的对象中。
  • 过早的层级结构(Premature hierarchy):在理解问题之前就构建深层继承树。继承应该是被发现的,而不是被强加的。
  • 消息链 / 火车残骸(Message chains / Train Wrecks):a.getB().getC().doSomething() ——违反了 Law of Demeter(迪米特法则),造成脆弱的耦合。
  • 忽视协作设计:只关注单个类的设计而不考虑对象之间如何交互,会导致笨重且紧耦合的系统。

设计就是做选择。好的设计将智能分散到一个对象社区中,每个对象都有清晰的职责和定义良好的协作关系。问题从来不是”这个对象持有什么数据?“而是”这个对象扮演什么角色?“



书籍二: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 从分析优先的视角切入,认为先把概念模型做对比急于跳到实现结构更重要。他们仔细区分了被建模的现实世界概念与用来表示它们的软件构造。


  • 对象(object)是一个事物——一个概念、抽象或实体——具有明确的边界和身份,封装了状态和行为。
  • 类型(type)或类(class)定义了一组共享公共特征的对象。类型与编程语言中的类不同——它们是概念性的范畴。
  • 本书强调类型(概念层面)与(实现层面)的区别。类型描述某物是什么;类描述它如何构建

核心思想: 一个对象可以同时是多个类型的实例。一个人可以同时是 Employee(员工)、Customer(客户)和 Shareholder(股东)。这种多重分类(multiple classification)是人类思维的自然特征,即使某些编程语言使其难以实现。


分类是根据共同属性将对象归入类型的行为。本书以不同寻常的严谨度对待它:

  • 分类(Classification)vs. 泛化(Generalization):分类是将单个对象分配到类型中。泛化定义的是类型之间的关系(超类型-子类型层级)。
  • 子类型化(Subtyping)意味着子类型的每个实例也是超类型的实例。Penguin(企鹅)是一种 Bird(鸟)。这就是”is-a”(是一种)关系。
  • 多重继承(Multiple inheritance):一个类型可以有多个超类型。FlyingCar(飞行汽车)可能既是 Car 又是 Aircraft。本书承认这带来的复杂性,但将其视为合理的建模概念。
  • 动态分类(Dynamic classification):对象可以随时间改变其类型。Caterpillar(毛毛虫)变成 Butterfly(蝴蝶)。今天是 EmployeePerson 明天可能变成 Retiree(退休人员)。大多数面向对象编程语言不直接支持这一点,但它是分析师应该了解的现实现象。[解读:这个概念预见了后来实践中更常见的模式,如 State pattern(状态模式)或基于角色的建模。]
  • Powertype(幂类型):一种其实例本身就是类型的类型。例如,Species(物种)是 Animal(动物)的 powertype——Species 的每个实例(如 Homo sapiensCanis lupus)定义了一种动物类型。这是一个高级且较为抽象的概念,本书对此做了详细阐述。

分类方案:

  • 分区(Partitioning):将一个类型划分为互斥的子类型(例如,Person 分为 MaleFemale)。
  • 重叠分类(Overlapping classification):子类型不互斥(例如,一辆 Vehicle 可以同时是 LuxuryVehicleElectricVehicle)。

组合(composition)是对象由其他对象构成的关系。本书区分了几种形式:

  • 部件-整体组合(Component-Integral composition):部分物理包含在整体中(例如,汽车中的发动机)。移除部分会损害整体。
  • 材料组合(Material composition):整体由某种材料构成(例如,桌子由木头制成)。材料在整体中失去了自身的身份。
  • 份额组合(Portion composition):将同质量分成份额(例如,一块饼的切片)。
  • 地点-区域组合(Place-Area composition):由子区域组成的地理区域(例如,一个国家由多个州组成)。
  • 成员-群组组合(集合)(Member-Group composition / Collection):由成员组成的群组(例如,由球员组成的团队)。成员保持各自的身份。
  • 成员-伙伴组合(Member-Partnership composition):成员在其中扮演特定角色的合作关系。

作者强调的关键区别在于组合(强所有权——部分不能独立存在)和聚合(aggregation,弱关联——部分有独立的生命周期)。在 UML 中,组合用实心菱形表示,聚合用空心菱形表示。

传播(Propagation):整体的属性传播到其部分,或反之。例如,移动汽车就移动了它的发动机。销毁文档就销毁了它的段落。作者指出,传播行为取决于组合的类型。


关联(association)表示对象之间有意义的连接:

  • 二元关联(Binary associations):两个类型之间的关联(例如,Person works-for Company)。
  • N元关联(N-ary associations):三个或更多类型之间的关联(例如,连接 DoctorPatientDrugPrescription)。
  • 多重性(基数)(Multiplicity / cardinality):一个类型的多少个实例可以与另一个类型的一个实例相关联(1, 0..1, , 1..)。
  • 角色(Roles):关联的每一端都有一个角色名,阐明对象如何参与其中(例如,在 Employment 关联中,Person 扮演 employee 角色,Company 扮演 employer 角色)。
  • 限定关联(Qualified associations):通过限定符(qualifier)精炼的关联,在众多相关对象中进行选择(例如,Bank 有多个 Account,通过 accountNumber 限定)。
  • 关联类(Association classes):当关联本身具有属性或行为时(例如,EmploymentstartDatesalary——这些属于关系本身,不属于任何一方参与者)。

  • 状态(State):对象在某一时间点的条件,由其属性值和关联决定。状态可以用状态图(state diagrams / statecharts)来建模。
  • 事件(Events):在特定时间点发生的事情,触发状态转换。
  • 操作(Operations):对象提供的服务。本书区分了:
    • 查询操作(Query operations):不改变状态
    • 修改操作(Modifier operations):改变状态
  • 方法(Methods):操作的实现。一个操作在不同类型中可以有不同的方法(多态性)。

状态建模被视为理解对象生命周期——从创建、经过各种有效状态到销毁——的必要手段。


本书赋予业务规则(business rules)和约束(constraints)作为一等建模元素的重要地位:

  • 不变约束(Invariant constraints):必须始终成立(例如,储蓄账户的 Account 余额不能为负)。
  • 前置条件与后置条件:如 Design by Contract 所述——操作前/后必须成立的条件。
  • 推导规则(Derivation rules):定义如何计算派生属性或关联(例如,agedateOfBirth 和当前日期推导得出)。
  • 刺激-响应规则(Stimulus-response rules):当事件 X 发生时,必须执行动作 Y(例如,“当库存降至阈值以下时,生成补货请求”)。

作者强调,规则应在分析阶段被捕获,而不是隐藏在代码中。它们是概念模型的关键组成部分。


虽然本书写于 UML 最终标准化之前,但它将其概念框架映射到了 UML 表示法上:

  • 类图(Class diagrams):展示类型、关联、泛化层级和组合。
  • 对象图(Object diagrams):展示某一时间点的具体实例及其链接。
  • 状态图(Statecharts)(State diagrams):展示对象生命周期和状态转换。
  • 交互图(序列图与协作图)(Interaction diagrams):展示对象之间如何交换消息。
  • 活动图(Activity diagrams):展示工作流和流程。
  • 用例图(Use case diagrams):从用户视角捕获功能需求。

作者将 UML 视为表达更深层概念思想的表示系统,而非思想本身。他们警告不要将表示法与概念混淆。


本书介绍了几种可重用的建模模式:

  • Party pattern(参与方模式):PersonOrganization 的通用抽象,两者都可以扮演 CustomerSupplier 等角色。
  • Accountability pattern(责任关系模式):建模各方之间的责任关系(例如,组织层级、汇报结构)。
  • Quantity pattern(数量模式):带计量单位的数量建模(例如,Moneyamountcurrency)。
  • Range pattern(范围模式):值范围的建模(例如,带有起止日期的 DateRange)。

[解读:这些模式与 Martin Fowler 的 Analysis Patterns(1997)有显著重叠,Fowler 明确表示受到了 Odell 的影响。]


本书清晰地区分了三个层次:

  • 概念层(分析)(Conceptual level / Analysis):按原样建模问题域——类型代表现实世界的概念,关联代表现实中的关系。不涉及实现细节。
  • 规约层(设计)(Specification level / Design):定义软件组件的接口和契约。类型成为带有指定操作的类。
  • 实现层(Implementation level):实际的代码、数据结构、算法。

作者强烈主张,分析应首先在概念层完成,不受实现关切的干扰。过早引入实现构造(指针、外键、存取器方法)会污染模型,使其更难理解。


本书讨论了从分析到设计和实现的转换:

  • 泛化到代码的映射:单继承可以直接映射;多重继承可能需要接口、委托或重构。
  • 关联的映射:在实现中,关联变为引用、指针、集合或连接表。概念模型比代码直接表达的内容更丰富。
  • 组合的映射:强组合意味着生命周期管理(整体被销毁时,部分也被销毁)。聚合则不然。
  • 动态分类的映射:由于大多数语言缺乏这种动态类型化能力,可使用 State pattern(状态模式)、Role Object(角色对象)或委托等模式。

本书主张迭代与增量的方法:

  • 先建立一个广泛范围的模型,然后深入特定领域。
  • 通过工作坊和评审与领域专家验证模型。
  • 模型是沟通工具——在分析阶段必须让非程序员也能理解。
  • 随着理解的加深不断精炼模型;没有模型在第一次就能完善。

  • 混淆类型与实例:将 January(一月)建模为 Month(月份)的子类型,而非实例。
  • 混淆泛化与组合:“汽车有一个发动机”是组合,不是继承。“跑车是一种汽车”是泛化。
  • 过度分类:在简单属性就能解决时创建过多子类型(例如,创建 MaleCustomerFemaleCustomer 子类型,而不是用 gender 属性)。
  • 忽略多重分类:在现实是多维度的情况下,强制使用单一分类层级。
  • 过早的实现思维:在分析模型中添加数据库 ID、getter/setter 方法或实现数据类型。
  • 忽视约束和规则:让业务规则隐含不明,留待”编码时再弄清楚”。
  • 把地图当成领土:UML 图不是设计——它是设计的表示。将表示法误认为实质会导致肤浅的建模。

面向对象建模是一门关于概念、范畴和关系的精确思考学科。现实世界是复杂的——对象扮演多重角色,随时间改变其本质,并存在于关系的网络之中。好的面向对象模型在概念层忠实捕捉这种复杂性,然后再为实现进行简化。表示法服务于概念,绝非反过来。



维度Wirfs-Brock & McKeanMartin & Odell
主要关注点设计——将行为分配给对象分析——建模领域概念
核心问题”谁负责这件事?""有哪些概念?它们之间有什么关系?“
关键技术CRC 卡片和角色扮演场景严谨的分类和组合分类法
对象观具有角色和职责的自治代理概念模型中类型的实例
灵活性机制Stereotypes(原型)、委托、可插拔协作者Multiple classification(多重分类)、Dynamic classification(动态分类)、Powertypes(幂类型)
表示法非正式(卡片、图表)正式(UML)
目标读者设计者和开发者分析师、架构师和建模者
与代码的关系紧密——设计决策直接映射到实现抽象——概念模型先于实现
共同点两者都拒绝数据优先的思维方式;都强调行为、关系和迭代精炼

这两本书很好地互相补充:Martin & Odell 提供了理解领域的概念基础和精确词汇,而 Wirfs-Brock & McKean 提供了将这种理解转化为可工作设计的实用技术。结合使用,它们形成了从分析到设计的完整流程。