类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。类也可以只是由一组子程序构成的集合,这些子程序提供一组内聚的服务,哪怕其中并未涉及公用的数据。成为高效程序员的一个关键就在于,当你开发程序任一部分的代码时,都能安全地忽视程序中尽可能多的其余部分。而类就是实现这以目标的首要工具。
类的基础:抽象数据类型(ADT)
抽象数据类型是指一些数据以及对这些数据所进行的操作的集合。抽象数据类型可以像在现实世界中一样的操作实体,而不必在低层的实现上摆弄实体。
使用ADT的益处:
1.可以隐藏细节
2.改动不会影响到整个程序
3.让接口能提供更多信息
4.容易提高性能
5.让程序的正确性更显而易见
6.程序更具有自我说明性
7.无须在程序内到处传递数据
8.你可以像在世界中那样操作实体,而不用在底层实现上操作它
ADT和类:
良好的类接口:
创建高质量的类,最重要的一步就是创建一个好的接口。这也包括了一个可以通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后。
好的抽象:
抽象是一种以简化的形式来看待复杂操作的能力。类的接口为隐藏在其后的具体实现提供了一种抽象。类的接口应能提供一组明显相关的子程序。如果类的接口不能展现出一种一致的抽象,因此它的内聚性就很弱。应该把这些子程序重新组织到几个职能更专一的类里去,在这些类的接口中提供更好的抽象。
对类的抽象进行评估的方法是基于类所具有的公用子程序所构成的集合——即类的接口。即使类的整体表现出一种良好的抽象,类内部的子程序也就未必就能个个表现出良好的抽象,也同样要把它们设计得可以表现出很好的抽象。
关于一些创建类的抽象接口的指导建议:
1.类的接口应该展现出一致的抽象层次:对于类来讲,类可以看做一种用来实现抽象数据类型的机制。每一个类应该实现一个ADT,并且仅实现这个ADT。如果发现某个类实现了不止一个ADT,或者不能确定究竟它实现了何种ADT,此时就应该把这个类重新组织为一个或多个定义更加明确的ADT。如果定义混乱,在修改程序时,混杂的抽象层次会让程序越来越难以理解,整个程序也逐渐变得无法维护。
2.一定要理解类所实现的抽象是什么:即这个类是处理什么事务的,或者描述什么事务的,如果认识不清写成的类也会混乱。
3.提供成对的服务:大多数操作都有和其相应的、相等的以及相反的操作。但是不要盲目地创建相反操作,但是一定要考虑,看看是否需要。
4.把不相关的信息转移到其他类中:有时某个类中一半子程序使用着该类的一半数据,而另一半子程序则使用另一半数据。而却把他们写在了一起,此时应该将他们分开。
5.尽可能让接口可编程,而不是表达语义:每个接口都有一个可编程的部分和一个语义组成部分。可编程的部分由接口中的数据类型和其他属性构成,编译器能强制性地要求他们。而语义部分则由“本接口将会被怎么使用”的假定组成,而这些事无法通过编译器来强制实施的。
6.谨防在修改时破坏接口的抽象
7.不要添加与接口抽象不一致的公用成员:每次向类接口中添加子程序时,要问问:“这个子程序与现有接口所提供的抽象一致吗?”如果发现不一致,就要换另一种方法来修改,以便能够保持抽象的完整性。
8.同时考虑抽象性和内聚性:抽象性和内聚性这两个概念之间的关系非常紧密——一个呈现出良好的抽象的接口通常也有很高的内聚性。而具有很强内聚性的类往往也会呈现为很好的抽象,尽管这种关系并不如前者那么强。
良好的封装:
封装是一个比抽象更强的概念。抽象通过提供一个可以让你忽略现实细节的模型来管理复杂度,而封装性则强制阻止看到细节.
1.尽可能地限制类和成员的可访问性:让可访问性尽可能低是促成封装的原则之一。
2.不要公开暴露成员数据:暴露成员数据会破坏封装性,从而限制对这个抽象的控制力。
3.避免把私用的实现细节放入类的接口中:做到真正的封装以后,这样是根本看不到任何实现细节的。
4.不要对类的使用者做出任何假设:类的设计和实现应该符合在类的接口中所隐含的契约。
5.避免使用友元类:在一般情况下友元类会破坏封装性,因为它让你在同一时刻需要考虑更多的代码量,从而增加了复杂度。
6.不要因为一个子程序里仅使用公用子程序,就把它归入公开接口:何时将接口公开应该考虑一下这个接口所展示的抽象是否一致的。
7.让阅读代码比编写代码更方便:为了让编写代码更方便而降低代码的可读性是非常不经济的。尤其是在创建类的接口时,即时某个子程序与接口的抽象很不相配,有时人们也往往把这个子程序加到接口里,从而让这在开发的这个类的某处调用代码能更方便地使用它。然而,这段子程序的添加正是代码走下坡路的开始,所以还是不要走出这一步为好。
8.要格外警惕从语义上破坏封装性:每当发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是在针对接口编程了,而是在透过接口针对内部实现编程了。如果透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就快遭殃了。(不要看内部实现细节,应该直接调用抽象的实现函数来处理数据)
9.留意过于紧密的耦合关系:”耦合“是指两个类之间关联的紧密程度。通常这种关系越松越好,通过以下方式可以做到:尽可能地限制类和成员的可访问性;避免友元类;在基类中吧数据声明为private而不是protected,以降低派生类和基类之间的耦合程度;避免在类的公开接口中暴露成员数据;要对从语义上破坏封装性保持警惕;
有关设计和实现的问题:
包含(“有一个……”的关系),他表示一个类含有一个基本数据元素或对象。
1.通过包含来实现“有一个/has a” 的关系。
2.在万不得已时通过private继承来实现“有一个”的关系
3.警惕有超过7个数据成员的类
继承(“是一个……”关系)继承的概念是说一个类是另一个类的一种特化。继承的目的在于,通过“定义能为两个或更多个派生类提供共有元素的基类“的方式写出来精简的代码。
使用继承的时候注意:
1.对于每一个成员函数而言,它应该对派生类可见吗?他应该有默认的实现吗?这一默认的实现能被覆盖吗?
2.对于每一个数据成员而言,他应该对派生类可见吗?
如何考虑上述问题?
1.用public继承来实现”是一个……“的关系:如果派生类不准备完全遵守有基类定义的同一个接口契约,继承就不是正确的实现,请考虑用包含的方式,或者对继承体系上层做修改。
2.要么使用阶乘并进行详细描述,要么就不用它
3.遵循Liskov替换原则:除非派生类真的”是一个“更特殊的基类,否则不应该从基类继承,派生类必须能通过基类的接口而被使用,且使用者无需了解两者之间的差异。
4.确保只继承需要继承的部分:派生类可以继承成员函数的接口和/或实现。
继承而来的子程序有三种情况:
a.抽象且可覆盖的子程序是指派生类只继承了该子程序的程序的接口,但不继承其实现。
b.可覆盖的子程序是指派生类继承了该子程序的接口及其默认实现,并且可以覆盖该默认实现。
c.不可覆盖的子程序是指派生类继承了该子程序的接口及其默认实现,但不能覆盖该默认实现。
5.不要”覆盖“一个不可覆盖的成员函数:如果一个成员函数在基类中是私用的话,其派生类可以创建一个同名的成员函数。对于阅读派生类代码的人员来说,这个函数是令人困惑的,因为他看上去似乎应该是多态的,但事实上却非如此,只是同名而已。(派生类中的成员函数不要与基类中不可覆盖的成员函数的重名)
6.把公用的接口、数据及操作放到继承树中尽可能高的位置:接口、数据和操作在继承体系中的位置越高,派生类使用它们的时候就越容易。
7.只有一个实例的类是值得怀疑的
8.只有一个派生类的基类也值得怀疑:为未来要做的工作着手进行准备的最好方法,并不是去创建阶层额外的、”没准哪天能用的上的“基类,而是让眼下的工作成果尽可能地清晰、简单、直截了当。
9.派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑:这表明基类设计的有问题,应该从基类着手修改它。
10.避免让继承体系过深
11.尽量使用多态,避免大量的类型检查:频繁重复出现的case语句有时是在暗示,采用继承可能是种更好的设计选择——尽管并不总是如此。
12.让所有数据都是private(而非protected)”继承会破坏封装“
继承规则:
何时使用继承,何时又该使用包含:
1.多个类共享数据而非行为,应该创建这些类可以包含的公用对象。
2.多个类共享行为而非数据,应该让他们共同的继承而来,并在基类里定义公用的子程序。
3.多个类既共享数据也共享行为,应该让他们从一个共同的基类继承而来,并在基类里定义共同的数据和子程序。
4.当想由基类控制接口时,使用继承;想要自己控制接口时,使用包含。
成员函数和数据成员:
1.让类中子程序的数量尽可能少
2.禁止隐式地产生你不需要的成员函数和运算符
3.减少类所调用的不同子程序的数量
4.对其他类的子程序的见解调用要尽可能少
5.一般来说,应尽量减小类和类之间的相互合作的范围。
构造函数:
1.如果可能,应该在所有的构造函数中初始化所有的数据成员。
2.用私用构造函数来强制实现单件属性
3.优先采用深层复本,除非论证可行,才采用浅层复本
在面临深层拷贝还是浅层拷贝时,一种合理的方式是优先实现深层拷贝。
创建类的原因:
1.为显示世界中的对象建模
2.为抽象的对象建模
3.降低复杂度
4.隔离复杂度
5.隐藏实现细节
6.限制变动的影响范围
7.隐藏全局数据
8.让参数传递更顺畅
9.建立中心控制点
10.让代码更易于重用
11.为程序族做计划
应该避免的类:
1.创建万能类
2.消除无关紧要的类
3.避免用动词命名的类