设计模式
设计模式是对一些常见问题解决方案的总结与归纳,设计模式需要满足6大原则,分别是:
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里氏替换原则
- 开放封闭原则
- 迪米特法则
- 合成复用原则
其中其他原则都是为了开放封闭原则。
创建型模式
单例模式Singleton
为什么要有单例模式,因为某些类只需要一个实例对象,如果创建太多对象,可能造成资源浪费,程序行为异常,增加GC压力等。那么问题来了,静态变量随类加载,也可以保证只有一个实例,为什么不用静态变量呢?涉及lazy load的思想,如果这个静态变量没有被用到就将其加载到了内存中,比较浪费内存空间,如果使用实例对象,可以在使用到这个单例对象时才将其加载到内存。
应用于各种Mgr与Factory
单例模式的需求为一个类只允许产生一个对象,实现的方式有饿汉式,懒汉式与嵌套类式。
饿汉式
1 | public class Singleton1{ |
基本思路为直接创建好一个私有的静态的对象,私有构造函数,创建一个public的静态方法去返回此对象(静态方法只能访问静态成员)。这种方法的优点是线程安全,缺点为如果不用到getInstance()方法,仍然会创建一个对象,增加开销。不过一般推荐使用。
懒汉式
1 | public class Singleton2{ |
基本思路也为创建一个私有静态对象,但一开始不初始,私有构造函数,创建public的静态方法返回此对象,需要加上双重验证,加synchronized是为了保证线程安全,外面加上一层判断是为了提高效率,其中因为new这个语句不是原子性的,为了避免语句重排,出现s没有被初始化就返回的情况,需要让instance被volatile修饰,利用内存屏障保证不重排语句。优点是节约了资源,缺点是写法比较复杂,写错了容易线程不安全。
静态内部类
1 | public class Singleton3{ |
在饿汉式基础上将对象构造放进内部类中,这样不调用内部类的时候不新建对象。
基本思路为私有构造方法,定义一个私有的静态类,避免其他类访问,直接类名调用,此类中持有外部类的私有静态实例,外部类提供方法,返回内部类中的对象。这样静态类只在被调用时加载一次,因此只有一个对象。优点是不使用getInstance方法不实例化,节约资源。缺点是第一次加载比较慢。
枚举类
1 | public enum Singleton4 { |
写法简单,调用方便。
JDK中用到的单例模式为:Runtime类,使用getRuntime(),使用的饿汉式。
1 | public class Runtime { |
策略模式
对于普通的排序,只能排数字,如果想要对多种类型进行排序,方法传入的参数需要为Compareble[]类型,然后比较的时候,调用数组的compareTo[]方法即可。但是这样依旧不够灵活,因为传入的类必须覆写compareTo()方法,更灵活的是让排序的策略可以灵活指定。定义比较器,实现Comparotor接口,实现compare()方法。
一般策略模式,需要有一个策略接口,如Comparator接口,策略接口可以有多个实现,策略模式封装的是做一件事不同的执行方式。
工厂模式Factory
任何可以产生对象的方法或类,都可以称之为工厂,单例也是一种工厂。
应用:Spring IOC
简单工厂
需求:需要使用不同的交通工具,并且调用他们的go方法。
定义一个交通工具的接口Moveable,定义go方法,让所有交通工具都要实现此接口。为了统一生产交通工具,定义一个简单工厂,里面写上不同的get方法来获取不同的交通工具对象,在Main方法中,通过定义接口类型的Module类,指向工厂方法获取的不同对象,以多态来获取不同对象的方法。
Main中
1 | public static void main(String[] args) { |
简单工厂的方法
1 | public class SimpleVehicleFactory { |
抽象工厂
需求:需要生产一系列产品,如需要生产食品,武器,交通工具。先定义生产的产品的抽象类,然后抽象工厂新建方法返回对应的抽象类。抽象类中只有一个方法,只有一个方法但是用抽象类,是因为食品这些本身是一个具体的概念,是名词。
1 | public abstract class AbstractFactory { |
然后如果是工厂1生产自己系列的产品,如面包,手枪,汽车,让产品继承自对应的抽象类,然后工厂1继承抽象工厂,返回自己生产的类即可。
1 | public class ModernFactory extends AbstractFactory { |
在Main方法中,持有抽象工厂的引用,具体的对象实例可以是继承了抽象工厂的那些工厂,然后调用工厂的对应方法即可以获取到对应的内容。
1 | public class Main { |
抽象工厂方便于产品族的设计。
建造模式Builder
构建复杂对象
- 分离复杂对象的构建和表示
- 同样的创建过程可以创建不同的表示
- 不同记忆,自然使用
如果地形类中有墙,暗堡,地雷,如下
1 | public class Terrain { |
如果想构建不同的地形,可以使用不同的构建器,有一个构建地形的接口,其中有构建墙、暗堡、地雷的方法,当构建完成后,返回一个地形对象。
1 | public interface TerrainBuilder { |
对于一个具体的实现类
1 | public class ComplexTerrainBuilder implements TerrainBuilder { |
聚合一个地形对象,当调用不同的构建方法时对地形对象分批进行构建,返回this即当前构建器对象,最后返回聚合的地形对象。
其中返回一个构建器的接口是因为方便链式编程。在main中调用时,使用链式编程调用非常方便
1 | TerrainBuilder builder = new ComplexTerrainBuilder(); |
在Java中应用于属性非常多的对象的构建,如果一个对象有些属性是必须的,但一些属性可有可无,如果每次构建对象 都要传入许多参数,则非常麻烦,可以私有类的构造器,在其内部有一个静态内部类,由静态内部类来创建对象,并对外提供对外部类对象构造的方法。
可以看到,下面的类私有了Person对象的构造方法,这样想要新建一个类,只能使用其静态内部类Builder,有基础信息的构建,也有可选信息的构建
1 | public class Person { |
在main中调用时,如果不想构建哪个属性,直接将其注释掉即可,这样构建非常方便。
1 | Person p = new Person.PersonBuilder() |
总结:用于被构建类非常复杂的情况,构建工具类聚合一个被构建类对象,有返回构建工具类的不同构建方法,便于链式编程,最后有一个返回返回被构建类对象的方法。
原型模式Prototype
又叫克隆模式,Object.clone()
一个类想要被克隆,需要
实现Cloneable标记型接口(其中没有方法)
若不实现接口,编译不出错,调用报异常
被克隆的类需要重写clone方法
因为Object类中的clone方法为native
一般用于一个对象的属性已经确定,需要产生很多相同对象的时候
克隆的只是基本属性,如果被克隆的类中有其他对象的引用,则拷贝的只是此对象的引用。因此是浅克隆。
在main中输出,p与p2的Location对象地址相同。
1 | public class Person implements Cloneable{ |
为了实现深克隆,需要让Location也实现Cloneable接口。
1 | class Location implements Cloneable{ |
而在Person类中,将location单独clone
1 |
|
Location中的String类型的属性不用进行深克隆,因为字符串在常量池中,String类引用不可变。若使用的是StringBuilder,则Location中新老对象使用的同一个StringBuilder引用。那么深克隆时候也需要把StringBuilder进行深克隆。
结构型模式
装饰模式Decorator
IO流中的Writer,有人说是装饰。如果要对一个类的功能进行装饰,即增加一些功能,要是使用继承,会产生类爆炸的现象,那么使用一个抽象类继承装饰抽象类,需要持有一个装饰类对象,在装饰类的实现类中,调用被装饰类的自己的方法,然后加入一些字自己的方法。
门面模式Facade
许多类之间有复杂的联系,如果Main类中去跟每个类交互,逻辑非常复杂。而使用一个类,封装所有的类之间的逻辑,对外只暴露一些接口,相当于一个门面。
享元模式Flyweight
共享元数据,将小对象放入池子中,需要用到的时候,从池子中取即可。
组合模式Composite
树状结构专用模式。
将Node组合到Branches类中。
如有节点的抽象类,其中有方法p(),用来打印当前的节点值
1 | abstract class Node{ |
有叶子节点,直接打印值
1 | class LeafNode extends Node{ |
如果是枝干节点,则下面有枝干或者叶子,因此持有一个Node的集合,对外提供将Node加入集合的方法。
1 | class Branches extends Node{ |
在遍历树的时候,传入一个节点Node,传入一个当前的高度(便于去打印树的深度)
使用递归的写法,base case为节点为空,直接返回;打印当前高度,打印当前节点。如果当前节点是枝干节点,获取当前枝干节点的节点集合,然后对集合中的所有节点递归调用遍历函数。
1 | //遍历树 |
代理模式Proxy
代理的含义是比如去买酒,不需要自己去找酒的生产商,而是找到其代理人即可。
静态代理。
有一个可移动的接口,其中有move方法,坦克类实现此接口并覆写方法,为了记录坦克类的运行时间,可以使用继承,但是这样非常臃肿而且不利于功能的复用,比如要记录时间,日志,时间与日志,就需要3个继承类。这时候使用静态的代理,一个时间代理类继承Moveable接口,然后聚合一个Moveable,在其move方法中,增加自己功能,再调用持有的Moveable的move方法。因为此代理也是一个Moveable,因此是可以被其他的代理类所代理的,这样增加功能非常方便,静态代理有点类似于装饰器模式。
1 | interface Moveable{ |
动态代理
想要日志代理不仅可以代理可以移动的,还可以代理任意类型的。本质是分离代理行为与被代理对象。因为不知道被代理的类是什么类型,其中有什么方法,需要使用jdk的动态代理。
动态代理不是改变原来类的代码,而是生成一个新的代理类。
仍然定义一个Moveable接口,坦克类实现此接口。为了获得代理类,使用Proxy的newProxyInstance方法获得,传入3个参数,类加载器,接口的Class数组及被调用时的处理器,即调用被代理对象的方法时该如何做。类加载器写被被代理类的加载器。
1 | interface Moveable{ |
可以看到生成代理对象后,调用其move方法,可以获取如下输出
1 | method move start |
但是开始与结束的逻辑写在了InvocationHandler里面的invoke方法,调用move方法时却调用到了invoke方法。原理是生成的动态代理类中,因为其要实现Moveable接口,因此实现了move方法,在此方法中,调用了传入的InvocationHandler的invoke方法。
其中对于invoke方法,通过method可以拿到其方法名,对应不同的方法可以有不同的方法对应。args为往方法里传递的参数,proxy为生成的代理对象m,被代理对象被调用事件是method.invoke,传哪个引用相当于调用哪个引用的方法,method.invoke(tank,args)相当于对坦克对象调用move方法,返回类型与传入引用被调用方法的返回类型相同,此处move返回的是空值,那么这里返回的object也为空。
总的来说,调用的逻辑是,当调用生成的m对象的move方法,其实调用的是传入的InvocationHandler的invoke方法,在其中有自己写的代理的方法,当要使用被代理类的方法时,使用method.invoke,传入被代理对象的引用与参数,返回其原方法的返回值,其实调用的是Tank类的move方法。
而生成动态代理类的过程,底层是使用的asm来实现的,其为二进制字节码操作类库。因此不管用什么语言写,只要能生成二进制字节码文件,就可以在JVM运行,如scala,kotlin等。
JDK反射的动态代理必须面对接口。通过接口来指定代理类生成的接口,否则不知道需要生成哪些方法。
而如果使用CGLIB方式(Code Generation Library)生成,不要求被代理类实现接口,生成的动态代理类为被代理类的子类,因此如果被代理的类被final修饰,是无法使用这种方式来动态代理的。cglib底层也用的asm。
1 | public class Main { |
动态代理可以对所有类型的对象执行一定的功能。可以在原来的功能上切入自己定义的功能,不用更改原来类的代码,叫做AOP,Aspect Oriented Programming,面向切面编程,使用的CGLIB的方式。
切面是往哪里加代码的集合。
Spring的AOP配置,需要maven导包org.aspectj
方式一:配置文件配置
1 | <bean id="timeProxy" class="com.zc.dp.spring.v1.TimeProxy">bean> |
pointcut代表在哪个点且,此处为执行Tank的move()方法时进行切面,然后指定在此切面前面执行before方法,后面执行after方法,去ref指定的id为timeProxy的类中去寻找这两个方法,如果想执行且多个方法或者多个类,可以使用.*的方式。这样如果想添加新的功能,非常方便。
方式二:注解配置
在要切的类上加入@Aspect注解,在要之前与之后执行的方法前加入@Before与@After注解。比起配置文件实现更方便。
1 |
|
当Tank被final修饰时,报错
1 | Could not generate CGLIB subclass of class com.zc.dp.spring.v2.Tank: Common causes of this problem include using a final class or a non-visible class; |
适配器模式Adapter(Wrapper)
接口转换器,一个类不能直接访问另外一个类,中间加一个转换。
电压转接头
java.io
BufferedReader,将字节流转为字符流
JDBC转为ODBC,再访问SQL Server,即JDBC与ODBC的Bridge
ASM Transformer
Reader直接传给Writer,相当于复制,这时候如果多加一层Adapter,可以自定义一些功能,也可以叫做适配器。
误区:常见的Adapter类反而不是Adapter,如WindowAdapter,KeyAdapter
在awt中,如果想要监听窗口,直接new WindowListner接口,需要重写6个方法,但很多时候只关心其中几个方法,这时候,WindowAdapter是一个抽象类,实现WindowListner,将全部方法实现为空,这时候继承WindowAdapter只重写关心的方法即可。
常见的带Adapter的类只是为了方便编程而已。
桥接模式Bridge
双维度扩展
- 分离抽象与具体
- 用聚合方式(桥)连接抽象与具体
抽象类的树与具体类的树分别发展,但是在抽象类中聚合一个实现类。
需求:礼物Gift有礼物的实现类GiftImlpl,而礼物下有分支温暖的,冷酷的礼物等。而礼物的实现类GiftImlpl下有花,书等。如果子类使用继承,比如一个温暖的花,冷酷的花,可以有多种继承方式,这时候会产生类爆炸。
如Gift抽象类在自己发展,其实现类GiftImlpl也在发展,这时候在Gift类中聚合一个GiftImlpl。就是用聚合代替继承。
对于Gift抽象类
1 | public abstract class Gift { |
对于其具体类GiftImlpl
1 | public class GiftImpl { |
比如Gift下有WarmGift,需要传入一个礼物的实现类
1 | public class WarmGift extends Gift { |
在具体应用时,将具体的实现类传入抽象类即可。
1 | public void chase(MM mm) { |
行为型模式
策略模式Strategy
对做同一件事情,有不同的策略。如坦克的开火,可以有普通的开火,也可以四个方向开火。
1 | interface Fire{ |
在main中持有Fire,传入不同的Fire实现类,有不同的策略实现。
责任链模式COR
场景:对字符串进行敏感词过滤操作。如有消息类Msg,其字符串中包含敏感词
其中尖括号中的内容可能会被直接访问,而996算敏感词,网站需要对这两种情况进行过滤。
定义一个Filter接口,定义抽象方法doFilter,需要传入消息类对象。
1 | interface Filer{ |
因为可能有多种敏感词过滤的情况,因此定义多个类,分别实现Filter接口,来覆写自己的doFilter方法。
1 | class HTMLFilter implements Filer{ |
为了更好封装多个Filter,定义一个FilterChain,可以添加Filter与获取所有的Filter。其中add方法返回的是当前的FilterChain对象,便于链式编程
1 | class FilterChain{ |
然后在主方法中,新建FilterChain对象,添加对应的Filter,调用fc的doFilter方法即可。
1 | public static void main(String[] args) { |
整个过滤逻辑如下图所示,每个Filter有自己的责任,串成了链条的样子。
但是当需要有另外的一个责任链fc2,也需要进行处理,为了让fc1与fc2串在一起,可以让FilterChain本身也实现Filter接口,这样可以让fc1直接add责任链fc2,基本逻辑是因为fc2本身也是一个Filter,这样将不同的责任链都串在了一起。
1 | class FilterChain implements Filer{ |
在main方法中
1 | FilterChain fc = new FilterChain(); |
某个Filter决定是否向下继续走。
做法是让Filter的doFilter方法返回一个布尔类型,返回真表示继续进行,返回假表示终止进行。
在FilterChain中,循环的逻辑中,当一个Filter返回的结果为假,表示不用再继续了,直接返回假;不然等全部结束了返回真。
1 | interface Filer{ |
在Servlet中,有Filter,其功能为可以同时处理request与response,同时处理的顺序为request1,request2,…,requestn,responsen,…,response2,response1。是一个递归的过程,那么需要在执行当前resquest的时候,知道下一个需要执行哪个Filter,那么需要拿到当前的FilterChain,在Filter接口的doFilter方法中,除了需要传入Response与Request,还需要传入FilterChain,以便去调用下一个的Filter。而在FilterChain中,维护有当前执行到的index,当index超出容器范围,直接返回。执行当前index对应的Filter,让index自增,返回当前filter执行执行的结果。在每个Filter中,先执行对Request的操作,然后可以选择是否执行下一个Filter,如果要就调用持有的FilterChain对象的doFilter方法,然后递归执行完毕后,再执行当前的Response。
1 | class Request{ |
可以看到,如果在实现的Filter中调用了FilterChain,责任链才会继续往下走,否则会处理当前的response并返回,在main方法中
1 | public static void main(String[] args) { |
这样就可以达到处理的要求了,总结就是思路为递归,需要拿到当前的chain,是否继续往下走的权利在每一个Filter中。
总结:控制责任链是否往下走有两种方式
- Filter接口的doFilter方法返回布尔类型的值,在FilterChain中对每个Filter的方法返回值判断,有false就停止
- Filter接口的doFilter方法传入FilterChain,FilterChain中有指针,指向当前下一个要执行的Filter,当指向末尾返回,不然执行Filter,指针后移,对执行的Filter传入当前FilterChain对象。控制的权利交给Filter,如果想要继续执行,则调用FilterChain的doFilter方法,否则不调用就不往下走了
观察者模式Observer
如果有一个小孩,哭了之后需要观察者做出不同的反应。做法是小孩哭了后,产生一个事件,将事件传给所有的观察者,依据不同的事件来做出不同的反应。观察者模式需要具有的3个基本类是Source(被观察类),Observer(观察类),Event(事件类),被观察者生产一个事件,传递给观察类接口,接口类方法目的是回调具体实现子类中的方法,当Observer观察到Event的时候,产生具体的反应。
对于事件类,需要知道是哪个被观察者,因为可能监听着多个被观察者。Event类中一般需要有getSource的方法。观察者拿到Event对象后,如果需要知道Source,使用getSource方法就可以,这样可以根据Source来处理相应的处理。这样观察者与被观察者解耦合。
应用:UI界面,很多都是观察者模式。如点击一个Button,产生一个事件Event,调用Observer的方法来处理事件。
很多系统中,Observer与责任链共同负责对事件的处理。其中的一个observer负责是否将事件进一步处理。
调停者模式Mediator
许多类之间有许多联系,为了简化类之间的关联,抽出一个类专门与其他类来通信,这个类称为调停者。
门面模式对外,调停者模式对内,可以是同一个。
实际应用:消息中间件MQ(Message Queue)。将消息统一放在MQ中,谁需要消息谁去拿,进行解耦。
迭代器模式Iterator
用于容器与容器遍历。
集合物理实现只有数组跟链表,为了方便操作不同的集合,让其均继承自一个接口,而为了遍历不同的容器,如何遍历只有容器自己清楚,因此实现的过程封装在不同的容器内部,只对外暴露接口。迭代器接口有hasnext与next方法。在集合接口中,提供获取迭代器的方法。
1 | public interface Collection_ { |
对于具体的实现类,比如LinkedList,提供具体的实现方法
1 | public class LinkedList_ implements Collection_{ |
其中LinkedList中的迭代器类为私有的内部类,因为此类只会在当前啊类中使用,而且为了方便访问外部类中的属性与方法,将其进行内部类的封装。而在main方法中,调用的流程如下
1 | Collection_ c = new LinkedList_(); |
如果需要进一步扩展,不指定数据的类型为Object,可以使用泛型。
访问者模式Vistor
在结构不变的情况下动态改变对于内部元素的动作。
具体应用:编译器。对抽象语法树的每个节点,传入Vistor,由Vistor来做具体的检查。单一职责原则。
需求:电脑的内部部件是固定的,比如有CPU,内存,主板,如果有不同的人需要去电脑店,电脑店给不同的人有不同组件的优惠,如果是写在每个电脑组件类的内部来判断,则需要有非常多的if else,而且当多一个人以后,就要增加代码,这样将判断的代码封装到一个Vistor中,在每个电脑组件中,需要有接收一个Vistor的方法,还要有调用vistor中优惠价格的方法。
电脑组件定义如下
1 | abstract class ComputerPart { |
对于一个电脑,以其只有3个组件为例,对所有的组件都传入同一个vistor
1 | public class Computer { |
对于电脑组件具体的实现类,是什么部件就调用Vistor的看什么部件的方法。一定会有accpet方法,传入一个Vistor。
1 | class CPU extends ComputerPart { |
对于Vistor接口
1 | interface Visitor { |
其具体的实现类,拿到对应组件的价格后,进行优惠即可。
1 | class PersonelVisitor implements Visitor { |
在main方法中
1 | public static void main(String[] args) { |
可以看到,其本质是对多个固定组件下,不同组件的if else的封装,适用于组件固定,但每个组件有不同情况的场景下,如果组件频繁增加,则Vistor接口改动也非常频繁,不适用于组件变化多的情况。
ASM框架,相当于使用Java语言来操作字节码文件,可以自己定义Vistor来读字节码文件,也可以来动态写字节码文件。
对于ASM,用Reader去读字节码文件,然后用Writer(一个Visitor)来写字节码文件,如果想要自己定制写的过程,将Reader传给自己定义的Adapter,将Adapter传给Writer,责任链模式。ASM就是用的Visitor,多个Visitor之间形成了链条。利用自己定义的Adapter,可以自己在原来的类字节码文件中的指定方法前后增加内容,这样生成的新的类变为原来类的动态代理类。
命令模式Command
封装命令,结合责任链实现undo(回滚)
别名:Action / Transaction
Command中封装了多个命令,常见的有执行与回滚,对于每个Command,需要能够将命令回退,在undo命令中记录了与execute中相反的操作。
常见的实现:文本编辑器。这时候相当于实现了一次execute,一次undo,但是为了实现一连串的undo,使用责任链。将所有的Commond放入一个集合,当实现一次回退,执行一个Commond中的undo,再实现一次则继续执行一个。
多次undo
Command与责任链
将command加入集合,然后逐个取出执行。如果是多次undo,需要倒着过来做
事务回滚
command与记忆
宏命令(多个命令组成)
command与组合Composite,因为宏命令是树状结构
备忘录模式Memento
记录状态,便于回滚
有一个原对象,使用Memento记录其状态。
- 记录快照(瞬时状态)
- 存盘
模板方法Template Method
模板方法,钩子函数
相当于定好了模板,具体的实现自己来写。
优点是子类不用改变父类中的逻辑框架,但是通过重写部分方法可以实现自己的功能。父类重框架,子类重功能。
重写一个方法,系统自动调用,如awt中的paint。在父类中有个方法,调用了method1与method2,子类去继承父类的时候,只需要重写method1与2,在父类中被自动调用。
如下,父类中m方法调用了op1与op2,但是是抽象的,子类中继承父类重写此方法
1 | abstract class F{ |
在主函数中,多态,调用父类的m方法,执行的是子类中重写的内容。
1 | F f = new C(); |
状态模式State
根据状态决定行为
如果一个类中很多行为都是要根据其状态决定,那么就将state抽象出来,在类中调用state的相应方法即可。类聚合的是state的实现类。
如果在一个类中,固定的几个方法都要根据不同的状态来做出不同的反映,那就把具体的状态给抽象出来,避免自己类的方法过于臃肿。如果自己类的方法会一直扩展,则不适合用State模式。
在设计模式书中的例子是,TCP连接中有open,listen,close等固定几个方法,根据TCP连接状态有不同的反应。
state类如下
1 | abstract class State{ |
其具体的实现类为
1 | class PersonState extends State{ |
在Person类中,聚合一个State,调用其相应方法就可以。
1 | public class Person { |
有限状态机(FSM),就是不同有限的状态之间的迁移。state design pattens 与 state machine不一样。
线程的一个状态迁移就是有限状态机。
解释器模式Intepreter
动态脚本解析