设计模式六大原则
1 设计模式的定义
设计模式是一套编码理论,由软件的先辈们总结出的一套可以反复使用的经验——代码组织的方法论。
设计模式是软件行业的经验总结,具有广泛的适应性,无关编程语言,无关业务类型,都可以自由使用设计模式。
设计模式不是工具,而是软件开发的艺术,它能指导你如何:设计一款优秀的框架,编写一段健壮的代码,解决一个复杂的需求。
2 设计模式的目的
设计模式可以提高代码的可重用性和可读性,增强系统的可靠性和可维护性,解决一系列的复杂问题,提高协作效率。
3 设计模式六大原则
3.1 单一职责原则(Single Responsibility Principle, SRP)
3.1.1 单一职责原则的定义
单一职责原则:应该有且只有一个原因引起类的变更。(There should never be more than one reason for a class to change.)
3.1.2 单一职责原则的优势和劣势
- 类的复杂性降低,任何职责实现都有明确清晰的定义。
- 可读性提高。
- 可维护性提高。
- 变更引起的风险降低。
变更是必不可少的,如果接口的单一职责原则做得好,一个接口修改只对相应的实现类有影响,而对其它接口无影响,从而提高系统的可扩展性和可维护性。
3.1.3 单一职责原则最佳实践
- 接口,在设计的时候,一定要做到单一。但是,对于接口的实现类需要根据具体业务多方面考虑。
- 单一职责原则不仅仅适用于接口和类,同时也适用于方法——一个方法尽可能只做一件事情。
总结:单一职责原则:接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
3.2 里氏替换原则(Liskov Substitution Principle, LSP)
3.2.1 里氏替换原则的定义
定义一:如果对每一个类型S的对象o1,都有类型T的对象o2,使得以T定义的所有程序P在所有的对象o1换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义二:所有应用基类的地方必须能透明地使用其子类的对象(只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常。使用者可能根本不需要知道使用的是父类还是子类)。
什么是好的设计模式?没有可以具体衡量的标准!完全可以以你自己的理解为准,你有多了解设计模式,就可能做出多好的设计和写出多优秀的代码。总之,没有最好的设计模式,只有最适合的设计模式。
3.2.2 里氏替换原则的最佳实践
- 在类中调用其他类时,务必使用父类或者接口。如果不能使用父类或者接口,说明类的设计已经违背了LSP原则。
- 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生了“畸变”(Override),则建议断开父子的继承关系,而是通过依赖、聚集或者组合等关系代替继承。
- 覆盖或实现父类的方法时,输入参数可以被放大。即:子类中方法的前置条件(输入参数)必须与超类中被覆写的方法的前置条件相同或者更宽松。
- 覆写或实现父类的方法时,输出结果可以被缩小。
总结:采用里氏替换原则的目的是增强程序的健壮性,版本升级的时候可以保持非常好的兼容性。即使增加了子类,原有的子类还可以继续运行。
在实际的项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。在项目中,采用里氏替换原则时,尽量避免子类的“个性”。一旦子类有“个性”,子类和父类之间的关系就很难调和了:把子类当作父类使用,子类的“个性”会被抹杀;把子类单独作为一个业务来使用,会破坏父子之间的继承关系。
3.3 依赖倒置原则(Dependence Inversion Principle, DIP)
3.3.1 依赖倒置原则定义
模块层级高低:应用层高于抽象层(抽象类、接口),高于具体实现细节。
依赖倒置原则定义:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
在Java语言中,抽象指的是接口或者抽象类,两者都不能直接被实例化。细节,就是具体的实现类,实现接口或者继承抽象类产生的类就是具体细节,可以直接被实例化。依赖倒置原则在Java语言中的表现:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系。所有的依赖关系都是通过接口或者抽象类产生的。
- 接口或者抽象类不能依赖于实现类。
- 实现类依赖接口或者抽象类,不能依赖实现类。
3.3.2 依赖的三种实现方式
-
构造函数传递依赖对象(构造参数注入)
-
Setter方法传递依赖对象(Setter依赖注入)
-
接口声明依赖对象(传入参数,接口注入)
3.3.3 依赖倒置原则最佳实践
依赖倒置原则的本质是通过抽象(接口或者抽象类)使各个类或模块的实现彼此独立,互不影响,从而实现模块间的松耦合。
再具体项目中实现依赖倒置原则,需要遵循以下几个规则:
- 每个类尽量都有接口或者抽象类,或者接口和抽象类两者都具备。这是依赖倒置原则的基本要求,抽象和接口类属于抽象的,有了抽象才能实现依赖倒置。
- 变量的表面类型尽量使接口或者抽象类。特例:如果需要使用类的clone方法,就必须使用实现类(JDK的规范)。
- 任何类都不应该从具体类派生。
- 尽量不要覆写积累的方法。如果基类是一个抽象类,并且某个方法已经实现了,那么子类尽量不要覆写。类间依赖的是抽象,如果覆写了抽象的某个方法,对依赖的稳定性会产生一定影响。
- 结合里氏替换原则使用
接口负责定义public属性和方法,并且声明与其它对象的依赖关系;抽象类负责公共部分的实现;实现类准确实现业务逻辑,同时在适当的时候对父类进行细化。
依赖倒置原则是实现开闭原则的重要途径,也是六大原则中最难以实现的原则。如果依赖倒置原则没有实现,就难以实现开闭原则(对扩展开放,对修改关闭)。
3.4 接口隔离原则(Interface Segregation Principle, ISP)
3.4.1 接口隔离原则定义
定义一:
客户端不应该依赖它不需要的接口。
定义二:
类间的依赖关系应该建立在最小的接口上。
也就是说,应该建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少。
要注意与单一职责原则的区分,两者角度不同。单一职责原则要求类和接口职责单一,注重的是职责,是业务逻辑上的划分;接口隔离原则要求接口的方法尽量少。
3.4.2 接口隔离原则对接口的约束
- 接口要尽量小
同时,根据接口隔离原则拆分接口时,首先必须满足单一职责原则。 - 接口要高内聚
接口中要尽量少公布public方法。接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险越少,有利于降低成本。 - 定制服务
做系统设计时,需要考虑对系统之间或者模块之间的接口采用定制服务。 - 接口设计是有限度的
接口的设计粒度越小,系统越灵活。但是,同时也带来了结构的复杂化,开发难度增加,可维护性降低。因此,在接口设计的时候,要根据经验和常识掌握这个度。
3.4.3 接口隔离原则的最佳实践
接口隔离原则是对接口和类的定义,接口和类尽量使用原子接口或原子类来组装。这个“原子”的划分,可以通过以下几个规则来衡量:
- 一个接口只负责一个子模块或业务逻辑。
- 通过业务逻辑压缩接口中的public方法。
- 已经被污染了的接口,尽量去修改。若变更风险较大,则采用适配器模式进行转化处理。
- 了解应用场景,不要盲从。场景不同,接口拆分的标准就不同。根据应用场景和自己的经验、常识,合理确定接口粒度大小。
3.5 迪米特原则(Law of Demeter, LoD)
3.5.1 迪米特原则定义
一个对象应该对其他对象有最少的了解。因此又称为最少知识原则(Least Knowledge Principle, LKP)。
- 在迪米特原则中,一个类只和朋友类交流。朋友类:出现在成员变量、方法的输入输出参数中的类,称为成员朋友类,而出现在方法内部的类不属于朋友类。
- 如果一个方法放在本类中,既不增加类间关系,也不对本类产生负面影响,就放在本类中。
- 迪米特法则要求类尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private(default)、protected等访问权限。
3.5.2 迪米特原则最佳实践
迪米特原则的核心理念就是:类间解耦,弱耦合,从而提高类的复用率。结果是:产生了大量中转或者跳转类,导致系统复杂性提高,可维护性降低。
使用迪米特原则,要反复权衡,既做到结构清晰,又做到高内聚低耦合。
3.6 开闭原则(Open Closed Principle, OCP)
3.6.1 开闭原则定义
一个软件实体(类、模块和防暑等)应该对扩展开放,对修改关闭。
也就是说,一个软件实体通过扩展来实现变化,而不是通过修改原来的代码来实现变化。开闭原则是对软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
软件实体:
- 项目或软件产品中按照一定的逻辑规则划分的模块
- 抽象或类
- 方法
3.6.2 开闭原则的优势
- 减小对测试的影响(例如,编写的单元测试的代码)
- 提高代码复用性
- 提高可维护性
- 面向对象开发的要求
- 快速响应需求变化
3.6.3 开闭原则最佳实践
- 抽象约束
要实现对扩展开放,首要的前提条件是抽象约束。接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放。抽象约束包含3层含义:
- 约束扩展边界
通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。 - 约束参数类型
应用参数对象尽量使用接口或者抽象类,而不是实现类。 - 抽象层尽量保持稳定
抽象层一旦确定,就不再允许修改。
-
元数据(metadata)控制模块行为
元数据:用来描述环境和数据的数据,也就是配置参数。配置参数可以来自配置文件,也可以来自数据库。
使用元数据来控制程序的行为,可以减少重复开发。 -
制定项目章程(约定优于配置)
团队中应该有所有成员都必须遵守的开发约定。约定可以提高开发效率和沟通效率。 -
封装变化
封装变化有两层含义:
- 将相同的变化封装到一个接口或抽象类中;
- 将不同的变化封装到不同的接口或抽象类中。
封装变化,也就是分户账可能的变化,一旦预测到可能将来会有变化,就要进行封装。23个设计模式就是从各个不同角度对变化进行封装。