面向对象设计的六大原则
1. 问题
软件的复用性和可维护性对于提高软件的开发效率、质量,节约开发成本有着至关重要的作用。
软件大师 Robert C.Martin 认为一个可维护性较低的软件设计,通常由于如下四个原因造成:
- 过于僵硬(Rigidity)
- 过于脆弱(Fragility)
- 复用率低(Immobility)
- 黏度过高(Viscosity)
软件工程和建模大师 Peter Coad 认为,一个好的系统设计应该具备如下三个性质:
- 可扩展性(Extensibility)
- 灵活性(Flexibility)
- 可插入性(Pluggability)
现实是,在软件开发过程中,我们经常会碰到如下问题:
- 架构无法适应业务的发展,架构的生命周期短
- 内聚和耦合的标准是什么
- 如何提高模块复用性
- 如何实现高可扩展性和易维护性
如何解决这些问题呢?在面向对象的设计时,有一些基本原则,遵循这些设计原则可以有效地提高系统的复用性和可维护性。
2. 概述
常用的面向对象设计原则包括6个,这些原则并不是孤立存在的,它们相互依赖,相互补充。
| 原则 | 说明 | 重要性 |
|---|---|---|
| 单一职责原则(Single Responsibility Principle, SRP) | 类的职责要单一,不能将太多的职责放在一个类中。 | ★★★★☆ |
| 里氏代换原则(Liskov Substitution Principle, LSP) | 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。 | ★★★★☆ |
| 依赖倒置原则(Dependency Inversion Principle, DIP) | 要针对抽象层编程,而不要针对具体类编程。 | ★★★★★ |
| 接口隔离原则(Interface Segregation Principle, ISP) | 使用多个专门的接口来取代一个统一的接口。 | ★★☆☆☆ |
| 迪米特法则(Law of Demeter, LoD) | 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。 | ★★★☆☆ |
| 开闭原则(Open-Closed Principle, OCP) | 软件实体对扩展开放,对修改关闭 | ★★★★★ |
3. 单一职责原则
定义:一个类只负责一个功能领域中的相应职责。对一个类而言,应该仅有一个引起它变化的原因。
分析:
一个类承担的职责越多,它被复用的可能性越小。
类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
如果一个类承担的职责过多,相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
单一职责原则是实现高内聚、低耦合的指导原则
简单但又难以运用的原则,需要设计人员发现并分享类的不同职责。
好处:
- 降低类的复杂性。因为类的职责明确
- 提高可读性
- 提高可维护性
- 降低变更引起的风险
4. 里氏代换原则
定义一:如果对每一个类型为S的对象 o1,都有类型为T的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有变化,那么类型 S 是类型 T 的子类型。
定义二:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
分析:
- 用子类替换基类,程序将正常运行
- 里氏代换原则是实现开闭原则的重要方式
- 由于使用基类对象的地方都可以使用子类对象,因此应尽量使用基类类型来定义对象,而在运行时再用子类对象来替换基类对象。
5. 依赖倒置原则
定义一:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
定义二:要针对接口编程,不要针对实现编程
分析:
- 代码依赖于抽象的类,而不要依赖于具体的类
- 针对接口或抽象类编程,而不是针对具体类编程
- 依赖倒置原则是面向对象设计的主要手段
- 常用实现方式是: 在代码中使用抽象类,通过配置文件使用具体类
6. 接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
分析
接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。
使用此原则时,需要注意以下问题:
1)接口尽量小,但是要有限度。
如果接口过于粗化,则达不到隔离的原则;如果接口过于细化,则会造成接口过多,设计过于复杂。
2) 一个接口仅仅提供客户端需要的行为(方法),隐藏它不需要的方法。
3)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
7. 迪米特法则
迪米特法则又称为最少知识原则(Least Knowledge Principle,LKP)。
定义:一个对象应该对其他对象保持最少的了解。
分析
- 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
- 采用迪米特法则,当一个模块修改时,就会尽量少的影响其它模块,扩展会相对容易
- 迪米特法则是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度
使用
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
虽然使用迪米特法则时,由于每个类都减少了不必要的依赖,的确可以降低耦合关系。但是过分地使用迪米特法则,会产生大量的中介和传递类,导致系统复杂度变大。
所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
8. 开闭原则
定义:Software entities like classes, modules and functions should be open for extension but closed for modifications
软件实体应当对扩展开放,对修改关闭。
分析
- 开闭原则主要含义:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 抽象化是开闭原则的关键。
- 开闭原则是一个务虚的原则,是面向对象设计中最基础的设计原则,是其它原则的总纲。
绝大部分的设计模式都符合开闭原则,在对每一个模式进行优缺点评价时都会以开闭原则作为一个重要的评价依据,以判断基于该模式设计的系统是否具备良好的灵活性和可扩展性。
9. 总结
六大原则相互依赖,相互补充,指导我们用抽象构建框架,用实现扩展细节。
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合;开闭原则告诉我们要对扩展开放,对修改关闭。
对这六个原则的遵守并不是是和否的问题,而是多和少的问题。我们需要在实际应用中做一些权衡,根据实际情况灵活运用。
过度使用原则,并不达到预期的效果。一个良好的设计通常是在合理的范围遵守这些原则。
10. Reference
- 设计模式六大原则, zhengzhb, 2012/11/2, http://www.uml.org.cn/sjms/201211023.asp