原文参考链接:www.cnblogs.com/pengdai/p/9151800.html
面向对象设计的基本原则是五个(SOLID)。但除了经常被提到的这五个之外,还有迪米特法则和合成复用原则。所以,有些书籍或是文章也称六大原则或是七大原则。
1. 单一职责原则(SRP)
Single-Responsibility Principle:一个类只做一件事,也即:只有一个引起它变化得原因。单一职责原则可以看做是低耦合、高内聚在面向对象原则的引申。它将职责定义为引起变化的原因,通过减少引起变化的原因来提高内聚性。(设计模式的指导方针)
1.1 定义
Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. 一个对象应该只包含单一的职责,且该职责被完整地封装在一个类中。即又可定义为有且仅有一个原因是类变更。
1.2 分析
- 一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性就越小。而且如果一个类承担的职责过多,相当于将这些职责耦合在一起,当一个职责变化时,可能会影响其他职责的运作。
- 类的职责主要包括两个方面:数据职责和行为职责。数据职责通过类属性来体现,而行为职责通过类方法来实现。
- 单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在。它是最简单却又是最难运用的原则。它需要设计人员发现类的不同职责并将它们分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
1.3 优点
- 降低类的复杂性,使类的职责清晰明确。
- 提高类的可读性和维护性
- 降低由变更引起的风险。变更是无法避免的,如果接口的单一职责做的好,一个接口的修改只对相应的类有影响,对其他接口无影响。这对系统的扩展性和维护性都有很大帮助。
注意:单一职责原则只是提出了一个编写程序的标准。它用“职责”或“变化原因”的数量来衡量接口或类设计得是否合理,但是“职责”和“变化原因”都是没有具体标准的。一个类到底要负责哪些职责?这些职责怎么细化?细化后是否都要有一个接口或类?这些都需要从实际情况考虑。因此,单一职责原则的应用因项目而异,因环境而异。
1.4 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class OrderManager {
private OrderRepository repository = repositoryFactory.createOrderRepository();
public void place(Order order) {
if (order.isValid()) {
repository.add(order);
} else {
throw new InvalidOperationException("Order can't be placed.");
}
}
public void cancel(Order order) {
if (order.isValid() && order.canCancel(new Date())) {
order.remove(order)
} else {
throw new InvalidOperationException("Order can't be canceled.")
}
}
public static class RepositoryFactory {
public static OrderRepository createOrderRepository() {
return new OrderRepository();
}
}
}
OrderManager 类的实现体现了单一原则的思想。首先 OrderManage 类中的 place() 和 cancel() 都属于订单管理的业务逻辑,与领域逻辑层关注的事情是一致的。但在这两个方法的实现中,我们分别需要检验订单的正确性(是否包含了必要信息,如联系人、联系地址和联系电话),以及判断当前时间是否在允许取消订单的时间范围内。虽然它们都属于订单处理的逻辑,但拥有这些检查信息的是 order 对象,而不是 OrderManager,即 order 对象是检查订单的信息专家。因此,isValid() 和 canCancel() 方法应定义在 order 类中。至于添加和移除订单,它们也是下订单和取消订单的业务逻辑的一部分,但其实现却属于数据访问层的范畴,因此该职责被委派给了 OrderRepository 类。至于 RepositoryFactory 类,则是负责创建 OrderRepository 对象的工厂类。
在这个例子中,将数据访问逻辑从领域对象中分离出去是合理的,因为数据访问逻辑的变化方向与订单业务逻辑的变化原因是不一致的。这也是单一职责原则的核心思想。
2. 开放封闭原则(OCP)
Open-Closed Principle:对扩展开放,对修改关闭(设计模式的核心原则)
2.1 定义
一个软件实体(如函数、类和模块)应该对扩展开放,对修改关闭。一个好的软件系统,可以在不修改源代码的情况下,扩展新的功能。而实现开闭原则的关键就是抽象化。
2.2 分析
- 当软件实体因需求变化时,尽量通过扩展已有软件实体来提供新的行为,以满足对软件的新需求,而不是修改已有的代码。这样,使得变化中的软件具有一定的适应性和灵活性。已有的软件模块,特别是最终重要的抽象层模块不能再修改,这能使变化中的软件系统具有一定的稳定性和延续性。
- 实现开闭原则的关键就是抽象化。因为“开”的是具体的实现类,“闭”的抽象类和接口。所以,抽象类和接口是是“开闭”原则的关键角色,其一经设计应用便不允许修改。因此,抽象类和接口的设计之初,既要预见可能变化的需求,又能预见可能已知的扩展。所以说,抽象化是实现开闭原则的关键。
2.3 例子
举一个《大话设计模式》里的例子。用面向对象的语言,设计并实现一个运算器,要求实现加法和减法。在这该程序的设计之初,运用开闭原则,我们应有预见性地考虑到,后续可能加入对乘法、除法的支持。因此,可以在设计之初,对加法、减法等运算过程进行抽象,得到一个运算过程抽象类,它只有一个抽象接口 getResult()。那么,加法类、减法类等具体类都可以继承该抽象类,对 getResult() 抽象接口给予不同实现。因为后续的可能加入的乘法、除法等规则,都符合当前抽象,可以依上述方式进行继承扩展,而无需修改已有代码,最重要的是不用修改已有抽象。
下面给出《大话设计模式》中运算器的 UML 图。

3. 里式替换原则(LSP)
Liskov Substitution Principle:任何基类对象可以出现的地方,子类对象也可以出现;这一思想表现为对继承机制的约束规范。只有子类对象能够替换基类对象时,才能够保证系统在运行期间的行为是稳定可预期的。这种继承约束是保证复用的基础。
3.1 定义
Subtype Requirement:Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T. 子类型的要求:假设类型 T 的对象 x 使得 q(x) 是可证明的。那么如果 S 是 T 的子类型,则要求 S 的对象y 也使得 q(y) 是可证明的。
换一种令人容易理解的方式来说,就是:子类(派生类)对象替换父类(基类)对象,程序行为不会发生改变。
3.2 分析
- 里式替换原则是对继承关系使用的一种约束。因为它暗含了子类不能覆盖父类已实现的接口。因为只要子类覆盖父类的接口,很有可能引入了新的依赖或是逻辑,这样势必造成子类与父类共有接口的程序行为不一致,也就违反了里式替换原则。
- 里式替换原则,揭示了子类应当做的事是新增新的程序行为或是实现父类未实现的接口。
- 里式替换原则,又称为一种强的行为子类型关系。它要求在共有的接口上,子类型的行为应与父类型的行为保持一致。
3.3 例子
正方形在概念上是矩形,在行为上可能不是矩形。按照封装的原则,矩形应具有针对宽高的 set、get 接口。如果正方形如果对矩形进行继承,那么针对从父类继承来的 set、get 的接口可以有三种处理方法:不处理、抛出异常、修改接口逻辑(维持宽高一致)。不处理,不符合需求,正方形应宽高一致;抛出异常,接口使用出现异常,与父类的程序行为不一致,违反里式原则;修改接口逻辑,也直接违反里式原则。所以说,里式替换原则,又称为一种强的行为子类型关系。
例子来源:https://www.cnblogs.com/gaochundong/p/liskov_substitution_principle.html
4. 接口隔离原则(ISP)
Interface Segregation Principle:客户端不应依赖于那些它不需要的接口。(该法则与迪米特法则相通)
4.1 定义
客户端不应依赖那些它不需要的接口。
4.2 分析
- 使用多个小接口胜过单个大接口,客户端只需知道自己感兴趣的部分接口。
- 使用接口隔离原则拆分接口时,首先满足单一职责原则,将一组相关的抽象操作定义在一个接口中。在满足高内聚的前提下,接口中的抽象方法越少越好。
- 可以在进行系统设计时采用定制服务的方式,针对不同的客户端,提供宽窄不同的接口。
- 接口隔离原则与单一职责原则,在某些意义上来说,是相通的,甚至可说是重复的。因为单一职责可以说是,客户端所感兴趣的抽象方法的集合。只是两者着眼于不同的视角,接口隔离原则是客户端(调用者)视角,而单一职责原则是程序设计者视角的。
5. 依赖倒置规则(DIP)
Dependency Inversion Principle:要依赖于抽象,而不要依赖于具体的实现。
5.1 定义
- 高层模块不应依赖与底层模块。它们都应该依赖于抽象(如:接口)
- 抽象不应依赖于细节。细节应该依赖于抽象。
5.2 分析
- 如果说开闭原则是面向对象设计的目标,依赖倒置原则就是达到开闭原则的手段。如果要达到最好的开闭原则,就要尽量地遵守依赖倒置原则。
- 达成赖倒置原则,所依赖的编码设计约束
- 类中的所有成员对象必须声明为接口或抽象类
- 所有具体类之间的连接,必须通过接口或抽象类
- 不能直接从一个具体的类衍生出新的类
- 不能重写已经实现的方法
- 所有的变量实例化必须通过类似于工厂方法或是工厂模式这样的创建模式的实现,或是通过依赖注入框架的使用。
“不能重写已经实现的方法”。在里式替换原则一节已论述得出,其违反里式替换原则,但是不知为何在此违反了依赖倒置原则。
在此给出原文链接:https://en.wikipedia.org/wiki/Dependency_inversion_principle#cite_note-Martin2003-1
5.3 例子
某系统提供一个数据转换模块,可以将来自不同数据源的数据换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本的数据(TextSource),转换后的格式可以是 XML 文件(XMLTransformer),也可以是 XLS 文件(XLSTransformer)等。

由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,那么客户类 MainClass 就需要修改源代码,才能使用新的类。但是,这样违背了开闭原则。现使用依赖倒置原则对其进行重构。

当然根据实际情况,也可以将 AbstractSource 注入到 AbstractStransformer,依赖注入方式有以下三种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 依赖注入是依赖AbstractSource抽象注入的,而不是具体
* DatabaseSource
*
*/
abstract class AbstractStransformer {
private AbstractSource source;
/**
* 构造注入(Constructor Injection):通过构造函数注入实例变量。
*/
public void AbstractStransformer(AbstractSource source){
this.source = source;
}
/**
* 设值注入(Setter Injection):通过Setter方法注入实例变量。
* @param source : the sourceto set
*/
public void setSource(AbstractSource source) {
this.source = source;
}
/**
* 接口注入(Interface Injection):通过接口方法注入实例变量。
* @param source
*/
public void transform(AbstractSource source ) {
source.getSource();
System.out.println("Stransforming ...");
}
}
6. 合成复用原则(CRP)
Composite Reuse Principle(Composition over inheritance):要尽可能使用对象组合,而不是继承关系达到软件复用的目的。
6.1 分析
- 继承复用:实现简单,易于扩展,容易破坏系统封装性;从基类继承来的实现是静态的,不能在运行时发生改变,灵活性较差(“白箱”复用)。合成复用:耦合度低,可以选择性地调用成员对象的接口,可以运行时利用多态实现不同的程序行为(“黑箱”复用)。
- 合成也称为组合。组合与聚合的概念有所区别。
- 组合
- 是一种“强拥有”关系,体现了严格的部分和整体之间的关系,部分与整体的声明周期一致
- 比如,机翼是飞机的一部分,没有了飞机就不存在机翼;比如 Jdk 中的 HashMap 与 Entry。
- 在 UML 类图中,组合关系用实心菱形的实线来表示
- 聚合
- 表示一种“弱拥有”关系,体现的是 A 对象可以包含 B 对象,但 B 对象不是 A 对象的一部分
- 比如:雁群和大雁
- 在 UML 类图中,聚合关系用空心菱形的实线来表示
- 组合
7. 迪米特法则(LoD)
Law of Demeter:系统中的类尽量不要与其他非直接相关的类相互作用,以减少类的耦合度。
7.1 定义
- 每个单元应只与朋友发生作用,而不应与陌生人发生作用。
- 在迪米特法则中,对于一个对象,其朋友包括以下:(1)当前对象本身(this);(2)以参数形式传入到当前对象方法中的对象;(3)当前对象的成员对象;(4)如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;(5)当前对象所创建的对象。(6)一个能被当前对象访问的全局对象
- 迪米特法则就是要求,减少与陌生人的作用。
- 更加正式的定义是:对于对象 O 的方法 a,a 应调用更多地调用对象 O 的朋友的方法 b,而应减少对对象 O 的陌生人的方法 c。
- 直观来说,“x.Method()”是满足迪米特法则的,而“x.y.Method()”是违背迪米特法则的。如果一个面向对象语言使用“.”作为属性和方法的标识符,那么迪米特法则可以归结为一句话:值使用一个“.”
- 例如:一个人想让一只狗走路,它并不会命令狗的腿去走路,而是命令狗去走路。然后,狗自己就会命令自己的腿去走路。
7.2 分析
- 遵循迪米特法则,会使得软件更具可维护性和适应能力。因为对象能更少地依赖于其他对象的内部结构。
- 遵循迪米特发则,会减少一个方法中潜在的可调用的其他方法的数量,有助于减少程序中的 bug。与此同时,迪米特规则,为了传递成员对象的调用,也会增加单个类中的方法数量,因此增加出现 bug 的可能。
8. 总结
在整个软件架构之初,应以开闭原则为设计目标,将依赖倒置原则作为实现手段,去做符合单一职责原则和接口隔离原则的抽象化;在开发过程中,遵循合成复用原则,组成/聚合优于继承;不得不继承时,遵循里式替换原则中对类继承的约束;在实现方法时,遵循迪米特法则,减少与陌生人的关联。