设计模式的六大准则
- 单一职责原则
单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。
单一职责的好处:
Ⅰ.类的复杂性降低,实现什么职责都有清晰明确的定义;
Ⅱ.可读性提高,复杂性降低,那当然可读性提高了;
Ⅲ.可维护性提高,可读性提高,那当然就更好维护了;
Ⅳ.变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的拓展性、维护性都有非常大的帮助。
单一职责原则最难划分的就是职责。一个职责一个接口,但是如何去划分这个职责呢?一个类需要负责哪些职责?这些都需要我们从实际的项目去考虑(项目工期、成本、人员技术水平、硬件情况等)。
- 里氏替换原则
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有以下优点:
Ⅰ.代码共享,减少创建类的工作量,每个子类都拥有父类的属性和方法;
Ⅱ.提高代码的复用性;
Ⅲ.子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠的儿子回打洞”是说子类拥有父类的种,“世界上没有完全相同的两片叶子”是指子类与父类的不同;
Ⅳ.提高代码得可拓展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来实现的;
Ⅴ.提高产品或项目的开放性。
自然界的事物都是优点和缺点并存的,继承的缺点如下:
Ⅰ.继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
Ⅱ.降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界里多了约束。
Ⅲ.增强了耦合性。当父类的常量、变量和方法被修改,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
Java使用extends关键字来实现继承,它采用了单一继承的法则。从整体上来看,利大于弊,如何让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?这个时候就引入了里氏替换原则(Liskov Substitution Principle, LSP),那么什么是里氏替换原则?它有两种定义:
第一种定义,也是最正宗的定义:如果一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
第二种定义:所有引用基类的地方必须能透明的使用其子类对象。
第二个定义是最清晰明确的,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何的错误和异常,使用者可能根本就不知道是父类还是子类。但是,反过来就不行了,有子类的地方,父类未必就能适应。
里氏替换原则为良好的继承定义了一个规范,一句话简单的定义包含了4层含义。
1.子类必须完全实现父类的方法。(如果子类不能完整的实现父类的方法,或者父类的某些方法在子类发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。)
子类方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。
覆写或实现父类的方法时输出结果可以被缩小。
- 依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP),它包含三层含义:
Ⅰ.高层模块不应该依赖低层模块,两者都应该依赖其抽象;
Ⅱ.抽象不应该依赖细节
Ⅲ.细节应该依赖抽象
依赖倒置原则在JAVA语言中的表现就是:
Ⅰ.模块间的依赖通过抽象发生,实体类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象产生的;
Ⅱ.接口或抽象类不依赖于实现类;
Ⅲ.实现类依赖接口或抽象类。
更加精简的定义就是“面向接口编程”——OOD的精髓之一。
依赖的三种写法:
Ⅰ.构造函数传递依赖对象
Ⅱ.Setter方法传递依赖对象
Ⅲ.接口声明依赖对象
依赖倒置可以减少类间的耦合性,提高系统的稳定性,降低并行开发的风险,提高代码的可读性和可维护性。
项目中怎么使用这个规则呢?
Ⅰ.每个类尽量都有接口或抽象类,或者两者都具备,接口和抽象类都属于抽象的,有了抽象才可能依赖倒置。
Ⅱ.变量的表面类型尽量是接口或者抽象类(不是绝对的,有些工具类XXXUtils一般是不需要抽象类的)。
Ⅲ.任何类都不应该从具体类派生。
Ⅳ.尽量不要覆写基类的方法。
Ⅴ.结合里氏替换原则使用。
- 接口隔离原则
什么是接口?接口分为两种:
Ⅰ.实例接口,在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口。
Ⅱ.类接口,Java中经常使用的interface关键字定义的接口。
什么是隔离呢?它有两种定义:
Ⅰ.客户端不应该依赖它不需要的接口。
Ⅱ.类间的依赖关系应该建立在最小的接口上。
建立单一接口,不要建立臃肿庞大的接口。再通俗的讲:接口尽量细化,同时接口中的方法尽量少。
接口隔离原则是对接口进行规范约束,其包含一下四层含义:
Ⅰ.接口要尽量小
根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
Ⅱ.接口要高内聚
提高接口、类、模块的处理能力,减少对外的交互。
Ⅲ.定制服务
一个系统或系统内的模块之间必然会耦合,有耦合就要有相互访问的接口,
我们设计时就需要为各个访问者定制服务。
Ⅳ.接口设计是有限度的
接口设计的粒度越小,系统就越灵活,这是个不争的事实。但是,灵活的同时也带来了结构的复杂,
开发难度增加,可维护性降低,所以接口设计一定要注意适度。
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装,我们怎么去划分这个原子呢?在实践中可以根据以下几个规则来衡量:
Ⅰ.一个接口只服务于一个字模块或业务逻辑;
Ⅱ.通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
Ⅲ.已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
Ⅳ.了解环境,拒绝盲从。每个项目和产品都有特定的环境因素,环境不同,接口拆分的标准就不同。
深入了解业务的逻辑,最好的接口设计要出于自己!
根据经验和常识决定接口的粒度大小,接口力度太小,导致接口剧增,开发人员呛死在接口的海洋里;接口力度太大,灵活性降低,无法提供定制服务,给项目整体带来无法预料的风险。
- 迪米特法则
迪米特法则(Law of Demeter,LoD),也称为最少知道原则(Least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗的讲,一个类对自己耦合或调用的类知道的最少,你的内部怎么实现,多么复杂都和我没关系,我只要知道你是干什么的,你的职责是什么就行(public方法)。
Ⅰ.类与类之间的关系是建立在类间的,而不是方法间,因此一个方法不要引入一个类中不存在的对象,
当然,JDK API提供的类除外。
Ⅱ.尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、protected
等访问权限。
Ⅲ.如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放在本类中。
Ⅳ.谨慎使用Serializable
迪米特法则的核心就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或者跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
- 开闭原则
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
开闭原则的定义已经非常明确地告诉我们:软件实体应该对扩展开放,对修改关闭,其含义就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
什么是软件实体呢?软件实体包含以下几个部分:
Ⅰ.项目或软件产品中按照一定的逻辑规则划分的模块;
Ⅱ.抽象和类;
Ⅲ.方法。
一个软件只要在生命期内,都会发生变化,既然变化是一个既定的事实,我们应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应该尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
开闭原则的优点:
1.开闭原则对测试的影响:新增加的类,新增加的测试方法,只要保证新增类是正确的就可以了。
2.开闭原则可以提高复用性:减少代码量,避免相同的逻辑分散在多个角落。缩小逻辑粒度,
直到一个逻辑不可再拆分为止。
3.开闭原则可以提高可维护性:相比于去修改原来的代码,维护人员更乐意去扩展一个类。
4面向对象开发的要求:在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
如何使用开闭原则:
Ⅰ.抽象约束
通过接口或抽象类4可以约束一组可能变化的行为,并且实现对扩展开放,其包含三层含义:
第一,通过接口或抽象约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
第三,抽象层尽量保持稳定,一旦确定即不允许修改。
Ⅱ.元数据控制模块行为
尽量使用元数据来控制程序的行为,减少重复开发。
通过一个元数据控制模块行为的例子,其中达到极致的就是控制反转,使用最多的就是Spring容器。
通过扩展一个子类,修改配置文件,完成了业务变化,这也是采用框架的好处。
Ⅲ.制定项目章程
建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,
约定大于配置。
Ⅳ.封装变化
第一,将相同的变化封装到一个接口或抽象类中;
第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同变化出现在统一接口或抽象类。