设计模式之禅(一)——六大设计原则
CSDN博客地址:https://blog.csdn.net/wf96390/article/details/89601820
六大设计原则
单一职责原则
英文名称是Single Responsibility Principle,简称是SRP。单一职责原则的定义是:应该有且仅有一个原因引起类的变更。
这个原则存在争议之处在哪里呢?就是对职责的定义,什么是类的职责,以及怎么划分类的职责。我以前的设计是不是有点问题了?不,不是的,不要怀疑自己的技术能力,单一职责原则最难划分的就是职责。一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑,从功能上来说。
单一职责原则有什么好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义;
- 可读性提高,复杂性降低,那当然可读性提高了;
- 可维护性提高,可读性提高,那当然更容易维护了;
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
里氏替换原则
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
- 提高代码的重用性;
- 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
- 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
- 提高产品或项目的开放性。
自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
- 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
- 增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果—大片的代码需要重构。
Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(Liskov Substitution Principle,LSP),什么是里氏替换原则呢?它有两种定义: - 第一种定义,也是最正宗的定义: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层含义。
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆写或实现父类的方法时输出结果可以被缩小
父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类。
依赖倒置原则
依赖倒置原则(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语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。依赖倒置原则在Java语言中的表现就是: - 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。
更加精简的定义就是“面向接口编程”—OOD(Object-Oriented Design,面向对象设计)的精髓之一。
对象的依赖关系有三种方式来传递,如下所示。
- 构造函数传递依赖对象
- Setter方法传递依赖对象
- 接口声明依赖对象
只要遵循以下的几个规则就可以:
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。 - 变量的表面类型尽量是接口或者是抽象类
很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对化了,比如一个工具类,xxxUtils一般是不需要接口或是抽象类的。还有,如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供的一个规范。 - 任何类都不应该从具体类派生
如果一个项目处于开发状态,确实不应该有从具体类派生出子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。特别是负责项目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上都是进行扩展开发,修复行为,通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必去继承最高的基类呢?(当然这种情况尽量发生在不甚了解父类或者无法获得父类代码的情况下。) - 尽量不要覆写基类的方法
如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。 - 结合里氏替换原则使用
接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
接口隔离原则
接口分为两种:
- 实例接口(Object Interface),在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口。比如你定义Person这个类,然后使用Person zhangSan=new Person()产生了一个实例,这个实例要遵从的标准就是Person这个类,Person类就是zhangSan的接口。疑惑?看不懂?不要紧,那是因为让Java语言浸染的时间太长了,只要知道从这个角度来看,Java中的类也是一种接口。
- 类接口(Class Interface),Java中经常使用的interface关键字定义的接口。
主角已经定义清楚了,那什么是隔离呢?它有两种定义,如下所示: - Clients should not be forced to depend upon interfaces that they don’t use.(客户端不应该依赖它不需要的接口。)
- The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)
接口隔离原则是对接口进行规范约束,其包含以下4层含义:
- 接口要尽量小
这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。 - 接口要高内聚
高内聚就是提高接口、类、模块的处理能力,减少对外的交互。 - 定制服务
定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法。 - 接口设计是有限度的
接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度,这个“度”如何来判断呢?根据经验和常识判断,没有一个固化或可测量的标准。
在实践中可以根据以下几个规则来衡量:
- 一个接口只服务于一个子模块或业务逻辑;
- 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
- 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
- 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!
迪米特法则
迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少。迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。
迪米特法则对类的低耦合提出了明确的要求,其包含以下4层含义。
- 只和朋友交流
迪米特法则还有一个英文解释是:Only talk to your immedate friends(只与直接的朋友通信。)什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。 - 朋友间也是有距离的
人和人之间是有距离的,太远关系逐渐疏远,最终形同陌路;太近就相互刺伤。迪米特法则就是对这个距离进行描述,即使是朋友类之间也不能无话不说,无所不知。一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量。 - 是自己的就是自己的
在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中。 - 谨慎使用Serializable
开闭原则
开闭原则的定义:
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
开闭原则是最基础的一个原则,前五章节介绍的原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
可通过以下几个方面来理解其重要性。
- 开闭原则对测试的影响
- 开闭原则可以提高复用性
- 开闭原则可以提高可维护性
- 面向对象开发的要求
把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的),其代表的含义也就是把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖。