设计模式遵循六大原则:单一职责原则、开闭原则、里氏替换原则、最少知识原则(迪米特法则)、接口隔离原则、依赖倒置原则
1.单一职责原则
单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。这个原则存在争议之处在对职责的定义,什么是类的职责,以及怎么划分类的职责。
定义:There should never be more than one reason for a class to change.
单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,他就负责一件事。一个职责就是一个接口。
对于接口,我们在设计的时候一定要做到单一,但是对于实现类就需要多方面考虑了,可能不会满足单一职责原则。
最佳实践:
- 单一职责适用于接口、类,同时也适用于方法。一个方法尽可能做一件事情。
- 类的单一职责受到非常多因素的制约,纯理论地来讲,这个原则是非常优秀的,但是现实有现实的难处,你必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
2.开闭原则
定义:Software entities like classes, modules and functions should be open for extension but closed for modifications.
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。即,一个软件实体(模块、类、接口、方法)应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则是最基本的原则,是其他原则和设计模式的精神。
开闭原则的重要性:
-
开闭原则对测试的影响
所有已经投产的代码都是有意义的,并且都不受系统规则的约束。如果通过修改而不是扩展代码来应对需求变化,需要重新测试已经修改的代码。
一个方法的测试:
- 正常的业务测试
- 边界条件测试
- 异常测试
-
开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被服用的可能性就越大。
- 开闭原则可以提高可维护性
-
面向对象开发的要求
快速的应对变化,需要在设计之初就考虑所有可能变化的因素,然后留下接口。
需求变化的三种类型:
-
逻辑变化
可以通过修改原有类中的方法的方式来完成
-
子模块变化
通过扩展来应对
-
可见视图变化
可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应,但还是可以通过扩展来完成变化,这就要看原来的设计是否灵活。
应对需求变化的原则:保持历史的纯洁性,不去修改历史。
应对需求变化的三种方法:
-
修改接口
不可靠的,该方案应该被直接否定。
-
修改实现类
该方法在项目有明确的章程或优良的架构设计时,是一个非常优秀的方法,但是仍有缺陷。
-
通过扩展实现变化
好方法,修改少,风险小。
最佳实践:
-
抽象约束
抽象是对一组食物的通用描述,没有具体的实现,也就可以跟随需求的变化而变化。
第一,通过接口或抽象类约束扩展,不允许出现在接口或抽象类中不存在public方法;
第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
第三,抽象层尽量保持稳定,一旦确定即不允许修改。
-
元数据(metadata)控制模块行为
尽量使用元数据来控制程序的行为,减少重复开发。
元数据,就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件获得,也可以从数据库中获得。
-
制定项目章程
章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
-
封装变化
第一,将相同的变化封装到一个接口或抽象类中;
第二,将不同的变化封装在不同的接口或抽象类中。
3.里氏替换原则
定义:
- 第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所在的对象o1都代换为o2时,程序P的行为没有发生变化,那么类型S是类型T的字类型。)
- 第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)
里氏替换原则包含了4层含义:
- 子类必须完全实现父类的方法
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生"畸变",则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
- 子类可以有自己的个性
子类可以有自己的行为和外观,也就是方法和属性。
- 覆盖或实现父类的方法时输入参数可以被放大
子类的方法可以重载(Overload)父类的方法,并把输入参数设置成为父类的方法的输入参数的父类(即把输入参数放大)。这时,通过父类的引用调用这个方法,实际调用的还是父类的方法,子类的方法由于只是重载而不是覆写(Override),会被隐藏掉。子类可以覆写(Override)父类的方法。
- 覆写或实现父类的方法时输出结果可以被缩小
采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。
最佳实践:
- 在项目中,采用里氏替换原则时,尽量避免子类的"个性",一旦子类有"个性",这个子类和父类之间的关系就很难调和了,把子类当作父类使用,子类的"个性"会被抹杀
4.最小知识原则(Least Knowledge Principle,LKP)
定义:一个对象应该对其他对象有最少的了解。
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少。
最小知识原则包含以下4层含义:
-
只和朋友交流
Only talk to your immedate friends(只喝直接朋友通信。)
两个对象之间的耦合就成为朋友关系。
朋友类:出现在在成员变量、方法的输入输出参数中的类成为成员朋友类,而出现在方法内部的类不属于朋友类。
-
朋友间也是有距离的
朋友类之间也不应该暴露太多方法。
-
是自己的就是自己的
如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。
-
谨慎使用Serializable
可能会因为对类的更改未在服务器和客户端之间同步而引起序列化失败问题。
5.接口隔离原则
定义:
-
第一种定义:客户端不应该依赖它不需要的接口。
客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其单纯性。
- 第二种定义:类间的依赖关系应该建立在最小的接口上。
与单一职责原则的区别:
单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑的划分,而接口隔离原则要求接口的方法尽量少。一个职责可能包含10个方法,这10个方法都放到一个接口中,但不同模块分别只访问其中几个,在系统外通过文档约束"不使用的方法",这是单一职责原则允许的,但按照接口隔离原则是不允许的。
保证接口的纯洁性:
-
接口要尽量小
这是接口隔离原则的核心定义。但是"小"是有限度的,首先就是不能违反单一职责原则,已经做到单一职责的接口不应该再分。即,根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
-
接口要高内聚
高内聚就是提高接口、类、模块的处理能力,减少对外的交互。具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
-
定制服务
定制服务就是单独为一个个体提供优良的服务。要求就是:只提供访问者需要的方法。
-
接口设计是有限度的
接口的设计粒度越小,系统越灵活。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低。所以接口设计一定要注意适度。
最佳实践:
- 一个接口只服务于一个子模块或业务逻辑;
- 通过业务逻辑研所接口中的public方法;
- 已经被污染了的接口,尽量去修改,若更改的风险较大,则采用适配器模式进行转化处理;
- 了解环境,拒绝盲从,深入了解业务逻辑才能设计出好接口。
6.依赖倒置原则
依赖倒置原则(Dependence Inversion Principle, DIP)的定义:
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
翻译过来,有三重含义:
- 高层模块不应该依赖底层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
更加精髓的定义就是"面向接口编程"——面向对象设计的精髓之一。
依赖倒置原则在Java语言中的表现就是:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
- 接口或抽象类不依赖于实现类。
- 实现类依赖与接口或抽象类。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并发开发引起的风险,提高代码的可读性和可维护性。
依赖是可以传递的。只要做到抽象依赖,即使是多层的依赖传递也无所畏惧。
最佳实践:
- 每个类尽量都有接口或抽象类,或者两者都具备。接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的世界对父类进行细化。
- 变量的表面类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要覆写(Override)基类的方法。
- 结合里氏替换原则使用。