设计模式再总结
愿景
话术可能有一些抽象对入门不是特别友好,主要针对已经在后端业务开发过多年,有设计模式应用的意识,对依赖倒置有一些使用经验的同学。
旨在精简每一种设计模式的场景和方法,抛弃教科书式的解释方式,力求言简意赅。反复体悟、备忘,增强设计模式的思想,在开发中做到灵活运用而增强代码易扩展易维护的目的。
1.类之间的关系(uml类图)及耦合强弱比较
依赖(狭义上的,指局部变量、方法的参数或者对静态方法的调用)<关联(聚合、组合)<接口实现=继承泛化(抽象类继承、普通类继承)
广义上的依赖上述都是
2.设计模式的原则
单一原则、接口隔离、依赖(具象到构造函数、setter方法、方法参数传递三种方式)倒置(多态)、里氏替换、开闭原则、迪米特原则、合成复用原则
从uml类关系的角度分析,其实依赖倒置也能称之为关联倒置
3.设计模式的分类及一句话概括
创建型类
3.1.单例模式
使用场景:希望一个类在当前的jvm中只有一个实例
实现方式:静态变量、静态代码块、懒加载线程不安全、懒加载+同步锁、懒加载+同步锁+volatile(推荐3🌟)、内部静态类(推荐4🌟)、枚举(推荐5🌟)
3.2.工厂模式
使用场景:希望一群可以倒置的类集合,可以方便的拿到想要的具体类
实现方法:简单工程、工厂方法、抽象工厂。类倒置,工厂也可以继续倒置。
3.3.原型模式
使用场景:希望快速复刻一个实例的当前状态
实现方法:jvm的cloneable浅复制 或序列化深度复制
3.4.建造者模式
使用场景:希望一个有几种固定的处理方式的类,把处理方式集中到建造类
实现方法:建造类关联一个目标新实例,在建造类中对新实例做处理;建筑类倒置可以选择很多不同的建筑类;记得最后return处理完成的目标实例对象
结构型类
3.5.适配模式(定义规范)
使用场景1:调用的目标方法不支持修改 但又要适配使用者
实现方法:类适配模式、对象适配模式;adapter继承或者关联调用的目标,再新增的方法内使用目标方法并增加自己的适配逻辑;adapter设计成倒置可以选择很多不同的适配类;
使用场景2: 我们在设计之初便想好了接口规范,让使用者可以灵活自己实现匿名内部类来自定义处理逻辑
实现方法:adapter作为抽象类实现定义的接口并空实现,adapter便可以随意重写任意方法(不还是倒置吗,只不过比单纯的倒置多了可选择实现的小技巧)
启发:通过继承抽象类,抽象类空实现接口的方式,可以任意选择重写方法。
3.6.桥接模式(接口的打散重组)
使用场景:在实现一个接口时,发现有部分的方法和具体实现类本身没有必然一对一关系(即不是新增一个实现类就必须重新实现所有新的方法,可能之前有一部分是可以复用的),考虑将这部分方法拆分成新的接口,按需组合。
实现方法:此时新接口的实现类通过关联的方式放入原来的接口,原来的接口就要通过抽象类的方式去倒置和接收新接口能力。如果“这部分接口”是多个集合那可以拆成多个。在某种情况下,这部分接口只有一个集合,且只有base跟other两种场景那么可以直接用抽象类直接完成该需求。
3.7.装饰器模式(递归加包递归拆包)
在说装饰器之前先感受一下类的两种递归。
public class A { private A a; private String name; public A(A a, String name) { this.a = a; this.name = name; } } public class B { private String name; public void setName(String name) { this.name = name; } } public class C extends B { private B b; private String decorate; public C(B b, String decorate) { this.b = b; this.decorate = decorate; } }
public class Test { public static void main(String[] args) { A pathA = new A(new A(new A(new A(null, "小麦"), "爸爸"), "爷爷"), "祖宗"); B b = new B(); b.setName("小麦"); C pathC = new C(new C(new C(new C(b, "学习数学知识"), "学习物理知识"), "学习化学知识"), "学习历史知识"); } }
此时如果A变成了抽象类进行倒置,能带来什么?此时如果B,C变成抽象类,能带来什么?B、C的结构很容易联想到B是一个等待装饰的对象,而C就是装饰的具体内容,C一层一层的装饰B。将B、C改成抽象类倒置之后,能力就能扩展、按需分配
使用场景:所以当一个类,需要对它进行不确定几次递归的信息附件添加时,就非常适合使用装饰模式。或者叫它递归加包模式怎么样?
实现方法:参考上述B、C的代码
3.8.组合模式(统一递归树)
依然还是上述两种递归中的A类,举一个例子,日常开发过程中的数据库部门表的树形关系一般直接用部门包含List<部门>;如果把A类变成抽象类或者接口做倒装处理,那么具有相同类型的类集合就可以统一成树形结构。比如建筑-楼层-区域三张表的关系建筑聚合楼层list 楼层聚合区域list的关系统一成父节点聚合子节点的关系。其实还是依赖倒装。
使用场景:当有一群类,具有相似的属性并存在上下级树形关系
实现方法:抽象一个抽象父类,并聚合自己的集合,间接得省去了对不同层级的策略模式
3.9.外观模式(内聚同层业务类)
使用场景:让类的使用者和被使用者有更明确的上下级分层关系,其实这在平时开发中都无意中用到了,controller-service-dao就是很典型的facade模式
实现方法:将下级的类关联到一起,在这个上级类种统一组装方法的调用,而使用者只要调用这个上级类即可。
3.10.享元模式(实例化对象集合池的管理)
使用场景:对实例化较为消耗资源的类,将它的实例做集合管理,池的概念。至于管理逻辑可以自由定义(1.jvm的String、几种基础包装类的池化技术 2.jdbc链接池 核心思想就是集中管理不需要重复实例化)
实现方法:用一个管理类维护一个类的实例化对象集合
3.11.代理模式(切片编程)
使用场景1:当一个接口已经被一个实现类具体实现,想要对具体实现类的某些方法执行的前、后 织入一些代码
实现方法:新建一个同样实现了这个接口的代理类,组合该实现类,手动的对每一个方法重写,其中就是 前 + 实现类方法 + 后;缺陷:每遇到一次这种情况就要写一个代理类;
使用场景2:类似场景1的接口有很多,不希望写很多的代理类,能根据传入的接口class信息动态生成代理类
实现方法:jdk的newProxyInstance,原理其实就是使用场景1的实现方法,只是根据接口类的反射信息动态帮你生成了代理类;缺陷:类必须要实现某一个接口,也就是说只能代理某接口的某实现类;
使用场景3:想要对任意一个类的方法执行的前、后 织入一些代码
实现方法:cglib包通过继承的关系去做代理,而不是通过接口+组合的关系,没有了接口的限制,但是有了继承的限制,简单来说被代理的对象不能是final不可继承的 方法也不能是final不可重写的。spring aop就是用这个方法来做的,想想日志的切片编程,是不是任意类都能切入。
行为型类
不要被行为型设计模式的名字吓到!不要被行为型设计模式的名字吓到!不要被行为型设计模式的名字吓到!行为型到底还是依赖倒置,只是小小的加了一点继承、聚合、引用。理解以后,根据类的需求关系出发也能直接使用到相关的设计模式,甚至你自己都不知道已经在用了,当然ocp思想在,具体的实现略有差别很正常。
3.12.模版模式(定义接口规范及模版方法)
使用场景:使用场景和适配模式场景2非常的相似,除了定义好规范之外,还要对已定义好的方法做好组合关系,提供模板方法,这就是模版模式,当然这里的倒置用抽象类。
实现方法:定义好方法的抽象类,并在抽象类种定义好方法之间的调用顺序的模版方法。实现者只需要实现方法,调用模版方法即可。
3.13.命令模式(定义命令接口规范)
使用场景:定义命令的接口规范,对一批实现了该接口的命令实现类做统一管理
实现方法:对命令类做依赖倒置
3.14.访问者模式(定义匿名内部类的接口规范并引用当前宿主)
使用场景:使用场景依然和适配模式场景2非常的相似,此时宿主也是继承抽象类的倒置设计,而定义的规范方法需要根据宿主的不同实现随之改变
实现方法:其实还是依赖倒置,将接口或者抽象类通过方法引用的方式注入,并引用当前宿主实现类的对象,即this,然后就可以在匿名内部类操作this的方法!
启发:匿名内部类的构造函数引用宿主实现关联;
3.15.迭代器模式(定义匿名内部类的接口规范并引用当前宿主)
使用场景:有几个类,都聚合了自己的业务对象的集合(或者数组)想要统一这几个类的递归的能力
实现方法:简单的可以将这几个类都实现Iterator接口;优雅的可以再新增一个MyIterator去实现Iterator接口,然后聚合到宿主类,在MyIterator构造函数中加入那些集合(或者数组),这样宿主就只需要聚合自己的MyIterator,接口的实现放在MyIterator,层级更清晰,java.util.collection里的集合也是这么做的。
启发:当类想要实现某些方法而不想在本类种增加代码量时,可以通过聚合新的类,让新的类来实现方法。有点桥接模式的味道。此新类的构造函数可能引用到宿主类下的成员变量,又有点访问者模式的味道。
3.16.观察者模式(定义接受通知的接口规范)
使用场景:当一个类的方法需要遍历调用几个依赖倒置的实现类时,可以通过集合来管理并遍历请求
实现方法:可以继承jdk提供的Observerable类,管理几个实现了Observer的实现类,通知遍历调用实现类的update方法
启发:其实跟命令模式很像,都是管理几个依赖倒置的实现类,只不过业务规则不太一样,管理的逻辑不一样;类之间的关系是一样的。
3.17.中介者模式(定义被调用者的接口规范)
使用场景:当一个类的方法需要有选择的调用几个类时,可以通过集合来管理并请求;
实现方法:将这几个被调用的类做依赖导致继承抽象类,抽象类引入这个类对象,初始化构造函数就放入这个类对象的集合管理中;通过取集合对象强转具体实现类的自定义方法来完成业务调用(一对多);因为聚合的宿主对象,也可以在这些实现类中调用宿主方法,此时可以把这个方法抽象到抽象类中,宿主根据instance判断是谁的请求再做对应的操作(一对一)。
启发:依然还是对依赖导致类的集合管理,这里有一个引入宿主对象,在构造函数时放入宿主读对象内部集合的小技巧,完全可以应用在命令模式、观察者模式中
3.18.备忘录模式
使用场景:当需要记录一个类的历史状态时
实现方法:聚合并管理该类的多个状态的集合
3.19.解释器模式(统一递归树)
使用场景:(((1-2)*3)/(4+5))=? 可以把(left+action_right)当成一个express对象层层递归,其实就是一颗二叉树,叶子节点是数字,父节点是运算符。这里我们的重点不是去理解运算过程的栈的先进后出,递归运算的时候实际上完成了先进后出,因为我们肯定是从节点一步一步向上传递结果的。
实现方法:简单的,通过node类聚合本身实现递归加减乘除,更优雅的,将node做成抽象类聚合本身,然后依赖倒置出加减乘除四个node实现类和纯数字类,将运算逻辑也内聚。
启发:抽象类的意思除了依赖倒置,有时候还是递归的好帮手,装饰器和组合模式也能看得出来。
引申:继续引申,这个树可以不局限于二叉树,通过集合管理left right & other,将运算或者业务规则依赖倒置,递归出结果。哈哈是不是回到了组合模式。action不就是各自的业务策略吗。
3.20.状态模式(定义状态的接口规范)
使用场景:当一个对象的状态机出现较为复杂的调度时,可以引入该设计模式
实现方法:通过依赖倒置并管理每一种状态的实现类,通过引入中间对象context上下文,即包含当前状态,又可以调用当前状态的方法,通过传递context在调用状态方法时再修改当前状态
启发:特殊的实现了抽象类的上下文的并聚合抽象类的小技巧,可以方便的管理currentobject,还能符合定义的接口规范
3.21.策略模式(定义策略的接口规范)
使用场景:依赖倒置就是策略模式
实现方法:单调的,通过组合或聚合一个接口的实现类(适配模式匿名内部类没有什么本质的区别),现实中,一般还会增加一个管理类去分流选择实现类,各个实现类聚合到管理类中去。
3.22.责任链模式(定义处理方法的接口规范)
使用场景:当一个请求要经过特定的处理流程时,需要将处理流程内聚解耦
实现方法:和组合模式一样,将处理流程依赖倒置,父子聚合递归,将请求层层传递处理。
引申:处理流程可以是固定的不需要判断是否扔出给下一个而是全部执行,请求内容也可以不停变化。处理流程也不一定是单链的,父可以聚合实现类集合,判断流转给哪一个处理流程的实现类。
4.总结:
a.通过继承泛化抽象类、实现抽象接口的多态特性实现倒置,通过依赖、关联的方式实现注入; 这里的“倒”指抽象类或者接口跟具体实现类的反向;
b.特别强调,spring的ioc(控制反转)容器可以帮助我们更优雅的实现依赖、关联注入;这里的“反”指的宿主对象和关联注入的对象,从原来的宿主主动new变成了被动接受关联注入对象的反向;
c.在依赖关系中,普通类继承的耦合要大于关联,尽量避免使用普通类继承(不是说倒置场景中的多态抽象类继承);
d.可以灵活使用泛型和反射