全书分为理论和应用两部分。理论部分深刻剖析了面向对象分析与设计(OOAD)的概念和方法。应用部分连续列出了5 个不同类型、不同领域的应用,描述如何从初始阶段到交付阶段,将OOAD 理论和方法应用到项目中。本书分成3 篇:概念、方法和应用,其中穿插了大量的补充材料。
概念
第 1 篇研究软件的内在复杂性及其表现方式。本书将对象模型作为一种手段来帮助我们管理这种复杂性,详细地研究了对象模型的基本元素——抽象、封装、模块化、层次结构,讨论了“什么是类?”以及“什么是对象?”等基本问题。由于确定有意义的类和对象是面向对象开发中的关键任务,因此我们花了相当多的时间来研究分类的本质。
方法
第 2 篇基于对象模型提出了复杂系统开发的一种方法。针对面向对象的分析与设计,首先提出了一套图形表示法(即UML),然后是一个通用的过程框架。还研究了面向对象开发的实践,具体来说,就是它在软件开发生命周期中的位置以及它对于项目管理意味着什么。
应用程序
第 3 篇提供了一组(5 个)不简单的例子,涉及不同问题域:系统架构、控制系统、密码分析、数据获取和Web 开发。
1. 复杂性
1.1. 复杂系统的结构
说明了个人计算机、植物和动物、物质的结构、社会机构的复杂结构
1.2. 软件固有的复杂性
主要关注工业级软件的复杂性、软件在本质上就是复杂的。问题域复杂、管理开发过程的困难性、软件中随处可能出现的灵活性(软件行业缺少标准)、描述离散系统行为的问题。
1.3. 复杂系统的5个属性
层次结构
相对本原(选择哪些作为系统的基础组件相对来说比较随意)
分离关注(组件内的联系通常比组件间的联系更强)
共同模式(层次结构通常只是少数不同类型的子系统按照不同的组合和安排方式构成的)
稳定的中间形式(复杂系统毫无例外都是从能工作的简单系统演变而来的)。
1.4. 有组织和无组织的复杂性
复杂系统的规范形式(类结构part of和对象结构is a)
人在处理复杂性时的能力局限
1.5. 从混沌到有序
分解的作用(算法分解、面向对象分解。自顶而下的结构化设计、数据驱动设计、面向对象设计)
抽象的作用
层次结构的作用
1.6. 复杂系统的设计
作为科学和艺术的工程
设计的含义,创造某个问题的解决方案,从而提供实现需求的途径。包括建模的重要性、软件方法学的要素、面向对象开发的模型。
1.7. 本章总结
软件本质上是复杂的,软件系统的复杂性常常超出了人类智能的范围。
软件开发团队的任务就是制造出简单的假象。
复杂性常常以层次结构的形式表现出来,建立复杂系统的“是一种”和“组成部分”
层次结构模型是有意义的。
复杂系统通常是从一些稳定的中间状态演进而来的。
人类的认识有一些基本的限制因素,我们可以通过分解、抽象和层次结构来克服这些限制。
复杂系统可以从事物或处理过程的角度来分析,采用面向对象的分解有一些令人感兴趣的理由。在这种方法中,将世界看做是一组有意义的对象进行协作,实现某种高级的行为。
面向对象分析和设计的方法实现了面向对象分解。面向对象的设计采用了一套表示法和过程来构造复杂软件系统,提供了丰富的模型,可以通过这些模型来阐明目标系统的不同方面。
2. 对象模型
对象模型包括抽象、封装、模块化、层次结构、类型、并发和持久等原则。
2.1. 对象模型的演进
有两个大趋势,关注点从小规模编程向大规模编程转变;高级程序设计语言的演进。
程序设计语言的换代、一代及二代早期语言的拓扑结构、二代后期和三代早期程序设计语言的结构、第三代后期程序设计语言的结构、基于对象和面向对象的程序设计语言的结构。
2.2. 对象模型的基础
2.2.1. 面向对象编程
是一种实现的方法,在这种方法中,程序被组织成许多组相互协作的对象,每个对象代表某个类的一个实例,而类则属于一个通过继承关系形成的层次结构。
这个定义有三个要点:(1)利用对象作为面向对象编程的基本逻辑构建块(第1 章中介绍的“组成部分”层次结构),而不是利用算法;(2)每个对象都是某个类的一个实例;(3)类与类之间可以通过继承关系联系在一起(第1 章中介绍的“是一个”层次结构)。
如果一种语言不提供对继承的直接支持,那么它就不是面向对象的,而是基于对象的。
2.2.2. 面向对象设计
是一种设计方法,包括面向对象分解的过程和一种表示法,这种表示法用于展现被设计系统的逻辑模型和物理模型、静态模型和动态模型。
这个定义中有两个要点:(1)面向对象设计导致了面向对象分解;(2)面向对象设计使用了不同的表示法来表达系统逻辑设计(类和对象结构)和物理设计(模块和处理架构)的不同模型,以及系统的静态和动态特征。
对面向对象分解的支持是面向对象设计与结构化设计的不同之处:前者利用类和对象抽象来构建逻辑系统结构,后者则利用算法抽象。
2.2.3. 面向对象分析
是一种分析方法,这种方法利用从问题域的词汇表中找到的类和对象来分析需求。
OOA、OOD 和OOP 之间的关系如何?基本上,面向对象分析的结果可以作为开始面向对象设计的模型,面向对象设计的结果可以作为蓝图,利用面向对象编程方法最终实现一个系统。
2.3. 对象模型要素
存在5 种主要的编程风格,这5种风格以及它们使用的抽象如下。
(1)面向过程 算法;
(2)面向对象 类和对象;
(3)面向逻辑 目标,通常以谓词演算的方式表示;
(4)面向规则 如果-那么规则;
(5)面向约束 不变的关系。
对于所有面向对象的东西,概念框架就是对象模型。这个模型有4 个主要要素:(1)抽象;(2)封装;(3)模块化;(4)层次结构。
对象模型有3 个次要要素:(1)类型;(2)并发;(3)持久。
2.3.1. 抽象的意义
抽象是我们人类处理复杂性的基本方式。描述了一个对象的基本特征,可以将这个对象与所有其他类型的对象区分开来,因此提供了清晰定义的概念边界,它与观察者的视角有关。“最少承诺”原则。
从那些准确地为问题域实体建模的对象到那些实际上没有什么理由存在的对象,存在着一系列的抽象。按最有用到最没有用的次序,这些抽象是:
实体抽象:一个对象,代表了问题域或解决方案域实体的一个有用的模型;
动作抽象:一个对象,提供了一组通用的操作,所有这些操作都执行同类的功能;
虚拟机抽象:一个对象,集中了某种高层控制要用到的所有操作,或者这些操作将利用某种更低层的操作集;
偶然抽象:一个对象,封装了一组相互间没有关系的操作。
抽象思想的核心是不变性的概念。“不变量(invariant)”是某种布尔(真或假)条件,它的值必须保持不变。对于对象的每个操作,我们可以定义“前置条件(precondition)”(操作假定的不变量)和“后置条件(postcondition)”(操作满足的不变量)。违反一个不变量将破坏一个抽象相关的契约。如果违反了前置条件,就意味着客户没有完成它那部分的责任,因此服务器不能可靠地执行。类似地,如果违反了后置条件,就意味着服务器没有完成它那部分的责任,所以客户不能再信任服务器的行为。
2.3.2. 封装的意义
抽象和封装是互补的概念:抽象关注的是对象可以观察到的行为,而封装关注这种行为的实现。
封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作用是分离抽象的概念接口及其实现。
2.3.3. 模块化的意义
模块化是一个系统的属性,这个系统被分解为一组高内聚、低耦合的模块。抽象、封装和模块化的原则是相辅相成的。
2.3.4. 层次结构的意义
层次结构是抽象的一种分级或排序。
单继承、多继承、聚合。
2.3.5. 类型的意义
类型是关于一个对象的类的强制规定,这样一来,不同类型的对象不能够互换使用,或者至少它们的互换使用受到非常严格的限制。
类型的强与弱和类型的静态与动态的概念是完全不同的。类型的强与弱指的是类型一致性,而类型的静态与动态指的是名字与类型绑定的时间。静态类型(也称为静态绑定或早期绑定)意味着所有变量和表达式的类型在编译时就固定了,动态类型(也称为延迟绑定)意味着所有变量和表达式的类型直到运行时刻才知道。
2.3.6. 并发的意义
并发是一种属性,它区分了主动对象和非主动对象。每个对象(来自于真实世界的一个抽象)都可以代表一个独立的控制线程(一种过程抽象)。这样的对象被称为“主动的”。
2.3.7. 持久的意义
持久是对象的一种属性,利用这种属性,对象跨越时间(例如,当对象的创建者不存在了的时候,对象仍然存在)和空间(例如,对象的位置从它被创建的地址空间移开)而存在
2.4. 应用对象模型:
对象模型的好处,首先使用对象模型帮助我们探索基于对象和面向对象编程语言的表达能力。
其次,利用对象模型不仅鼓励软件的复用,而且鼓励整个设计的复用,这导致了可复用应用框架的产生。
第三,使用对象模型将得到构建在稳定的中间状态之上的系统,这样的系统更适合变化。
最后,对象模型引起了对人类认知工作的兴趣。Robson 说,“许多不知道计算机如何工作的人会发现,面向对象系统的思想非常自然”。
3. 类与对象
3.1. 对象的本质
3.1.1. 什么是对象,什么不是对象
从人类认知的角度来看,对象可以是一个可以触摸或可以看见的东西;在智力上可以理解的东西;可以指导思考或行动的东西。
某些对象可能有明确的概念边界,但其代表的是不可触摸的事件或过程。例如,在制造工厂中的一个化学处理过程可能被作为一个对象,因为它具有明确的概念边界,通过一组有序的操作,在不同的时间与某些其他对象打交道,并展示出定义良好的行为。
对象是一个具有状态、行为和标识符的实体。结构和行为类似的对象定义在他们共同的类中。‘实例’和‘对象’这两个术语可以互换使用。
3.1.2. 对象的状态
包括这个对象的所有属性(通常是静态的)以及每个属性当前的值(通常是动态的)。
3.1.3. 行为
是对象在状态改变和消息传递方面的动作和反应的方式。
对象像自动机,主动的对象有自己的控制线程,而被动的对象则没有。主动的对象通常是自动的,这意味着它们不需要由其他对象操作,就能表现出一些行为。而对于被动对象来说,只有在显式地操作它时,才会发生状态变化。
一个对象的操作常见的有
修改操作:更改一个对象的状态的操作。
选择操作:访问一个对象的状态但并不更改这个状态的操作。
遍历操作:以一种定义良好的方式访问一个对象的所有部分的操作。
构造操作:创建一个对象并初始化它的状态的操作。
析构操作:释放一个对象的状态并销毁对象本身的操作。
3.1.4. 标识符
是一个对象的属性,它区分这个对象与其他所有对象。
3.2. 对象之间的关系
包括链接和聚合。
3.2.1. 链接
3.2.2. 聚合
聚合可以代表物理上的包含,也可以不代表。股票持有人及其持有股票之间的关系则是不需要物理上包含的聚合关系。
3.3. 类的本质
3.3.1. 什么是类,什么不是类
类是一组对象,它们拥有共同的结构、共同的行为和共同的语义。
类仅代表一种抽象,即一个对象的“本质”。某些抽象很复杂时可以用一组类表示。
3.3.2. 接口和实现
编程在很大程度上是一种“制定契约”:一个较大问题的不同功能通过子契约被分配给不同的设计元素,从而被分解成较小的问题。
一个单独的对象是一个具体实体,在整体系统中扮演某个角色,而类则记录了所有相关对象的共同结构和行为。因此,类起到的作用是在一种抽象和所有它的客户之间建立起协议。
3.3.3. 类的生命周期
3.4. 类之间的关系
存在三种基本类型的类关系[22]。
第一种关系是一般/特殊关系,表示“是一种”关系。例如,玫瑰是一种花,这意味着玫瑰是一种特殊的子类,而花是更一般的类。
第二种关系是整体/部分,表示“组成部分”关系。因此,花瓣不是一种花,它是花的一个部分。
第三种关系是关联,表示某种语义上的依赖关系,如果没有这层关系,这些类就毫无关系了,如瓢虫和花之间的关系。再如,玫瑰和蜡烛基本上是独立的类,但它们都是可以用来装饰餐桌的东西。
3.4.1. 关联
3.4.2. 继承
代表一般/特殊的关系。继承的一种替代方法是一种所谓“委托”的语言机制,通过这种机制,对象将它们的行为委托给一些相关的对象。
在继承和封装之间存在着非常真实的压力。从大的方面来讲,利用继承暴露了被继承类的一些秘密。具体来说,这意味着要理解某个类的含义,就必须常常研究它的所有超类,有时还要研究超类的内部视图。
多态是类型理论中的一个概念,即一个名字可能代表许多不同类的实例,只要它们都有共同的超类。于是,由这个名字所代表的对象就能够以不同的方式对同一组操作做出反应。利用多态,一个操作可以被层次结构中的类以不同的方式实现。通过这种方式,子类可以扩展超类的能力,或者覆写父类的操作。
多态和延迟绑定是分不开,方法和名字的绑定要在执行时确定。
3.4.3. 聚合
提供了类实例中的整体/部分关系。
聚合不一定是物理上的包容。例如,虽然股票持有人拥有股票,但股票持有人并没有在物理上包容拥有的股票。相反,这些对象的生存期完全可以是独立的,虽然概念上的整体/部分关系仍然存在(每股股票都是股票持有者的一部分资产)。
3.4.4. 依赖关系
警告了设计者,如果其中一个元素发生了改变,可能会影响到另一个元素。
3.5. 类与对象的互动
3.5.1. 类与对象的关系
类是静态的,对象是动态的。
3.5.2. 类与对象在分析和设计中的角色
在分析阶段和设计的早期阶段,开发者有两项主要任务:
从问题域的词汇表中确定出类;
创建一些结构,让多组对象一起工作,提供满足问题需求的行为。
3.6. 创建高品质的类与对象
系统应该利用一组最少的不会变化的部分进行构建,这些部分应该尽可能地通用,系统的所有部分应该被放入一个统一的框架。”对于面向对象开发来说,这些部分就是构成系统关键抽象的类和对象,这个框架由系统的机制来提供。
3.6.1. 评判一种抽象的品质
耦合
内聚
充分性
完整性
基础性
一个类或模块应该是充分的、完整的、简单的。所谓充分,指的是类或模块应该记录某个抽象足够多的特征,从而允许有意义的、有效的交互。
所谓完整,指的是类或模块的接口记录了某个抽象全部有意义的特征。充分性意味着最小的接口,但一个完整的接口就意味着该接口包含了某个抽象的所有方向。因此完整的类或模块是足够通用的,可以被任何客户使用。
基础性操作就是只有访问该抽象的底层表现形式才能够有效地实现的那些操作。因此,对一个集合添加一个元素是一项基础性操作,因为要实现这个Add 操作,必须知道底层表现形式。另一方面,向集合添加4 项元素的操作不是基础性操作,因为它可以通过更为基础性的Add 操作来有效地实现,不必访问底层的表现形式。
3.6.2. 选择操作,即方法
l 功能语义
对于一个给定的类,我们的风格是让所有的操作保持基础性,这样每个操作都展示出小的、定义良好的行为。我们把这样的方法称为“细致的(fine-grained)”。我们也倾向于分离方法,让它们相互之间不通信。通过这种方式,我们更容易构造一些子类,它们可以有意义地重新定义超类的行为。
将一个行为提取为一个方法还是多个方法的决定有两个互相竞争的原因:将一个行为塞进一个方法中将导致更简单的接口,但方法会更大、更复杂;将一个行为分散到多个方法中将导致更复杂的接口,但方法会更简单。
Meyer 指出:“好的设计者知道如何在在太多契约和太少契约之间平衡折中,太多契约导致片段化,太少契约导致无法管理的大模块。”
我们必须决定将方法放到哪个类中
可复用性:这个行为可以在多种上下文中使用吗?
复杂性:实现这个行为的难度有多大?
适用性:这个行为与打算放入的类型之间相关程度如何?
实现知识:这个行为的实现依赖于一个类型的内部细节吗?
l 时间和空间语义
主要指多线程。
3.6.3. 选择关系
l Demeter 法则
类的方法不应该以任何方式依赖于任何类的结构,除了它自己类的当前(顶层)结构之外。而且,每个方法只能够对一个非常有限的类集的对象发出消息”。
l 机制和可见性
决定对象之间的关系主要是设计这些对象进行交互的机制。开发者要问的问题很简单:这些知识应该放在哪里?例如,在一份制造计划中,物料(称为批次)进入制造车间等待处理。当它们进入特定的车间时,我们必须通知车间的经理采取相应的行动。现在面临一个设计选择:一个批次的物料进入一个车间,这个操作是车间的操作,是批次的操作,还是两者的操作?如果我们决定它是车间的操作,那么车间就必须对批次可见。如果我们决定它是批次的操作,那么批次就必须对车间可见,因为必须知道它进了哪个车间。最后,如果我们认为它既是车间的操作,也是批次的操作,那么就必须使它们互相可见。我们还必须决定车间和经理之间的某种可见关系(而不是批次和经理),经理必须知道要管理的车间,或者车间必须知道它的经理。
3.6.4. 选择实现
只有当我们使某个类或对象的外部视图稳定下来之后,才会转向它的内部视图。
l 表示形式
类或对象的表示形式几乎总是该抽象封装起来的秘密。
l 打包
3.7. 小结
对象具有状态、行为和标识符。
类似对象的结构和行为定义在它们共同的类中。
对象的状态包括对象所有的属性(通常是静态的)加上这些属性当前的值(通常是动态的)。
行为是对象在状态改变和消息传递方面的动作和反应。
标识符是对象的属性,它区分这个对象和其他对象。
类是具有共同的结构和行为的一组对象。
三种关系包括关联、继承和聚合。
关键抽象是来自于问题域词汇表的类和对象。
机制是一种结构,一组对象通过它互相协作,提供满足问题域的某种需求的行为。
抽象的品质可以通过它的耦合、内聚、充分性、完整性和基础性来度量。
4. 分类
4.1. 正确分类的重要性
确定类和对象是面向对象分析和设计中具有挑战性的部分。分类帮助我们确定类之间的泛化、特化、聚合等层次结构。通过识别对象交互中的共同模式,我们逐渐发明一些机制,成为实现的核心。
4.1.1. 分类的困难