code2读书笔记-第六章 可以工作的类
类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。
6.1 类的基础:抽象数据类型
抽向数据类型(ADT,abstract data type)是指一些数据以及对这些数据所进行的操作的集合
需要用到ADT的例子
假如你正在写一个程序,它能用不同的字体,字号和文字属性来控制显示在屏幕上的文本。程序的一部分功能是控制文本的字体。如果使用一个ADT,你就能有捆绑在相关数据上的一组操作字体的子程序。这些子程序和数据集合为一体,就是一个ADT。
如果不使用ADT,你就只能使用一种拼凑的方法来操作字体
使用ADT的益处
- 可以隐藏实现细节
- 改动不会影响到整个程序
- 让接口能够提供更多信息
- 更容易提高性能
- 让程序正确性显而易见
- 程序更具有自明性
- 无需在程序内到处传递数据
- 可以像在现实世界中那样操作实体,而不用在底层实现上操作
更多ADT示例
- 把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据。(尽可能的选择最高的抽象层次)
- 把像文件这样的常用对象当成ADT
- 简单的事物也可以当做ADT
- 不要让ADT依赖于其存储介质(尽量让类和访问器子程序的名字与存储数据的方式无关,并只提及抽象数据类型本身)
6.2 良好的类接口
创建高质量的类,第一步也可能是最重要的一步,就是创建一个好的接口。
好的抽象
抽象是一种以简化的形式来看待复杂操作的能力。
如果类的接口不能展现出一种一致的抽象,它的内聚性就很弱。应该将它的子程序重新组织到几个职能更专一的类里去,在这些类的接口中提供更好的抽象。
创建累的抽象接口的指导建议
-
类的接口应该展现一致的抽象层次
每个类应该实现一个ADT,并且仅实现这个ADT。 -
一定要理解类所实现的抽象是什么
一些类非常的像,你必须非常仔细地理解类的接口应该捕捉的抽象到底是哪一个。 -
提供成对的服务
大多数操作都有和其相对应的,相等的以及相反的操作。在设计一个类的时候,要检查每一个公用子程序,决定是否需要另一个与其互补的操作。不要盲目创建相反操作。 -
把不相关的信息转移到其他类中
某个类中一半子程序使用该类的一半数据,而另一半子程序则使用另一半数据。这时就需要考虑拆分。 -
尽可能让接口可编程,而不是表达语义
每个接口都由一个可编程的部分和一个语义部分组成。可编程的部分由接口中的数据类型和其它属性构成,编译器能够强制性要求。而语义部分是假定的,无法通过编译器强制实施的。要想办法把语义接口元素转化为编程接口元素 - 谨防在修改时破坏接口的抽象
- 不要添加与接口抽象不一致的公用成员
- 同时考虑抽象性和内聚性
一个呈现出很好抽象的类接口通常也有很高的内聚性。而具有很强内聚性的类往往也会呈现为很好的抽象。
良好的封装
封装是一个比抽象更强的概念。抽象通过提供一个可以让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节。
这两个概念之所以相关,是因为没有封装时,抽象往往很容易被打破。要么就是封装与抽象两者皆有,要么就是两者皆失。
- 尽可能地限制类和成员的可访问性:让可访问性尽可能低是促成封装的原则之一
- 不要公开暴露成员数据
- 避免把私用的实现细节放类的接口中
- 不要对类的使用者做出任何假设:类的设计和实现应该符合在类的接口中所隐含的契约。它不应该对接口会被如何使用或不会被如何使用做出任何假设(在组件开发中,经常会过多的假设使用者的调用方式或者个性化需求,其实都是可以通过拓展或者门面模式进行处理)
- 避免使用友元类
- 不要因为一个子程序仅适用公用子程序,就把它归入公开接口
- 让阅读代码比编写代码更方便:为了让编写代码更方便而降低代码的可读性是非常不经济的
- 要格外警惕从语义上破坏封装性:让调用方法代码不是依赖于类的公开接口,而是依赖于类的私用实现。这就不是针对接口编程,而是在透过接口针对内部实现编程了。封装性就被破坏。
- 留意过于紧密的耦合关系
6.3 有关设计和实现的问题
给类定义合理的接口,对于创建高质量程序起到了关键作用。然而类内部的设计和实现也同样重要。这一节就来论述关于包含、继承、成员函数和数据成员、类之间的耦合性、构造函数、值对象与引用对象等问题。
包含(“有一个”的关系)
与包含相比,继承的论述要多得多,这是因为继承需要更多技巧,而且更容易出错,而不是因为继承要比包含更好。包含才是面向对象编程中的主力技术。
- 通过包含来实现“有一个”的关系
- 在万不得已时通过private继承来实现“有一个”的关系
- 警惕有超过约七个数据成员的类
继承(“是一个”的关系)
继承的概念是说一个类是另一个类的一种特化。继承的目的在于,通过“定义能为两个或者更多派生类提供共有元素的基类”的方法写出更精简的代码。
- 用public继承来实现“是一个”的关系
- 要么使用继承并进行详细说明,要么就不要用它
- 遵循liskov(里氏)替换原则:派生类必须能够通过基类的接口而被使用,且使用者无须了解两者之间的差异(如果程序员必须要不断思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了)
- 确保只继承需要继承的部分:继承而来的子程序有以下三种基本情况
- 抽象且可覆盖的子程序是指派生类只继承了该子程序的接口,但不继承其实现
- 可覆盖的子程序是指派生类继承了该子程序的接口以及其默认实现,并且可以覆该默认实现
- 不可覆盖的子程序是指该派生类继承了该子程序的接口及默认实现,但不能覆盖该默认实现。
针对每个子程序仔细考虑你所希望的继承方式。仅仅是因为要继承接口所以才继承实现,或仅仅是因为要继承实现所以才继承接口,都要注意。如果只想使用一个类的实现而不是接口,那么就应该采用包含方式,而不该用继承。
- 不要“覆盖”一个不可覆盖的成员函数:派生类中的成员函数不要与基类中不可覆盖的成员函数重名。
- 把共用的接口、数据及操作放到继承树中尽可能高的位置
- 只有一个实例的类是值得怀疑的:这可能表明设计中把对象和类混为一谈了
- 只有一个派生类的基类也是值得怀疑的:这可能又是一个“提前设计”。为未来的工作着手准备,不是创建几层额外的基类,而是让眼下的工作成果尽可能地清晰、简单、直接了当。
- 派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑
- 避免让继承体系过深:继承层数限制在最多6层之内,大多数保持在2到3层。过深的继承层次增加了复杂度,这恰恰与继承所应解决的问题相反
- 尽量使用多态,避免大量的类型检查
- 让所有数据都是private(而非protected)
为什么有这么多关于继承的规则
继承往往让你和程序员的首要技术使用(管理复杂度)背道而驰。下面总结一下何时可以使用继承,何时又该使用包含:
- 如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。
- 如果多个类共享行为而非数据,应该让它们从共同的基类继承而来,并在基类定义共用的子程序。
- 如果多个类既共享数据也共享行为,应该让它们从一个共同的基类继承而来,并在基类里定义共用的数据和子程序。
- 当你想由基类控制接口时,使用继承;当你想自己控制接口时,使用包含。
成员函数和数据成员
下面就有效地实现成员函数和数据成员给出一些指导建议:
- 让类中子程序的数量尽可能少
- 禁止隐式地产生你不需要的成员函数和运算符
- 减少类所调用的不同子程序的数量:类所用到的其它类的数量越高,其出错率也会越高。这个概念有时称为“扇入”
- 对其他类的子程序的间接调用要尽可能少:基本上就是说A对象可以任意调用它自己的所有子程序。如果A对象创建了一个B对象,它也可以调用B对象的任何(公用)子程序,但是它应该避免再调用由B对象所提供的对象中的子程序。
- 一般来说,应尽量减小类和类之间相互合作的范围
构造函数
针对构造函数的这些建议对于不同的语言(c++、java和VB)都差不多。但对于析构函数而言则略有不同。
- 如果可能,应该在所有的构造函数中初始化所有数据成员
- 用私用构造函数来强制实现单件属性
- 优先采用深层拷贝,除非论证可行,才采用浅层拷贝
为了不确定的性能提高而增加复杂度是不妥的,因此面对拷贝时,优先深拷贝。实现浅拷贝除了要用到两种方法到需要的代码外,还需要增加很多代码用于引用计数、确保安全地复制对象、安全比较对象以及安全地删除对象等
6.4 创建类的原因
实际上创建类的理由远不止要为世界中的物体建模。下面列出一些创建类的原因:
- 为现实世界中的对象建模
- 为抽象的对象建模:所谓抽象对象并不是一个现实世界中的具体对象,但它却能为另一些具体的对象提供一种抽象。
- 降低复杂度
- 隔离复杂度
- 隐藏实现细节
- 限制变动的影响范围
- 隐藏全局数据
- 让参数传递更顺畅
- 建立中心控制点
- 让代码更易于重用
- 为程序族做计划:仔细考虑整个程序族(family of programs)的可能情况,而不单是考虑单一程序的可能情况。这是一种用于预先应对各种变化的强有力的启发方法。
- 把相关操作包装到一起:类也是把相关操作组合在一起的一种方法。除此之外,根据语言不同,还可以使用包,命名空间或者头文件方法。
- 实现某种特定的重构
应该避免的类
- 避免创建万能类
- 消除无关紧要的类
- 避免用动词命名的类:没有数据的类往往不是一个真正的类
要点
- 类的接口应提供一致的抽象。很多问题都是由于违背该原则而引起的。
- 类的接口应隐藏一些信息——如某个系统接口、某项设计决策、或一些实现细节。
- 包含往往比继承更为可取–除非你要对“是一个/is a”的关系建模。
- 继承是一种有用的工具,但它却会增加复杂度,这有违于软件的首要技术使命–管理复杂度。
- 类是管理复杂度的首选工具。要在设计类时给予足够的关注,才能实现这一目标。