面向对象设计原则的核心:可维护性和可复用性。
3.1软件系统的可维护性
导致软件设计会随着性能要求的变化而腐烂的可维护性较低的真正原因有四个:
1.过于僵硬:很难在一个软件系统里加入新的性能,新性能可能波及到很多模块的修改。
2.过于脆弱:加入新的性能可能在加入的地方不出现BUG,但在其他代码或模块出现BUG。
3.复用率低:复用提取某部分代码,可能在系统多个模块依赖此部分代码,很难将他们分开。
4.黏度过高:一个改动可以按照原有设计意图进行,或者破坏原有设计意图。第一种对系统的未来有利,
第二种可是解决短期问题,牺牲中长期利益。如果系统总是按照第二种进行,那么程序的黏度过高不易维护。
设计的目标:
1.可扩展性:新的性能在修改其他模块的前提可以很容易地加入到系统中去。过于僵硬的反面。
2.灵活性:新的性能加入系统之后,程序不会出现新的BUG。过于脆弱的反面。
3.可插入性:很容易将一个类抽出去,同时将另一个有同样接口的类加入进来。比如应该很容易地将一辆汽车的
防撞气囊取出来,换上新的。如果将气囊拿出来后,汽车传动杆不工作了,那么就不是一个可以插入性很好的系统。
3.2 系统的可复用性
复用的重要性:1.较高的生产效率 2.较高的软件质量 3.恰当使用复用可以改善系统的可维护性。
传统的复用:1.代码的剪贴复用 2.算法的复用 3.数据结构的复用
对可维护性和可复用性的支持设计原则:
1.开闭原则
2.里氏代换原则
3.依赖倒转原则
4.接口隔离原则
5.组合、聚合复用原则
6.迪米特法则
4. 开闭原则:一个软件实体应当对扩展开放,对修改关闭。
抽象化是关键,这个抽象层预见了所有可能的扩展,因此,在任何扩展情况下都不会改变。这就使得系统的抽象层不
需要修改,从而满足了“开闭”原则的第二条,对修改关闭。同时由于从抽象层导出一个或多个新的具体类可以改变
系统的行为,因此系统的设计 对扩展是开放的,这就满足“开闭”原则的第一条。
4.3 与其他设计原则的关系
里氏代换原则,任何基类可以出现的地方,子类一定可以出现。
依赖倒转原则,针对抽象编程,不要针对具体编程
合成、聚合复用原则,尽量使用合成、聚合,而不是继承关系达到复用的目地。
迪米特法则,一个软件实体应当与尽可能少的其他实体发生相互作用。
接口隔离原则,应当为客户端提供尽可能小的单独的接口,而不是提供大的总接口。
4.6 一个重构做法的讨论
“将条件转移语句改写成为多态性”是一条广为流传的代码重构做法。但是这一做法本身并不能保证实现“开闭”原则,
设计师应当以“开闭”原则为指导原则。这一代码重构的做法并不能成为设计的原则。事实上,这一做法有明显的缺点:
1.任何语言都能提供条件转移功能,条件转移本身并不是错误的,更不是什么罪恶。如果需要,设计师完全可以选择使用条件转移。
2.使用多态性代替条件转移意昧着大量的类被创建出来。管理和识别相当繁琐。
何时使用这种重构做法:应发从“开闭‘原则出发来做判断。如果 一个条件转移语句确实封装了某种商务逻辑的可变性,那么将此种
可变性封装起来就符合“开闭”原则设计思想了。但是如果一个条件转移语句没有涉及重要的商务逻辑,或者不会随着时间的变化而变化,
也不意味着任何的可扩展性,那么它就没有涉及任何有意义的可变性。这个时候将这个条件转移语句改写成为多态性就是一种没有意义的
浪费。
6.抽象类:代表一个抽象概念,它提供一个继承的出发点。抽象类一定是用来继承的,具体类不是用来继承的。
代码重构的建议:
抽象类应当拥有尽可能多的共同代码
在一个从抽象类到多个具体类的继承关系中,共同的代码应该尽量移动到抽象类里。
抽象类应该拥有尽可能少的数据
与代码的移动方向相反的是,数据的移动方向是从抽象类到具体类。一个对象的数据不论是否使用
都会占用资源,因此数据应该尽量放到具体类或者等级结构的低端。
6.3 基于抽象类的模式和原则
针对抽象编程,不要针对具体编程。这就是依赖倒转原则。换言之,应该针对抽象类编程,不要针对具体子类编程,
这一原则点出了抽象类对代码复用的一个最重要的作用。
正确使用继承
继承关系可以分成两种:一种是类对接口实现,称做接口继承。另一种是类对类的继承,称做实现继承。第二种继承关系是很容易被滥用的
一种复用工具。
正如本章前面所指出的,抽象类是用来继承的,因此抽象类注定要与继承关联在一起。只要可能,尽量使用合成,而不要使用继承来达到复用的目的。
6.4 什么时候才应该使用继承复用
当以下条件全部满足时,才应当使用继承关系:
a.子类是基类的一个特殊种类,而不是基类的一个角色,也就是要区分“has-A"与"Is-A"两种关系的不同。Has-A关系应该使用聚合关系,而只有Is-A关系
才符合继承关系。
b.永远不会出现需要将子类换成另一个类的子类的情况。如果设计师不是很肯定一个类会不会在将来变成另一个类的子类的话,就不应该将这个类设计
成当前这个基类的子类。
c.子类具有扩展基类的责任,而不是具有置换掉或注销掉基类的责任。如果子类需要大量地置换掉基类的行为,那么这个子类不应该成为这个基类的子类。
d.只有在分类学角度上有意义时,才可以使用继承,不要从工具类继承。
备注;里氏代换原则是可否使用继承关系的准绳。凡是不符合Coad条件的均不会满足里氏代换原则。
子类扩展基类的责任
1.子类扩展基类的责任,而不是置换掉或撤销掉基类的责任。如果一个子类需要将继承自基类的责任取消或置换后才能使用的话,很有可能这个子类根本不是哪个基类的子类。
2.将狗设计成猫的子类,如右图所示。猫有上树能力,狗没有。为了继承关系成立,只好将猫上树的能力取消掉,这个继承关系显然是错误的。
正确的继承关系是引入一个抽象类,在这里就是”动物“类,将两个具体类设计成抽象类的子类
3.一般而言,如果子类需要置换掉太多的基类的行为,那么一定是因为子类的行为与基类有太大的区别。这个时候,
很可能子类并不能取代基类出现在任何需要基类的地方,也就是说它们不满足里氏代换原则。
8 依赖倒转原则
依赖倒转原则讲的是,要依赖于抽象,不要依赖于具体。
具体耦合:具体性耦合发生在两个具体的类之间,经由一个类对另一个具体类的直接引用造成。
抽象耦合:抽象耦合关系发生在一个具体类和一个抽象类之间,使两个必须发生关系的类之间存有最大的灵活性。
9 接口隔离原则
10 合成、聚合复用原则
这个设计原则有另一个更简短的表述:要尽量使用合成、聚合,尽量不要使用继承。
合成和聚合均是关联的特殊种类。聚合用来表示"拥有”关系或者整体与部分的关系;而合成则用来表示一种强得多的“拥有”关系。在一个合成关系里,
部分和整体的生命周期是一样的。一个合成的新的对象完全拥有对其组成部分的支配权,包括它们的创建和销毁。
用C程序员较易理解的语言来讲,合成是值的聚合,而通常所说的聚合则是引用的聚合。
合成、聚合的优点:
1.新对象存取成分对象的唯一方法是通过成分对象的接口。
2.这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
3.这种复用支持包装。
4.这种复用所需的依赖较少。
5.每一个新的类可以将焦点集中在一个任务上。
6.这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
合成、聚合的缺点:
主要缺点:就是通过使用这种复用建造的系统会有较多的对象需要管理。
继承复用的优点:
1.新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入子类。
2.修改或扩展继承而来的实现较为容易。
继承复用的缺点:
1.继承复用破坏包装,因为继承将基类的实现细节暴露给子类。由于基类的内部细节常常是对子类透明的,
因此这种复用是透明的复用,又称“白箱”复用。
2.如果基类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变会像水中投入石子
引起的水波一样,将变化一圈又一圈地传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应基类的变化。
3.从基类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。