软件设计的基本原则
Rober C指出,导致一个软件设计的可维护性较低,也就是说会随着性能要求的变化而“腐烂”的真正原因是4个:
1.过于僵硬(Rigidity) 很难加入一个新功能
2.过于脆弱(Fragility) 在一个地方的修改,可能会导致其它地方的错误发生
3.复用率低(Immobiligy)可以复用的代码总是依赖于其它的代码,导致难以复用
4.黏度过高(Viscosity) 在模块之间中搭建桥梁来建立一个新功能
一个好的系统设计的目标是:
1.可扩展性(Extensibility)
2.灵活性(Flexibility)
3.可插入性(Pluggability)
系统的可扩展性是由“开-闭”原则,里氏代换原则,依赖倒转原则和组合/聚合复用原则保证的。
系统的灵活性是由“开-闭”原则,迪米特原则,接口隔离原则所保证的
系统的可插入性是由 “开-闭"原则,里氏代换原则,组合/聚合复用原则以及依赖倒转原则保证的
1,开闭原则(Open-Close Principle OCP )
open to extension,but close to modification(对扩展开放,对修改关闭)
如何实现开闭原则
1.1抽象是关键
解决问题的关键在于抽象化,在面向对象的编程语言里,可以给系统定义出一个一劳永逸,不再更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。在抽象类或接口中,规定所有具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层好像预见了所有可能 的扩展,因此在任何扩展情况下都不会改变。这使得系统的抽象层不需修改,满足了,对修改关闭。同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计以扩展开放,满足了对扩展开放原则。
1.2.对可变的封装原则
”开-闭“从另一个角度讲,就是所谓的”对可变的封装原则(Principle of Encapsulation of Variation.EVP)“,意思是,找到一个系统的可变因素,将之封装起来
对可变性的封装原则有两点:
1.2.1一种可变性不应当散落在代码的很多角落里,应当被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。继承应当被看作是封装变化的方法,而不应当被认为是从一般对象生成特殊的对象 的方法
1.2.2一种可变性不应当与另一种可变性混合在一起。如果读者留心本书研究的设计模式类图,会发现所有类图的继承结构一般不超过两层,不然就意味着将两种不同的可变性混合在了一起
2.里氏代换原则--
指任何基类可以出现的地方,子类一定可以出现,它是对开闭原则的补充。实现”开闭原则“的关键步骤是抽象化,而基类与子类的继承关系就是抽象化的具体体现,所以里氏代换原则则是对实现抽象化的具体步骤的规范。违反里氏代换原则的也违背”开闭“原则,反过来不一定成立
3.依赖倒转原则
要依赖于抽象,不要依赖于实现。开闭原则是目标,而达到这一目标的手段是依赖倒转原则。要想实现开闭原则就要实现依赖倒转原则,违反依赖倒转原则就不可能达到”开闭“原则的目标
实现开闭原则的关键是抽象化,并且从抽象化导出具体化的实现,如是说开闭原则是面向对象设计的目标的话,依赖倒转原则就是这个面向对象设计的主要机制。依赖倒转原则讲的是:要依赖于抽象,不要依赖于具体。
依赖倒转原则的表述:抽象不应当依赖于细节,细节应当依赖于抽象。另一种表述:要针对接口编程,不要针对实现编程
联合使用Java接口和Java抽象类:
由于Java抽象类具有提供缺省实现的优点,而Java接口具有其它所有的优点,所以联合使用两者(让抽象类实现接口)是一个很好的选择。首先,声明类型的工作仍然是由Java接口承担的,但是同时给出的还有一个Java抽象类,为这个接口提供缺省实现。其它同属于这个抽象类型的具体类可以选择实现这个Java接口,也可以选择继承自这个抽象类。如果一个具体类直接实现这个Java接口的话,它就必须自行实现所有的接口,相反,如果它继承自抽象类的话,它可以省去一些不必要的方法,因为它可以从抽象类中自动得到这些方法的缺省实现。如果需要向Java接口加入一个新的方法的话,那么只要同时向这个抽象类加入这个方法的具体实现就可以了,因为所有类继承自这个抽象类的子类都会自动从这个抽象类得到这个具体方法。这其实是缺省的适配模式。
4.合成/聚合复用原则
要尽量使用合成/聚合,而不是继承关系达到复用的目的。显然,合成/聚合利原则是与里氏代换原则相辅相成的,两者又都是对实现开闭原则的具体步骤的规范。前者要求设计师首先考虑合成/聚合关系,后者要求在使用继承关系时,必须确定这个关系是符合一定条件的。
遵守合成/聚合复用原则是实现“开闭原则”的必要条件,违反这一原则就无法使系统实现开闭原则这一目标。
合成/聚合原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象和委派达到复用已有功能的目的。
5.迪米特法则
一个软件实体应当与尽可能少的其它实体发生相互作用。当一个系统面临功能扩展时候,其中会有一些模块,它们需要修改的压力比其他一些模块要大,最后的结果可能是这些模块需要修改或者不需要修改,但无论是那一种情况,如果这些模块是相对孤立的,那么它们就不会将修改的压力传递给其它的模块。这就是说,一个遵守迪米特原则设计出来的系统在功能需要扩展时,会相对更容易做的到修改关闭。
又叫最少知识原则,就是说,一个对象应当对其他对象尽可能少的了解
狭义上讲:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另外一个类的某一个方法的话,可以通过第三者转发这个调用。
广义迪米特法则在类设计上的体现:1.优先考虑将一个类设计成不变类(如String,BigInteger,BigDecimal等),当涉及任何一个类的时候,都首先考虑这个类的状态是否需要改变,即便一个类必须是可变类,在给它的属性设置赋值方法时候,也要保持吝啬态度。除非真的有必要,否则不要为一个属性设置赋值方法。
2.尽量降低一个类的访问权限 一个类具有package-private访问权限的好处是,一旦这个类发生修改,那么受影响的客户端必定都在这个库内部。相反,如果 一个类被不恰当地设置成public,那么客户程序就有可能会使用这个类。这旦这个类在一个新版本中被删除,就可能造成一些客户程序停止运行的情况。
3.谨慎使用Serializable
一个类如果实现了Serializable接口的话,客户端就可以将这个类实例串行化,然后再并行化。由于串行化和并行化涉及到类的内部结构,如果这个类的内部private结构在一个新版本中发生变化的话,那么客户端可能会根据新版本的结构试图将一个老版本的串行化结果并行化,这会导致失败。换言之,为防止这种情况发生,软件提供商一旦将一个类设置成为Serializable的,就不能再在新版本中修改这个类的结构,包括private的方法和句段
4.尽量降低成员的访问权限
6.接口隔离原则
应当为客户端提供尽可能小的单独的接口,而不是提供大的总接口。
显然接口隔离原则与广义 的迪米特法则都是对一个软件实体与其他的软件实体的通信的限制。广义的迪米特法则要求尽可能限制通信的宽度和深度。接口隔离原则则所限制的是通信的宽度,也就是说,通信应当尽可能窄。这两个原则都会使用软件系统在功能扩展的过程中,不会将修改的压力传递到其它的对象
使用多个专门的接口比使用单一的总接口要好。从一个客户的角度讲:一个类对另外一个类的依赖性应当是建立在最小的接口上的。
“对可变性的封装”实际上是设计模式的主题,换言之,所有的设计模式都是对不同的可变性的封装,从而使系统在不同的角度上达到“开-闭”原则的要求。
接口使得软件系统在灵活性和可扩展性,可插入性方面得到了保证。由于抽象类在java中是单继承的,所以并不总是能保证这些。在理想情况下,一个具体Java类应当只实现Java接口和抽象类中声明过的方法,而不应当给出多余的方法
一个抽象类的构造子可以被其子类调用,从而使一个抽象类的所有子类都可以有一些共有的实现,而不同的子类可以在些基础上有其自己的实现。抽象类和子类的这种关系实际上是模板方法模式的应用。
Scott Meyers曾指出,只要有可能,不要从具体类继承
抽象类应该拥有尽可能多的共同代码,抽象类应该拥有尽可能少的数据(因为数据移到是自顶向下,这样会占用很多资源,一个对象的数据不论是否使用都会占用资源,这样可以节省内存资源)
针对抽象编程,就是依赖倒转原则
只要可能,尽量使用合成,而不是使用类继承来达到复用的目的(组合、聚合复用原则)。
什么时候才应当使用继承复用:
peter Coad认为,继承代表“一般化、特殊化”关系,其中基类代表一般,而衍生类代表特殊,衍生类将基类特殊化或扩展化。只有当以下的Coad条件全部满足时,才应当使用类继承:
1.子类是超类的一个特殊种类,而不是超类的一个角色,也就要区分“Has-A”与“Is-A”两种关系的不同。Has-A应当使用聚合,而只有Is-A关系才符合继承关系。
2.永远不会出现需要将子类换成另一个类的子类的情况。如果设计师不是很肯定一个类会不会在将来变成另一个类的子类的话,就不应当将这个类设计成当前这个超类的子类。
3.子类具有的扩展超类的责任,而不是具有置换掉(Override)或注销(Nullify)超类的责任。如果子类需要大量地置换掉超类的行为,那么这个子类不应当成为这个超类的子类
4.只有在分类学角度上有意义时,才可以使用继承,不要从工具类继承。
里氏代换原则是可否使用继承有关系的准绳。不要从工具类继承
从开闭原则,中可以看出面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化,具体化可以给出不同的版本,每一个版本都给出不同的实现。
里氏代换原则说,如果一个方法对一个基类成立的话,那么一定适用于其子类。