第6章 可以工作的类
6.1 抽象数据类型:一些数据和对这些数据进行的操作的集合
使用ADT的益处:
可以隐藏实现细节;
改动不会影响到整个程序;
让接口能提供更多信息;
更容易提高性能;
让程序的正确性更显而易见;
程序更具自我说明性;
无须在程序内到处传递数据;
可以像在现实世界中那样操作实体,不用在底层实现上操作它。
6.2 良好的类接口: 应能提供一组明显相关的子程序
1.好的抽象
指导建议:
类的接口应该展现一致的抽象层次:
在添加接口时问问“新增接口与现有接口所提供的抽象一致吗?”;
(理解类的抽象是什么,提供成对的服务);
把不相关的信息转移到其他类中;
尽可能让接口可编程,而不是表达语义:
可编程的部分由接口中的数据和其他属性构成,编译器在编译时会检查错误;而语义部分则由“本接口会被如何使用”的假定组成,应该使接口尽可能不依赖于这些说明。
谨防在修改时破坏接口的抽象;
同时考虑抽象性和内聚性。
2.好的封装
尽可能地险坠类和成员的可访问性:采用最严格且可行的访问级别,最好地保护接口抽象的完整性;
不要公开暴露成员数据;
避免把私用的实现细节放入类的接口中;
不要对类的使用者作出任何假设;
避免使用友元类;
警惕从语义上破坏封装性:“当发现是通过查看类的内部实现来得知如何使用这个类的时候,就不是在针对接口编程了”;
(当根据类的接口文档无法得知如何使用一个类的话,就说么类的设计出了问题)
留意过于紧密的耦合关系;
6.3 有关设计和实现的问题
1.包含:has-a关系
“7+/-2”原则考虑数据成员个数;
万不得已考虑使用private继承实现has-a关系
2.继承:is-a关系
用public继承实现is-a关系;
要么使用继承并进行详细说明,要么就不用它;
遵循Liskov替换原则(LSP):子类必须能通过父类接口而被使用,且使用者无须了解两者之间的差异;
确保只继承需要继承的部分:继承抽象接口、继承接口及其默认实现但子类可override、继承接口及其实现不可覆盖(如果只是想使用一个类的实现而不是接口,则应该采用包含);
子类的成员函数不要与父类的不可覆盖函数同名;
把共用的接口数据及操作放到继承树中尽可能高的位置;
只有一个实例/子类的类是值得怀疑的;
子类覆盖了某个子程序但没有任何操作是值得怀疑的(不会飞的企鹅也是鸟类fly(){//什么也不做});
避免让继承体系过深;
尽量使用多态,避免大量的类型检查;
让所有数据都是private:如果子类真的需要访问父类的属性,则应该提供protected访问器函数;
*继承与包含的使用规则:
如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象;
如果多个类共享行为而非数据,应该从共同的父类继承,并在父类定义共用子程序;
如果多个类既共享数据又共享行为,应该从共同的父类继承,并在父类定义共用数据和子程序;
如果想从父类控制接口使用继承,想自己控制接口使用包含。
3. 成员函数和数据成员
让类中子程序的数量尽可能少;
禁止隐式地产生不需要的成员函数和运算符;
减少类所调用的不同子程序的数量;
对其他类的子程序的间接调用要尽可能的少(迪米特法则);
4. 构造函数
尽量在构造函数中初始化所有的数据成员;
用private构造函数实现单例;
优先使用深拷贝;
6.4 创建类的理由
对现实世界中对象的建模
对抽象对象建模
降低、隔离复杂度
隐藏实现细节
限制变化所影响到的范围
隐藏全局数据
让参数传递更流畅
创建中心控制点
使代码易于重用
为程序族做计划:预计到某个程序会被修改,则将可能修改的部分放到单独的类中。
把相关操作放到一起
实现特定的重构
*应该避免的类:
万能类
只有数据没有行为的类
只有行为没有数据的类