引言
- 良好的程序布局和配置管理可以减少设计中的偶然复杂性
- 良好的类的定义和对类本质的理解可以管理设计中的本质复杂性
- 什么是良好的类的定义
类的基础
- 类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的明确定义的职责
- 良好的类定义把类当做抽象数据类型(ADT,abstract data type)是指一些数据以及对这些数据进行操作的集合(表面的)
- 这些操作既向程序的其余部分描述了这些数据是什么样的,也允许程序的其余部分改变这些数据(关联)
抽象数据类型
- 抽象数据类型可以让你像在现实世界中一样操作实体,而不必在底层的实现上操作
- 抽象数据类型的本质是抽象和封装
使用ADT的好处
- 可以隐藏实现细节(封装模块)
- 改动不会影响到整个程序(隔离性)
- 让接口能提供更多的信息(抽象)
- 更容易提高性能(重用性)
- 让程序的正确性更加显而易见(重用性,更效率)
- 程序更具有自我说明性(模块化,功能区分明显)
- 无须在程序内到处传递数据(隔离,封装)
- 你可以像在现实世界中那样操作尸体,而不用再底层实现上操作它(封装,面向对象)
在使用面向对象编程过程中,使用(ADT)更加有助于代码的读取理解(一个好的代码,对它的阅读理解应该比编写它更加容易)。使用各种子程序来在较高层次对底层数据进行操作。有助于对功能进行增减和其他改动,并且不会影响其他部分的程序运行。使代码更加简洁,明了。
良好的类接口
好的抽象往往是优秀类的第一步。
- 类的接口应展现一致的抽象层次。每一个类应该实现一个ADT,并且仅实现这个ADT。如果你发现一个类不止实现了一个ADT,或者不能确定究竟它实现了何种ADT,你就应该把这个类重新组织为一个或多个定义更加明确的ADT。(当你发现你的某一个数据或者几乎所有的数据之间的关系都不是太深或太明显,那么你就需要考虑将这个数据或多个数据重新分类)。例如:我需要定义一个车的类,我在这个车里面加入门,轮子,发动机,都是合理且必要的成员,但是我加入一个驾驶员呢?他好像与车有点儿关系,但是没了他,车依然可以叫做车,但是没了门,轮子之类的成员就不是车了。
- 一定要理解类实现的抽象是什么。(需要满足的条件是什么,例如:我想要一个车的类,把相关车的成员放进来。)
- 提供成对的服务。大多数接口都有相应的,相等的,相反的操作。(如灯,让你想设置一个开的程序,就应该还加入一个关的程序。)
- 把不相关的信息转移到其他类中
- 尽量让接口可编程,而不是表达语义。编程部分有接口中的数据类型和其他属性构成,编译器强制要求他们。语义部分则由“本接口将会怎样被使用”的假定组成,编译器无法强制检查,比如,RoutineA应该在RoutineB调用前调用,否则会引起崩溃;调用RoutineA前需要设置全局变量。等。
- 谨防在修改时破坏接口的抽象。
- 不要添加与接口抽象不一致的公用成员,每次向类中的接口中添加子程序时,问问“这个子程序与现有接口提供的抽象一致吗?,如果不一致,该如何解决?
- 同时考虑内聚性和抽象性。
关注类的接口所表现出来的抽象,比关注类的内聚性更有助于深入的理解类的设计。
良好的封装
- 尽可能限制类和成员的可访问性。让可访问性尽可能的低是促成封装的原则之一。如果无法确定子程序的访问级别(公用,私用,受保护),经验之举是采用最严格且可行的访问级别。更好的建议:采用那种访问级别能够最好的保护接口抽象的完整性?
- 不要公开暴露成员数据。
- 避免把私用的实现细节放入类的接口中。不要在private中暴露内部细节。
- 不要对类的使用做出任何的假设。即尽量让接口可编程,而不是表达语义。
- 避免使用友元类。State模式中按照正确的方式使用友元类有助于管理复杂度。但一般友元会破坏封装。他让你在同一时刻考虑更多的代码。
- 不要因为一个子程序仅使用公用子程序,就把它归入公开接口。
- 让阅读代码比编写代码更方便。
- 要格外警惕从语义上破坏封装性。语义上对封装性的破坏很大:它们让调用代码不是依赖于类的公开接口,而是依赖于类的私用实现。每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是针对接口编程了,而是透过接口针对内部实现编程了。如果你透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就被破坏了。
- 留意过于紧密的耦合关系。建议:1)在基类中把数据声明为private,而不是protect,以降低派生类和基类的耦合关系。避免在公开接口中暴露成员数据。要对从语义上破坏封装性保持警惕。紧密的耦合性总是发生在抽象不严谨或封装遭到破坏的时候。
封装是比抽象更强的一个概念。抽象通过让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节。
创建类的理由
- 对现实世界的对象建模
- 对抽象对象建模
- 降低复杂度
- 隔离复杂度
- 隐藏实现细节
- 限制变化所影响的范围
- 隐藏全局数据
- 让参数传递更顺畅
- 创建中心控制点
- 创建中心控制点
- 让代码更易于重用
- 为程序族做计划
- 把相关操作放到一起
- 实现特定的重构
核对表:类的质量
抽象数据类型:
你是否把程序中的类都看作抽象类型了?是否从这个角度评估他们的接口了?
- 抽象
- 类是否有一个中心目的?
- 类的名字是否恰当?其名字是否表达了其中心目的?
- 类的接口是否展示了一直的抽象?
- 类的接口是否能让人清楚明白地知道该如何使用它?
- [类的接口是否足够抽象,使你能不必顾虑它是如何实现其服务的?你能把类看作的是黑盒子吗?
- 类提供的服务是否足够完整,能让其他类无需动用其内部参数?
- 是否已从类中出去无关信息?
- 是否考虑过把类进一步分解为组件类?是否已尽可能将其分解?
- 在修改类时是否维持了其接口的完整性?
封装
- 是否把类的成员的可访问性降到最小?
- 是否避免暴露类中的数据成员?
- 在编程语言所许可的范围内,类是否1⃣️尽可能地对其他的类隐藏了自己的实现细节?
- 类是否避免对其他使用者,包括其派生类会如何使用它做假设?
- 类是否不依赖于其他类?他是松散耦合的吗?
继承
- 继承是否只用来建立“是一个”的关系?也就是说,派生类是否遵循了LSP?
- 类的文档是否记述了其继承策略?
- 派生类是否避免了“覆盖”不可覆盖的方法?
- 是否把共用的接口、数据和行为都放到尽可能高的继承层次中了?
- 继承层次是否很浅?
- 基类中所有的数据成员是否都被定义为private而非protected的了?
跟实现相关的其他问题?
- 类中是否只有大约七个或更少的数据成员?
- 是否把类直接或间接调用其他类的子程序的数量减到最少了?
- 类是否只在绝对必要时才与其他的类相互协作?
- 是否在构造函数中初始化了所有数据成员?
- 除非拥有经过测量的、创建浅层复本的理由,类是否都被设计为当作深层复本的使用?
与语言相关的问题 - 你是否研究过所用编程语言里和类相关的各种特有的问题?
要点
- 类的接口应该提供一致的抽象。
- 类的旧口应隐藏一些信息——如某个系统接口、某项设计决策。或一些实现细节。
- 包含往往比继承更可取——除非你要对“是一个”的关系建模。
- 继承是一种有用的工具,但它却会增加复杂度,这有违于软件的首要技术使命——管理复杂度。
- 类是管理复杂度的首要选工具。要在设计类时给予足够的关注,才能实现这一目标。
本文探讨了良好程序设计中类的重要性和封装原则,强调了抽象数据类型(ADT)的作用,以及如何创建清晰、一致的类接口。文章还讨论了封装、继承和实现的注意事项,提供了检查类质量的标准。
5150

被折叠的 条评论
为什么被折叠?



