类的基础:抽象数据类型。
抽象数据类型(ADT)——是指一些数据以及对这些数据进行操作的集合。
使用抽象数据类型的益处有下列几点。
1、 可以隐藏实现细节。
2、 改动不会影响到整个程序。
3、 让接口提供更多的信息。
4、 更容易提高性能。
5、 让程序的正确性更显而易见。
6、 让程序更具有自我说明性。
7、 无须在程序内部到处传递数据。
8、 你可以像在现实世界中那样操作实体,而不用在底层实现细节上操作他。
在使用面向对象编程过程中,使用(ADT)更加有助于代码的读取理解(一个好的代码,对它的阅读理解应该比编写它更加容易)。使用各种子程序来在较高层次对底层数据进行操作。有助于对功能进行增减和其他改动,并且不会影响其他部分的程序运行。使代码更加简洁,明了。
良好的类接口
好的抽象往往是优秀类的第一步。
1、 类的接口应展现一致的抽象层次。每一个类应该实现一个ADT,并且仅实现这个ADT。如果你发现一个类不止实现了一个ADT,或者不能确定究竟它实现了何种ADT,你就应该把这个类重新组织为一个或多个定义更加明确的ADT。(当你发现你的某一个数据或者几乎所有的数据之间的关系都不是太深或太明显,那么你就需要考虑将这个数据或多个数据重新分类)。例如:我需要定义一个车的类,我在这个车里面加入门,轮子,发动机,都是合理且必要的成员,但是我加入一个驾驶员呢?他好像与车有点儿关系,但是没了他,车依然可以叫做车,但是没了门,轮子之类的成员就不是车了。
2、 一定要理解类实现的抽象是什么。(需要满足的条件是什么,例如:我想要一个车的类,把相关车的成员放进来。)
3、 提供成对的服务。大多数接口都有相应的,相等的,相反的操作。(如灯,让你想设置一个开的程序,就应该还加入一个关的程序。)
4、 把不相关的信息转移到其他类中
5、 尽量让接口可编程,而不是表达语义。编程部分有接口中的数据类型和其他属性构成,编译器强制要求他们。语义部分则由“本接口将会怎样被使用”的假定组成,编译器无法强制检查,比如,RoutineA应该在RoutineB调用前调用,否则会引起崩溃;调用RoutineA前需要设置全局变量。等。
6、 谨防在修改时破坏接口的抽象。
7、 不要添加与接口抽象不一致的公用成员,每次向类中的接口中添加子程序时,问问“这个子程序与现有接口提供的抽象一致吗?,如果不一致,该如何解决?
8、 同时考虑内聚性和抽象性。
关注类的接口所表现出来的抽象,比关注类的内聚性更有助于深入的理解类的设计。
良好的封装
1、 尽可能限制类和成员的可访问性。让可访问性尽可能的低是促成封装的原则之一。如果无法确定子程序的访问级别(公用,私用,受保护),经验之举是采用最严格且可行的访问级别。更好的建议:采用那种访问级别能够最好的保护接口抽象的完整性?
2、 不要公开暴露成员数据。
3、 避免把私用的实现细节放入类的接口中。不要在private中暴露内部细节。
4、 不要对类的使用做出任何的假设。即尽量让接口可编程,而不是表达语义。
5、 避免使用友元类。State模式中按照正确的方式使用友元类有助于管理复杂度。但一般友元会破坏封装。他让你在同一时刻考虑更多的代码。
6、 不要因为一个子程序仅使用公用子程序,就把它归入公开接口。
7、 让阅读代码比编写代码更方便。
8、 要格外警惕从语义上破坏封装性。语义上对封装性的破坏很大:它们让调用代码不是依赖于类的公开接口,而是依赖于类的私用实现。每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是针对接口编程了,而是透过接口针对内部实现编程了。如果你透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就被破坏了。
9、 留意过于紧密的耦合关系。建议:1)在基类中把数据声明为private,而不是protect,以降低派生类和基类的耦合关系。避免在公开接口中暴露成员数据。要对从语义上破坏封装性保持警惕。紧密的耦合性总是发生在抽象不严谨或封装遭到破坏的时候。
封装是比抽象更强的一个概念。抽象通过让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节。
有关设计和实现的问题
包含(“有一个 ”的关系)
1、 包含表示有一个的关系。
2、 在万不得已是通过private继承来实现有一个的关系。
3、 警惕有超过约7个数据成员的类。如果超过7(+ -)2个成员,考虑把这个类分解成更小的类。简单数据成员上限9个,复杂对象5个。主要的原理就是分而治之,降低单个复杂度。
继承(“是一个 ”的关系)
使用继承是考虑:
1、 对以每一个成员函数,它应该对派生类可见吗?它应该有默认的实现吗?这一默认实现能被覆盖吗?
2、 对每个成员数据:是否对派生类可见?
包含表示一个类含有一个基本数据元素或对象,继承比包含复杂的多,不是因为继承比包含更好。包含才是面向对象中的主力技术。
继承往往会让你和程序员的首要技术使命(管理复杂度)背道而驰。从控制复杂度的角度说,你应该对继承持有非常歧视的态度。
总结:何时可以使用继承,何时使用包含:
1、 如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。
2、 如果多个类共享行为而非数据,应该使用继承。
3、 如果多个类即共享数据,又共享行为,应该让它们从一个共同的基类继承而来,并在基类中定义共用的数据和子程序。
4、 当你先通过基类控制接口时,使用继承。当你想自己控制接口是,使用包含。
创建类的理由
1、对现实世界的对象建模
2、对抽象对象建模
3、降低复杂度
4、隔离复杂度
5、隐藏实现细节
6、限制变化所影响的范围
7、隐藏全局数据
8、让参数传递更顺畅
9、创建中心控制点
10、创建中心控制点
11、让代码更易于重用
12、为程序族做计划
13、把相关操作放到一起
14、实现特定的重构
Key Points
1、类的接口应该提供一致的抽象。
2、类的旧口应隐藏一些信息——如某个系统接口、某项设计决策。或一些实现细节。
3、包含往往比继承更可取——除非你要对“是一个”的关系建模。
4、继承是一种有用的工具,但它却会增加复杂度,这有违于软件的首要技术使命——管理复杂度。
5、类是管理复杂度的首要选工具。要在设计类时给予足够的关注,才能实现这一目标。