开闭原则(Open Close Principle)

Software entites like classes, modules and funtcions should be open for extension but closed for modification.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)

开闭就是说对扩展开放,对修改关闭。在程序需要进行扩展的时候,不能去修改原有代码,实现一个热插拔的结果,所以一句话概括就是:为了使程序的扩展性好,易于维护各升级,想要达到这样的效果,需要用接口和抽象类。

问题由来

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

  1. 开闭原则对测试的影响
  2. 开闭原则可以提高复用性
  3. 开闭原则可以提高可维护性
  4. 面向对象开发的要求

注意

开闭原则也只是一个原则

项目规章非常重要

预知变化


里氏替换原则(Liskov Substitution Principle)

If for each object o1 for type S there is an object o2 of type T such that all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtyple of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

Functions that use pointers or referencesss to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

里氏替换原则面向对象设计的基本原则之一,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用基石,只有当衍生类可以替换掉基类,软件单位的功能不受影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏替换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键就是抽象化,而基类与了类的关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。

问题由来

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案

当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

  1. 子类必须完全实现父类的方法
  2. 子类可以有自己的个性
  3. 覆盖或实现父类的方法时输入参数可以被放大(子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松)
  4. 覆写或实现父类的方法时输出结果可以被缩小

目的

建议

采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀(委屈点);把子类单独作为一个业务使用,则会让代码间的耦合关系变得扑朔迷离(缺乏类替换的标准)。


依赖倒置原则(Dependence Inversion Principle)

High level modules should not depend upon low level modules. Both should depend upon abstractios. Abstractions should not depend upon details. Details should depend upon abstractions.(高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。)

开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

问题由来

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案

将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  2. 抽象不应该依赖细节;
  3. 细节应该依赖抽象。

  4. 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;

  5. 接口或抽象类不依赖实现类;
  6. 实现类依赖接口或抽象类。

写法

  1. 构造函数传递依赖对象
  2. Setter方法传递依赖对象
  3. 接口声明依赖对象

建议

每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备

变量的表面类型尽量是接口或者是抽象类

任何类都不应该从具体类派生

尽量不要覆写基类的方法

结合里氏替换原则使用


接口隔离原则(Interface Segregation Principle)

Client should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)

The dependency of one class to another on should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好,这有一个是降低类之间的耦合度意思,从这可以看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。

问题由来

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

解决方案

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

建议

一个接口只服务于一个子模块或者业务逻辑;

通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;

已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;

了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!


迪米特法则(Law of Demeter)(最少知道原则)

Only talk to your immediate friends(只与直接的朋友通信。)

最少知识原则(Least Knowledge Principle,LKP)。一个对象应该对其他对象有最少的了解,即,一个类应该对自己需要耦合或调用的类知道得最少,被耦合或调用的类的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

问题由来

类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案

尽量降低类与类之间的耦合。

建议

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也维护带来了难度。


单一职责原则(Single Responsibility Principle)

There should never be more than one reason for a class to change.(不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。)

一个类,只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。

问题由来

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案

遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

好处

建议

接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。