第六章 可以工作的类
成为高效程序员的一个关键就在于,当你开发程序任一部分的代码时,都能安全地忽视程序中尽可能多的其余部分,而类就是实现这一目标的首要工具。
6.1 类的基础:抽象数据类型(ADTs)
首先考虑ADT,而后考虑类,这是一个“深入一种语言去编程”而不是“在一种语言上编程”的例子。
一.使用ADT的益处:
1.可以隐藏实现细节
2.改动不会影响到整个程序
3.让接口能提供更多信息
4.更容易提高性能
5.让程序的正确性更显而易见
6.程序更具自我说明性
7.无须在程序内到处传递数据
8.你可以像在现实世界中那样操作实体,而不用在底层实现上操作它
二.一些指导建议:
1.把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据类型
2.把像文件这样的常用对象当成ADT
3.简单的事物也可当做ADT
4.不要让ADT依赖其存储介质
考虑类的一种方式,就是把类看作是抽象数据类型再加上继承和多态两个概念。
6.2 良好的类接口
一.好的抽象
个人理解,一层抽象即对应一个类。
创建类的抽象接口的指导建议:
1.类的接口应该展现一致的抽象层次
在考虑类的时候有一种很好的方法,就是把类看做一种用来实现抽象数据类型的机制。
每个类应该实现一个ADT,并且仅实现这个ADT。
2.一定要理解所实现的抽象是什么
3.提供成对的服务
在设计一个类时,要检查每一个公用子程序,决定是否需要另一个与其互补的操作。
不要盲目创建相反操作,但你一定要考虑,看看是否需要它。
4.把不相关的信息转移到其他类中
5.尽可能让接口可编程,而不是表达语义
每个接口都由一个可编程的部分和一个语义部分组成。
可编程的部分由接口中的数据类型和其他属性组成,编程器可以强制要求。
语义部分则由“本接口将会怎么样使用"的假定组成,编译器无法强制实施。如RoutineA必须在RoutineB之前被调用。
语义接口应通过注释说明,但要尽可能让接口不依赖这些说明,如果很依赖,那么要想把办法把语义接口的元素转换为编程接口元素。
比如用ASSERT或其他技术。
一个原则是一个接口中任何无法通过编译器强制实施的部分就是一个可能被无用的部分。
6.谨防在修改时破坏接口的抽象(详见Chapter24 重构)
7.不要添加与接口不一致的公用成员
8.同时考虑抽象性和内聚性
关注类的接口所表现的抽象,比关注类的内聚性更有助于深入理解类的设计。
二. 良好的封装
抽象可以通过提供一个可以让你忽略实现细节的模型管理复杂度,而封装则强制阻止你看到细节。
一些指导建议:
1.尽可能地限制类和成员的可访问性
让可访问性尽可能低是促成封装的原则之一
2.不要公开成员数据
3.避免把私用的实现细节放入类的接口中
建议把类的接口与类的实现隔离开,并在的类的声明中包含一个指针,让该指针指向类的实现,但不能包含任何其他实现的细节。
在类接口的私用部分不应包含实现细节的线索。
4.不要对类的使用者做出任何假设
不应该对接口会如何使用或者不会被如何使用做出任何假设--除非接口中有过说明。
5.避免使用友元类
在某些场合,如State模式中,按照正确的方式使用友元类会有助于管理复杂度。但在一般情况下友元类会破坏封装。
6.不要因为一个子程序里仅使用公用子程序,就把它归入公开接口
应该更多的考虑到:如果把这个子程序暴露给外界,接口所展示的抽象是否还是一致的。
7.让阅读代码比编写代码更方便
不要为了让编写代码更方便而降低代码的可读性
8.要格外警惕从语义上破坏封装性
每当你发现自己是通过查看的类的内部实现来得知该如何使用这个类的时候,你就不是针对接口编程了,而是在透过接口针对内部实现编程了。
如果你透过接口编程,封装性就被破坏了。而一旦封装性遭到破坏,抽象能力也就快遭殃了。
9.留意过于紧密的耦合关系
根据耦合(两个类之间关联的紧密程度)得出以下指导建议:
(a)尽可能地限制类和成员的可访问性
(b)避免友元类,因为它们之间是紧密耦合的
(c)在基类中把数据声明为private而不是protected,以降低派生类和基类之间耦合的程度
(d)避免在类的公开接口中暴露成员数据
(e)要对从语义上破坏封装性保持警惕
(f)觉得"Demeter"法则
6.3 有关设计和实现的问题
一.包含(”有一个......“的关系)
包含表示一个类含有一个基本数据元素或对象,包含才是面向对象编程中的主力技术。
1.通过包含来实现”有一个/has a“的关系
2.在万不得已时通过private继承来实现”有一个“的关系
在一个对象无法把另一个对象当作成员来实现包含关系时,一些专家建议此时可采用private继承自所要包含的对象的方法。
这么做的主要原因是要让外层的包含类能够访问内层被包含类的protected成员函数和数据成员。
3.警惕有超过7个数据成员的类
如果超过7个,请考虑要不要吧它分解为几个更小的类
如果数据成员都是整形或字符串这种简单数据类型,你可以按7加减2的上限来考虑;
如果数据成员都是复杂对象的话,就应该按7加减2的下限来考虑了。
二.继承(”是一个......“的关系)
继承的概念是说一个类是另一个类的一种特化,继承的目的在于,通过“定义能为两个或更多派生类提供共有元素的基类”的方式写出更精简的代码。
注意事项:
1. 用public继承来实现“是一个......”的关系
2.要么使用继承并进行详细说明,要么就不要用它
3.遵循Liskov替换规则
4.确保只继承需要继承的部分:如果只是想使用一个类的实现而不是接口,那么久应该采用包含的方式,而不该用继承
5.不要“覆盖”一个不可覆盖的成员函数:建议派生类中的成员函数不要与基类中不可覆盖的成员函数的重名
6.把共用的接口、数据和操作放到继承树中尽可能高的位置
7.只有一个实例的类是值得怀疑的,单例模式除外
8.只有一个派生类的基类也值得怀疑
9.派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑:说明基类设计有错误。
10.避免让继承体系过深:继承层次尽量低些,请确保你在继承避免代码重复并使复杂度最小
11.尽量使用多态,避免大量的类型检查。
12.让所有数据都是private(而非protected)
13.多重继承的用途:主要是定义“混合体”,也就是一些给对象增加一组属性的简单类。
三.成员函数和数据成员
1.让类中子程序的数量尽可能少
2.禁止隐式地产生你不需要的成员函数和运算符:这种情况下,完全可以通过把构造函数、赋值运算符或其他成员函数或运算符定义为private,从而禁止代码调用它们。
3.减少类所调用的不同子程序的数量
4.对其他类的子程序的间接调用要尽可能少:
研究人员总结为“Demeter法则”,基本上就是说A对象可以任意调用它自己的所有子程序
如果A对象创建了B对象,它也可以调用B程序的任何(公用)子程序,但它应该避免再调用由B对象所提供的对象中的子程序
四.构造函数
1.如果可能,应该在所有的构造函数中初始化所有的数据成员
2.用私用构造函数来强制实现单件属性(singleton property)
3.优先采用深层副本(deep copy),除非论证可行,才采用浅层复本,实现浅层复本的动机一般是为了改善性能。
一种可行的方案是:在面临选择时优先实现深拷贝,除非可论证浅拷贝更好。
实现浅拷贝除了要用到两种方法都需要的代码之外,还要增加许多代码用于引用计数、确保安全地复制对象、安全地比较对象以及安全地删除对象等。
6.4 创建类的原因
1.为现实世界建模
2.为抽象的对象建模
3.减低复杂度
4.隔离复杂度
5.隐藏实现细节
6.限制变动的影响范围
7.隐藏全局数据
8.让参数传递更顺畅
9.建立中心控制点
10.让代码更容易重用
11.为程序族做计划
12.把相关操作包装到一起
13.实现某种特定的重构
应该避免的类
1.避免创建万能类
2.消除无关紧要的类
3.避免用动词命名的类