第1章 面向对象思想
第2章 抽象
第3章 面向对象设计
第4章 类和方法
第5章 消息,实例和初始化
第6章 案例研究: 八皇后问题
第7章 研究研究: 台球游戏
第8章 继承与替换
第9章 案例研究: 纸牌游戏
第10章 子类和子类型
第11章 静态行为和动态行为
第12章 替换的本质
第13章 多重继承
第14章 多态及软件复用
第15章 重载
第16章 改写
第17章 多态变量
第18章 泛型
第19章 容器类
第20章 案例研究: 标准模板库
第21章 框架
第22章 框架实例: AWT和Swing
第23章 对象互连
一种考虑对象互连的方式就是研究可视性(visibility)和依赖性(dependency)这两个概念.可视性这个软件工程术语描述了关于名称的特性----通过该名称句柄可以存取对象.如果对象的名称是合法的且代表该对象,那么在这个特定的环境下,该对象就是可见的.通常,用于描述可视性的相关术语还包括标识符的范畴(scope)
可视性与连通性的关系体现在: 如果我们能够控制并降低作为标识符的名称的可见性,那么就能更加容易确定应该如何使用标识符.
依赖性这个概念将两个对象或者类联系起来.在不存在另外一个对象的条件下,如果一个对象的存在无任何意义,我们就说该对象依赖于另外那个对象.例如,子类几乎总是依赖于它的父类.
23.1 耦合和内聚
耦合(coupling)和内聚(cohesion)的思想提供了一个框架,用于评价对象和类的应用是否有效.耦合描述了类之间的关系,而聚合描述了类内部的关系.因此,如果想要降低类之间的互连性,可以通过减少类之间的耦合来实现.另一方面,设计良好的类应该具有特定的目的;所有元素都应该与一个任务相关.这意味着在一个良好的设计中,类内部的元素应该具有内部的内聚性
23.1.1 耦合的种类
类之间的耦合可以有各种原因,其中某些耦合比另外一些耦合更可接受或更理想.从最差到较好的耦合如下面所示
内部数据耦合
全局数据耦合
控制(或顺序)耦合
组件耦合
参数耦合
子类耦合
内部数据耦合发生在当一个类的实例直接修改另外一个类中的本地数据值(实例变量)时.
class SneakyModifier { public: void sneaky() { // change my friends name myFriend->name = "Lucy"; } Person * myFriend; }; class Person { public: Person() { name = "Larry"; } string name; };
内部数据耦合这种做法非常不好的原因在于,这种方式使得想要独立理解类变得非常复杂.如果某个开发者只负责当前的类,那么如果这个类的内部数据字段戏剧性地被外部的某种行为所修改,该开发者将如何知道这一行为呢?这种做法将使程序非常难以理解和推断,应该尽可能地避免.
全局数据耦合发生在两个或者更多个类都依赖于公用的全局数据结构而绑定到一起的时候
double todaysDow; class One { public: void setDow() { todaysDow = 9473; } }; class Two { public: void printDow() { cout << "Today the Dow hit " << todayDow; } };
同样,这种耦合形式不好的原因在于独立理解类会变得非常复杂.每个类自身都是不完整的,只有同时研究多个类定义时,才能够理解这些类之间的相互作用.但是,全局数据耦合有时是不可避免的
在实践中,如何区分下面这两种全局变量非常重要.在涉及多文件的程序中,某些全局变量具有文件范畴(file scope),这意味着只能在一个文件中使用这些变量.另一种全局变量则具有程序范畴(program scope),这意味着可以在程序中的任何位置对其进行修改.了解具有程序范畴的全局变量的使用比了解具有文件范畴的全局变量的使用要困难得多
许多语言都提供了用来在单独类与整个程序之间控制名称可视性的技术.这方面的实例包括C++语言中的名称空间,Java语言中的包以及Object Pascal语言中的单元等.这些特征支持某些名称具有超过一个单独类但却小于整个程序的范畴.当对象之间通过这种数值进行交互时,程序之间发生作用的部分减少了,因此这种做法将比对象之间通过真正的全局变量进行交互的做法要更好,但是,这种耦合仍然难以理解,并且也应该尽可能地避免
在面向对象框架中,可能存在的代替全局数据耦合的方法就是建立新类,整个类负责"管理"数据值,所有对全局数值的存取都需经过这个类(这种方法类似于我们使用存取函数来屏蔽在对象内部直接存取本地数据的做法).这种技术将全局数据耦合降低到参数耦合级别上,而参数耦合更容易理解和控制.在Java语言中,不存在全局变量,所有的数值都必须通过类来管理
class MyClass { public: void mustDoFirst() { ... } void doSecond() { ... } void doThird() { ... } };
控制或者顺序耦合发生在一个类必须以一种由任意位置控制的特定的顺序来执行操作时.数据库系统可能会顺序经历以下几个步骤:执行初始化阶段,读取当前记录,更新记录,删除记录,以及产生报告等.但是,每个阶段都通过不同的例程来调用,并且调用顺序依赖于不同位置上的代码.控制耦合的存在说明类的设计者只需跟随低级别的抽象即可(每个步骤都对应一条指令,“处理数据库”).即使控制耦合不可避免,通常也会谨慎地要求正被顺序化的类必须确保其自身能够以正确的顺序实现操作,而并不依赖于调用者的正确处理
组件耦合发生在一个类包含的数据字段或数值为另外一个类的实例时.组件耦合的这种关系是一种理想的耦合方式.容器显然知道它所包含的数值所属的类,但是它所包含的元素却不应该知道关于它所处的容器的情况
class Set { . . . private: List data; };
参数耦合发生在一个类必须调用另外一个类的服务和例程时,此时两个类之间所发生的唯一关系就是一个类需要为另一个类提供参数数目,类型和返回值类型.这种耦合形式很常见,容易理解,并易于验证(例如,使用检查参数调用与定义不匹配的工具);因此,这是一种最良好的耦合方式
class MyClass { public: void doSomething(Set aSet) { // do something using the argument value . . . } }
子类耦合是面向对象编程所特有的.它描述了一个类与其父类之间的关系.通过继承,子类的实例可以被看成父类的实例
class Parent { . . . } class Child extends Parent { . . . }
23.1.2 内聚的种类
类的内部内聚性是该结构之中各个元素之间绑定程度的量度.与耦合类似,从最弱的内聚(最少期望的)到最强的内聚(最期望的)依次排列的结果如下所示
随机内聚
逻辑内聚
时间内聚
通信内聚
顺序内聚
功能内聚
数据内聚
随机内聚发生在没有明显原因而对一个类的元素进行分组时.通常这都是对一个大型程序随意划分成多个部分(类似于模块化)的结果.这往往是设计不好的一个标志.在面向对象框架中,当类由多个无关的方法组成时,我们就称这种情况为随机内聚
逻辑内聚发生在每个数据或控件中,类的各个元素之间存在着逻辑的联系但并不存在实际的联系时.如果算术函数库(正弦, 余弦等函数)中的各个函数都在不参照其他函数的条件下独立实现各自的功能,那么该函数库就展示了一种逻辑内聚
时间内聚发生在由于多个元素几乎同时使用而绑定到一起时.典型的例子就是用来实现程序初始化的类.这里,更好的设计是通过不同的类来实现不同的初始化活动,使每个类都负责更具体的后续行为
通信内聚发生在一个类的所有方法由于需要存取相同的"输入/输出"数据或设备而组合到一起时.类将扮演数据或者设备的"管理者"的角色
23.1.3 德墨特尔法则
23.1.4 类级别可视性与对象级别可视性
23.1.5 活动值
23.2 子类客户和用户客户
23.3 存储控制和可视性
23.3.1 Smalltalk语言中的可视性
23.3.2 C++语言中的可视性
23.3.3 Object Pascal语言中的可视性
23.3.4 Java语言中的可视性
23.3.5 Objective-C语言中的可视性
第24章 设计模式
第25章 反射和内省
第26章 分布式对象
第27章 实现
参考文献