参考教材:刘伟《设计模式》第2版 清华大学出版社
链接:https://pan.baidu.com/s/1ZQgwSacGUJ0BWh5XnRm7lw
提取码:0000
面向对象设计原则
可维护性与复用性
Robert Cecil Martin认为,可维护性较低的软件表现在:
(1)僵硬(Rigidity)。添加一个新功能时,需要改动大量模块,代码的灵活性差。
(2)脆弱(Fragility)。只是修改某处的代码,却使得另一处似乎与本次修改没什么关系的地方发生故障。
(3)低复用率(Immobility)。代码的复用,或称重用,指的是同一段代码在本软件不同的地方甚至在其它软件中重复使用——开发人员无需反复编写高度相似甚至相同的代码。对代码进行简单的复制粘贴,广义上也算作重用,但很多时候这样的重用并没有什么实用价值。
(4)高黏度(Viscosity)。改动系统时,有时可能得以保存原有的设计意图与框架,有时也可能破坏原始意图和框架。显然,前者对于扩展更有利。如果在改动软件时,发现后者比前者更容易,则称该软件系统的黏度过高,这将导致程序员采用错误的代码维护方案,使得维护困难。
Peter Coad认为,一个好的系统,应当具备如下的性质:
(1)可扩展性(Extensibility)。容易将新的功能添加到现有系统中,与“过于僵硬”相对。
(2)灵活性(Flexibility)。代码修改时不会波及很多其它模块,与“过于脆弱”相对应。
(3)可插入性(Pluggability)。可以很方便地将一个类抽取出去,同时将另一个有相同接口的类添加进来,与“黏度过高”相对应。
如何使得系统满足上述的三个性质,其关键在于恰当提高系统的可维护性和可复用性。
这两者一般是冲突的。例如,A和B两个模块都需要使用另一个模块C。如果A需要C增加一个新的行为,但B不需要,甚至不允许C增加该行为。假如坚持复用,就不得不以系统的可维护性为代价,如:修改B的代码,这将破坏系统的灵活性;而如果从保持系统的可维护性出发,就只好放弃复用。
传统的软件复用技术,包括代码的复用、算法的复用和数据结构的复用等,但这些复用有时候就会破坏系统的可维护性;而面向对象设计复用,在一定程度上可以解决这两个质量属性之间发生冲突的问题。
面向对象设计复用的目标,在于实现支持可维护性的复用。面向对象技术中的抽象、继承、封装和多态等特性,可以用来实现更高层次的复用性。通过抽象和继承,使得类的定义可以复用;通过多态,使得类的实现可以复用;通过抽象和封装,可以保持和促进系统的可维护性。在面向对象的设计里,可维护性复用都是以面向对象设计原则为基础的。这些设计原则,首先都是复用的原则。遵循这些设计原则,可以有效地提高系统的复用性,同时提高可维护性。
面向对象设计原则和设计模式,也是对系统进行合理重构的指南针。重构(Refactoring),是在不改变软件现有功能的基础上,通过调整程序代码,改善软件的质量、性能,使程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。设计模式和重构,也是普通程序员过渡到优秀程序员必学的两项技能。在这里,只介绍面向对象设计原则和设计模式。关于重构的详细学习超出本书的范围,在此不予以扩展,感兴趣的读者可以自行学习相关重构知识。
单一职责原则
一个对象应该只包含一个(或一类)职责,并且,该职责被完整地封装在一个类中。
另一种定义:就一个类而言,应该仅有一个(或一类)引起它变化的原因。
一个类(或者大到模块,小到方法),承担的职责越多,被复用的可能性越小。如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其它职责的运作。
单一职责原则,是最简单但又最难运用的原则,需要设计人员发现类的不同职责,并将其分离。而发现类的多重职责,需要设计人员具有较强的分析设计能力和相关的重构经验。
开闭原则
一个软件实体应当对扩展开放,对修改封闭。也就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即:在不修改源代码的情况下,改变这个模块的行为。
软件实体可以指:一个软件模块、一个由多个类组成的局部结构、或一个独立的类。
开闭原则(Open-Closed Principle,OCP)还可以通过一更加具体的“可变性封装原则”来描述,对可变性封装原则(Principle of Encapsulation of Variation,EVP)要求找到系统的可变因素,并将其封装起来。如:将抽象层的不同实现封装到不同的具体类中。EVP要求,尽量不要将一种可变性和另一种可变性混合在一起,这将导致系统中类的个数急剧增长,增加系统的复杂度。
百分之百的开闭原则很难达到,但是要尽可能使系统设计符合开闭原则。后面所学的里氏代换原则、依赖倒转原则等,都是开闭原则的实现方法。在即将学习的设计模式中,绝大部分的模式都符合开闭原则。
Liskov替换原则
形式定义:
如果对每个具有类型S的对象o_1,存在一个T类型的对象o_2,使得对于所有使用T定义的程序P中,当o_1被o_2替换时,程序P的行为不发生变化,则S是T的子类。
等效表述:
(1)任何基类可以出现的地方,子类一定可以出现。
(2)派生类(子类)对象,可以在程序中代替其基类(超类)对象。
里氏代换原则是实现开闭原则的重要方式之一。由于使用基类对象的地方都可以使用子类对象,因此在程序中,尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
使用Liskov替换原则时,通常需要注意:
(1)子类的所有方法必须在父类中声明。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义。如果一个方法只存在于子类中,父类不提供相应的声明,则无法在父类对象中直接使用该方法。此时无法直接使用父类来定义,只能使用子类,则说明该设计违背了里氏代换原则。
(2)在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法。运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
(3)Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则。这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
依赖反转原则
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
另一种表述:要针对接口编程,不要针对实现编程。
简单来说,依赖倒转原则就是指:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。也就是说,在程序代码中传递参数时或在组合聚合关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
依赖倒转原则是COM、CORBA、EJB、Spring等常用的技术和框架背后的基本原则之一。
接口隔离原则
客户端不应该依赖那些它不需要的接口。
另一种定义:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
实质上,接口隔离原则(Interface Segregation Principle,ISP)是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,如Java和C#里的interface。对于这两种不同的含义,ISP的表达方式以及含义都有所不同。
合成复用原则(组合 / 聚合复用原则)
尽量使用对象组合,而不是继承来达到复用的目的。
通俗地说,合成复用原则,就是指在一个新的对象里,通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法,达到复用其已有功能的目的。简言之,要尽量使用组合或聚合关系,少用继承。
通过继承来实现复用很简单,而且子类可以覆盖父类的方法,易于扩展。但其主要问题在于:继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类。由于基类的某些内部细节对子类来说是可见的,所以这种复用又称为“白箱”复用。如果基类发生改变,那么子类的实现有时也不得不发生改变;从基类继承而来的实现是静态的,不能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(例如,类不能被final修饰)。
通过组合 / 聚合来复用,是将一个类的对象作为另一个类的对象的一部分,或者说,一个对象是由另一个或几个对象组合而成。由于组合或聚合关系可以将已有的对象纳入到新对象中作为成员变量,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做可以使得成员对象的内部实现细节对于新对象是不可见的,所以这种复用又称为“黑箱”复用。相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其它对象。
组合 / 聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其它类造成的影响相对较少,因此一般首选使用组合 / 聚合来实现复用,其次才考虑继承。在使用继承时,需要尽量遵循里氏代换原则。有效使用继承会有助于对问题的理解,降低复杂度;滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
Demeter定律
迪米特法则(Law of Demeter,LoD),又称为最少知识原则(Least Knowledge Principle,LKP)。它有多种定义方法,其中几种典型定义如下:
(1)不要和“陌生人”说话。英文定义为:“Don’t talk to strangers.”
(2)只与你的直接朋友通信。英文定义为:“Talk only to your immediate friends.”
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
英文定义:“Each unit should have only limited knowledge about other units:only units ‘closely’ related to the current unit.”
简单地说,Demeter定律,就是指一个软件实体应当尽可能少地与其它实体相互作用。这样,当一个模块修改时,就会尽量少地影响其它模块,扩展会相对容易。这是对软件实体之间通信的限制,它要求限制软件实体之间通信的