开放-封闭原则(Open Close Principle)
一个软件实体,比如类,模块,函数应该对扩展开放,对修改关闭。说的通熟易懂一些就是一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现改变
注意事项:
- 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的 public 方法
- 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
- 抽象层尽量保持稳定,一旦确定不允许修改
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求
单一职责原则(Single Responsibility Principle)
应该有且只有一个原因引起类的变更。换句话说就是一个接口只做一件事,即一个职责一个接口。但是困难的是划分职责时并没有一个标准,最终都是需要从实际的项目去考虑。我们在设计的时候,尽量单一,然后对于其实现类就要多方面的考虑。不能死套单一职责原则,否则会增加很多类,给维护带来不便
里氏替换原则(Liskov Substitution Principle)
核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类
通俗来讲:只要父类能出现的地方子类就能出现。反之,父类则未必能胜任
好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行
在使用里氏代换原则时需要注意如下几个问题:
① 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法
② 在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一
③ 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合等关系代替继承
依赖倒转原则(Dependence Inversion Principle)
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现
依赖倒转原则是程序要依赖于抽象接口,不要依赖于具体实现。简单的来说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块的耦合
核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
表现为以下的三点:
① 模块间的依赖关系通过接口和抽象类产生,实体类之间不直接发生依赖关系
② 接口和抽象类不依赖于实现类
③ 实现类依赖接口或者抽象类
说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
通俗来讲: 依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合
问题描述: 类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类 B 和类 C 是低层模块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险
解决方案: 将类 A 修改为依赖接口 I,类 B 和类 C 各自实现接口 I,类 A 通过接口间接与类 B 或者类 C 发生联系,则会大大降低修改类 A 的几率
好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter 注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过 Setter 方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象
迪米特法则(Law of Demeter,也称为最小知识原则)
一个软件实体应当尽可能少地与其他实体发生相互作用
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系
迪米特法则还有几种定义形式,包括:不要和 “陌生人” 说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的 “朋友”,否则就是 “陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与 “陌生人” 发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
下面通过一个简单实例来加深对迪米特法则的理解:
某 CRM 系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如图所示结构:
由于界面控件之间的交互关系复杂,导致在该窗口中增加新的控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件
现使用迪米特对其进行重构
在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图所示:
接口隔离原则(Interface Segregation Principle)
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
表现
①接口尽量小。根据具体业务把一个接口按照接口隔离原则细化成更多的接口。但是在此基础之上必须不能违背单一职责原则
②接口要高内聚。高内聚的意思就是提高接口和类处理能力,减少对外的交互。接口是对外的承诺,因此设计时应该尽量少公布接口中的 public 方法,承诺越少系统开发越有利且变更风险就越少
③定制服务。定制服务就是单独为一个个体提供服务,即只提供访问者需要的方法
④使用接口隔离原则时,需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法
和单一职责的区别
单一职责要求类和接口,或者方法的职责单一,侧重点在职责,这是根据业务逻辑进行划分的
接口隔离原则要接口中的方法尽量少。比如,一个接口有十个方法,不同的方法做不同的事情,但是这个接口总体就是处理一件事情,然后具体细分成了 10 个方法。不同的模块根据不同的权限进行访问,这是符合单一职责原则的。但是按照接口隔离的原则是要求接口中的方法尽量少,落实到这个实例就是要求尽量多几个专门的接口供不同的模块使用,而不是只有一个臃肿的接口,依据权限去限制不同模块可以访问的方法
总结
单一职责原则告诉我们实现类要职责单一
里氏替换原则告诉我们不要破坏继承体系
依赖倒置原则告诉我们要面向接口编程
接口隔离原则告诉我们在设计接口的时候要精简单一
迪米特法则告诉我们要降低耦合
开闭原则是总纲,告诉我们要对扩展开放,对修改关闭