前言
本文基于我个人在日常开发过程中对于设计模式的使用,以通俗易懂的表达方式分享我对设计模式七大原则的看法。
个人认为在看Spring源码,或者其他项目源码之前,最好了解一下设计模式,不管是新手还是老手,都有必要学习Java设计模式。拿Spring来说,Spring将设计模式运用得淋漓尽致,比如IOC容器就用了单例模式、工厂模式等,Spring MVC的DispatchServlet采用了委派模式(虽然不是23中的一种)、适配器模式、责任链模式等。看过之后才知道,Spring的代码质量真的高,人家牛逼是有原因的。
什么是设计模式?
设计模式(Design pattern)是众多软件开发人员经过了长时间的试验和错误经验总结得出来的常见问题的解决方案。旨在提高代码的可读性、重用性、可扩展性、健壮性,使程序呈现高耦合、低内聚的特性。
PS:很多人认为,设计模式可读性很差,各种接口,各种父类,继承和实现关系错综复杂。个人觉得,只有在刚开始接触设计模式的时候才会觉得程序难以读懂,如果自己按照设计模式多写几个案例,平时在开发中多使用设计模式的话,代码的可读性还是很高的,总之就一句话,懂设计模式的人都觉得代码很容易读懂。
补充一下,平时使用设计模式的时候,一定要按照设计模式的规范来写,比如适配器模式,一定要以XXXAdaptor来命名,工厂模式,就一定要用XXXFactory命名,养成这样的良好习惯,因为Spring源码中就用到了很多设计模式,人家就写得很规范,值得学习!值得学习!值得学习!重要的事情说三遍。比如给你举个例子,看下面的代码,内容不要求你看,如果想看的话,代码来源(org.springframework.web.servlet.handler包中的AbstractHandlerMapper.class中的 getHandlerExecutionChain方法),只看方法名,getHandlerExecutionChain,关注最后一个单词,“Chain”是链的意思,结合设计模式,第一时间就想到的是责任链模式,责任链模式当中,每个实现类之间是通过next指向下一个实现类的,所以代码体中才会出现.next()方法,就是调用下一个Handler处理。
所以,一定要学会按照规范来写,不要自己乱命名,否则,代码就真的难读了。
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = handler instanceof HandlerExecutionChain ? (HandlerExecutionChain)handler : new HandlerExecutionChain(handler);
Iterator var4 = this.adaptedInterceptors.iterator();
while(var4.hasNext()) {
HandlerInterceptor interceptor = (HandlerInterceptor)var4.next();
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor)interceptor;
if (mappedInterceptor.matches(request)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
设计模式常用七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
原则详细介绍
1)单一职责原则
我觉得这个原则是最不值得介绍的,见名知意,单一职责,一个类就只负责一件事情。其实从另一个方面讲,也是为了高内聚考虑。
2)接口隔离原则
一个类不应该依赖它不需要的接口。
怎么说呢,举个例子,这里有一个接口A,里面有方法1,方法2,方法3,方法4共四个抽象方法。有两个类B和C,B需要A当中的方法1和方法2,C需要A当中的方法3和方法4,那么B和C都要去实现接口A,然后问题就来了。
B只需要方法1和方法2,但是B要实现A接口里面的全部四个方法,那多余的两个方法是不是就冗余了。同理C也是这样,方法1和方法2对于C来说就是冗余的方法,有点多此一举的意思。
这时候就应该采用接口隔离,解决方案就是,写两个接口,接口A1只有方法1和方法2,接口A2只有方法3和方法4,这样B直接去实现A1接口,C直接去实现A2接口,这样就不会出现要实现与自己无关方法的情况了。
我觉得接口隔离的本质就是拆分接口。在Java中,@FunctionalInterface注解标识的接口就有点这个意思,一个接口里面就一个方法,你要用到,就实现这个接口,而且还有点单一职责原则的意思,一个接口就只负责一个功能。
3)依赖倒转(倒置)原则
1)高层模块不应该依赖低层模块,二者都应该依赖其抽象。
2)抽象不应该依赖细节,细节应该依赖抽象。
上面这两句话是不是听起来很抽象,其实归根结底,就是大家经常听说的一句话:面向接口或抽象类编程。
接口/抽象类就是为了制定规范,具体的任务细节交给子类去实现。也就是领导喊一句口号,然后下属去自己想办法怎么执行。
模板方法模式这方面就体现得很明显,我方法中的具体流程,一共分几步我都给你写好了,至于每一步你怎么做,交给子类去实现。
4)里氏替换原则
里氏替换原则是1988年,麻省理工的一位姓里的女士提出的…
如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类对象。
上面这句话是不是没有看懂,很正常,我第一遍也没有看懂。其实里氏替换原则是针对具有继承关系的类而言的,总结为一句话:子类尽量不要去重写父类的方法。
因为有时候父类中的方法是不希望子类去重写的,希望所有子类都默认使用父类中的这个方法。一旦这个方法需要修改的时候,就可以直接在父类中修改。如果有子类对这个方法重写了,那么父类在修改的时候,就要对重写了该方法的子类进行考虑,就很复杂。其实在设计父类的时候,如果不希望子类继承的话,可以采用方法加final的方式拒绝子类重写。父类的设计还是得需要长时间的开发经验的积累。
其实继承会使两个类的耦合度增强,在适当的情况下,可以采用依赖、聚合、组合的方式来解决(这三种什么意思,可以参考UML建模中的概念)。
5)迪米特法则
1)一个对象应该对其他对象保持最少的了解。
2)类和类关系越密切,耦合度就越高。
3)迪米特法则又叫最少知道原则,即一个类对于自己依赖的类知道的越少越好,对于被依赖的类而言,不管被依赖的类有多么复杂,被依赖的类对外只提供一个public方法,不对外泄露任何信息。
4)迪米特法则还有个更简单的定义:值与直接的朋友通信。
对于最后一个定义,什么是直接的朋友?直接的朋友就是方法的成员变量、方法的参数、方法的返回值中的类。方法中的局部变量对应的类不是直接朋友,尽量不要在方法的局部变量使用非直接朋友的类。
举个例子,在开发SSM项目的时候,Controller层通常要去调用Service层中的方法,这时Controller就依赖了Service,Service层就是被依赖类。Service用来处理业务逻辑,里面充满了各种复杂的逻辑判断,但是对于Controller而言,我只需要调用Service给我提供的一个方法即可,至于你怎么实现的,我Controller不需要知道。
PS:迪米特法则的主要目的是为了降低类之间的耦合关系,只是降低了耦合关系,并不是完全不依赖。
6)合成复用原则
在4)里氏替换原则中我提到过,采用聚合和组合的方式可以适当减少继承关系的发生。其实这就是合成复用原则。
举个组合的例子,比如一辆汽车,可以定义一个Car类,汽车需要引擎,可以定义一个Engine类,需要轮子,可以定义一个Wheel类。可以在Car类中定义两个成员变量,比如“private Engine engine = new Engine();”和“private Wheel wheel = new Wheel();”,然后可以使用engine和wheel中的方法。而不是让Car类继承/实现Engine类/接口或者Wheel类/接口。
7)开闭原则
1)开闭原则是设计模式最基础、最重要的原则。
2)一个软件实体(例如类),模块和函数应该对扩展方开放(服务提供方),对修改方关闭(服务使用方)。用抽象构建框架,用实现扩展细节。
3)当软件的需求发生变化时,尽量通过扩展软件实体的行为来实现修改,而不是修改已有的代码来实现。
4)前面六种原则的最终目的就是实现开闭原则。
个人认为,开闭原则是其他设计原则和设计模式的最终目标。通俗地讲,服务提供方如果对功能进行了扩展和修改,那么对于服务使用方而言,服务使用方尽量不要出现修改代码的情况。
举个现实一点的例子,就是,如果你在使用一个第三方的类库,你用到了其中的一个方法。后来这个第三方类库更新了,你之前的方法不能用了,那么你就要重新修改代码,修改代码可是一个十分复杂的过程。这种情况就不符合开闭原则。