设计模式学习总结!
设计模式学习总结!
月伴飞鱼基本概念
封装
通过对类的封装,仅仅暴露少许必要的方法给调用方使用,调用方不需要了解太多背后的业务细节,大大降低了用错的概率,提高了代码的易用性。
抽象
通过抽象化只暴露出方法定义,而隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
作用:通过抽象化手段,没有暴露出来实现细节,所以当后来改变具体的实现细节时,对于调用方来说不需要或仅有少量修改,提高了代码的可扩展性和可维护性。
继承
表示类之间is-a的关系,比如猫是一种哺乳动物。
作用:最大的一个好处就是提高了代码的复用性。
多态
一个父类有多个不同的子类,而且多个子类都可以替换父类去使用。
作用:提高代码的可扩展性和复用性。
面向对象和面向过程
面向对象编程:以类为组织代码的基本单元,面向过程编程:以过程(或方法)作为组织代码的基本单元。
面向过程编程:数据和方法相分离。不支持丰富的面向对象编程特性,比如继承、多态、封装。
设计原则
单一职责原则(Single Responsibility Principle)
一个类应该只负责一项职责。
- 如类A负责两个不同职责:职责1,职责2。
当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。
接口隔离原则(Interface Segregation Principle)
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
- 大接口要分割成小接口,接口专用。
- 客户端使用专用接口。
依赖倒转原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,二者都应该依赖其抽象(接口或者抽象类)。
- 抽象不应该依赖细节,细节应该依赖抽象。
依赖倒转(倒置)的中心思想是面向接口编程。
里氏替换原则(Liskov Substitution Principle)
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
- 把父类设计为抽象类或者接口。
- 子类必须实现父类的所有方法。
开闭原则(Open Closed Principle)
对扩展开放,对修改关闭。
- 使用接口和抽象类,实现不修改原代码,又可以拓展新方法。
迪米特法制(Demeter Principle)
一个对象应该对其他对象保持最少的了解。
一个实体应当尽量少的与其他实体之间发生相互作用。
- 依赖者只依赖该依赖的对象,被依赖者只暴露该暴露的对象。
合成复用原则(Composite Reuse principle)
尽量使用合成/聚合的方式,而不是使用继承。
UML
关联关系
泛化:
- 继承关系,符号:空心三角箭头的实线,箭头指向父类。
实现:
- 类与接口的关系,表达类实现了接口的特征行为,符号:带三角箭头的虚线,箭头指向接口。
组合:
- 整体与部分的关系,但部分不能离开整体而单独存在,符号:带实心菱形的实线,菱形指向整体。
聚合:
- 整体和部分关系,且部分可以离开整体而单独存在,符号:空心菱形的实心线,菱形指向整体。
关联:
- 类和类的关系,表达一个类知道另一个类的属性和方法,符号:带普通箭头(或实心三角形箭头)的实心线。
依赖:
- 使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖,符号:带箭头的虚线,指向被依赖的类。
类图表示方法
从上到下分为3部分:类名、属性、方法。
符号含义:
+ public、- private、#protected
工厂模式
简单工厂模式
定义一个工厂类,使用
static
方法创建对象,根据不同参数返回不同实例,每增加一个对象需要修改工厂类,违背了开闭原则。
- 不推荐使用
只适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,对于如何创建对象不需要关心。
一旦有了新的实现类,就需要修改工厂实现,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
UML类图:
使用手机生产来讲解该模式:
1 | public interface Phone { |
制造小米手机(Product1
):
1 | public class MiPhone implements Phone { |
手机代工厂(Factory
):
1 | public class PhoneFactory { |
工厂方法模式
定义一个用于创建对象的接口,让子类决定将哪一个类实例化,此模式使一个类的实例化延迟到其子类。
- 但是工厂方法会导致类的个数过多,增加复杂度,增加系统的抽象性和理解难度。
它可以创建一个工厂接口和多个工厂实现类,这样如果增加新的功能,只需要添加新的工厂类就可以,不需要修改之前的代码。
另外,工厂方法模式还可以和模板方法模式结合一起,将他们共同的基础逻辑抽取到父类中,其它的交给子类去实现。
UML类图:
代码体现
某平台先拥有Java和Python学习视频,将来需要拓展业务开放FE(前端)视频学习。
1 | public abstract class Video { |
1 | public class JavaVideo extends Video { |
1 | public class PythonVideo extends Video { |
1 | public abstract class VideoFactory { |
1 | public class JavaVideoFactory extends VideoFactory { |
1 | public class PythonVideoFactory extends VideoFactory { |
1 | public class Test { |
当拓展业务时,可以仿照
Java
与Python
,可以直接添加FEVideo
与FEVideoFactory
,可以体现出开放封闭原则。
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类,用于创建一个产品族的产品。
工厂方法模式和抽象工厂模式,它们之间最大的区别在于:
- 工厂方法模式只有一个抽象产品类,具体工厂类只能创建一个具体产品类的实例。
- 抽象工厂模式有多个抽象产品类,具体工厂类可以创建多个具体产品类的实例。
UML类图:
定义PC产品的接口(AbstractPC
):
1 | public interface PC { |
定义小米电脑产品(MIPC):
1 | public class MiPC implements PC { |
定义苹果电脑产品(MAC):
1 | public class MAC implements PC { |
增加PC产品制造接口:
1 | public interface AbstractFactory { |
增加小米PC的制造(ConcreteFactory1
):
1 | public class XiaoMiFactory implements AbstractFactory{ |
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
构造器模式使用多个简单的对象一步一步构建成一个复杂的对象。
它提供了一种创建对象的最佳方式。
UML类图:
桥接模式
桥接模式就是把抽象和实现分离出来,然后中间通过组合来搭建他们之间的桥梁。
UML类图:
代码实现
中国有很多银行,有中国农业银行和中国工商银行,关于账号,有定期账号和活期账号,一个就是银行一个账号。
1 | public interface Account { |
1 | /** 定期的账号 */ |
1 | /** 活期账号 */ |
1 | public abstract class Bank { |
1 | public class ABCBank extends Bank { |
1 | public class ICBCBank extends Bank { |
1 | public class Test { |
单例模式
单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。
饿汉模式
1 | //在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快 |
饿汉模式优缺点:
缺点:
- 不能实现懒加载,造成空间浪费。
- 如果一个类比较大,在初始化的时就加载了这个类,但是长时间没有使用这个类,这就导致了内存空间的浪费。
懒汉模式
在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例。
- 所以懒汉模式解决了饿汉模式带来的空间浪费问题。
1 | public class SingletonObject { |
1 | public class SingletonObject { |
懒汉模式的优缺点:
优点:
- 实现了懒加载,节约了内存空间。
缺点:
- 在不加锁的情况下,线程不安全,可能出现多份实例。
- 在加锁的情况下,会是程序串行化,使系统有严重的性能问题。
双重检查锁模式
1 | public class SingletonObject { |
在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
1 | private SingletonObject(){ |
我们编写的顺序是1、2、3,JVM 会对它进行指令重排序。
- 所以执行顺序可能是3、1、2,也可能是2、3、1,不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。
如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。
双重检测锁出现空指针问题的原因就是出现在这里。
- 当某个线程获取锁进行实例化时,其他线程就直接获取实例使用。
- 由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。
要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile
关键字。
1 | // 添加volatile关键字 |
静态内部类单例模式
静态内部类单例模式实例由内部类创建。
由于
JVM
在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。
静态属性由static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
1 | public class SingletonObject { |
枚举类单例模式
枚举类型是线程安全的,并且只会装载一次。
设计者充分的利用了枚举的这个特性来实现单例模式。
- 枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
1 | public class SingletonObject { |
破坏单例模式的方法及解决办法
除枚举方式外,其他方法都会通过反射的方式破坏单例。
反射是通过调用构造方法生成新的对象。
- 所以如果想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
1 | Class objClass = SingletonObject.class; |
1 | private SingletonObject(){ |
如果单例类实现了序列化接口
Serializable
,就可以通过反序列化破坏单例。所以可以不实现序列化接口。
- 如果非得实现序列化接口,可以重写反序列化方法
readResolve()
, 反序列化时直接返回相关单例对象。
1 | public Object readResolve() throws ObjectStreamException { |
策略模式
定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
UML类图:
使用举例
以去公园为例,可以有很多路线方式去公园。
1 | //抽象策略(Strategy)类 |
1 | //具体策略(Concrete Strategy)类 |
1 | //负责和具体的策略类交互,使得算法和客户端分离 |
1 | public class Client { |
模板方法模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
Template Method
使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。在模板模式中,一个抽象类公开定义了执行它的方法的方式/模板。
- 它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
简单来说,有多个子类共有的方法,且逻辑相同,可以考虑作为模板方法。
模板模式的关键是:
- 子类可以置换掉父类的可变部分,但是子类却不可以改变模板方法所代表的顶级逻辑。
UML类图:
抽象模板角色类:
1 | public abstract class AbstractTemplate { |
具体模板角色类:
1 | public class ConcreteTemplate extends AbstractTemplate{ |
访问者模式
表示一个作用于某对象结构中的各元素的操作。
它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
UML类图:
适配器模式
适配器模式它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
适配器模式有两种实现方式:类适配器和对象适配器。
- 类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
UML类图:
代码实现
ITarget 表示要转化成的接口定义。
Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口。
1 | // 类适配器: 基于继承 |
如何选择
如果 Adaptee 接口并不多,那两种实现方式都可以。
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器。
- 因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器。
- 因为组合结构相对于继承更加灵活。
应用场景
封装有缺陷的接口设计:
假设依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到自身代码的可测试性。
为了隔离设计上的缺陷,希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
统一多个类的接口设计:
假设系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。
但是,每个系统提供的过滤接口都是不同的,这就意味着没法复用一套逻辑来调用各个系统。
这个时候,就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样可以复用调用敏感词过滤的代码。
代理模式
代理模式提供了对目标对象额外的访问方式,即通过代理对象访问目标对象。
- 这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
UML类图:
静态代理
在编译时就已经实现,编译完成后代理类是一个实际的class文件。
使用方式
创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。
之后再创建一个代理类,同时使其也实现这个接口,在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。
1 | public interface UserDao { |
1 | public class UserDaoImpl implements UserDao { |
1 | public class TransactionHandler implements UserDao { |
1 | public class Main { |
使用JDK静态代理很容易就完成了对一个类的代理操作。
但是
JDK
静态代理的缺点也暴露了出来:
- 由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。
动态代理
JDK动态代理
使用JDK动态代理的五大步骤:
- 通过实现InvocationHandler接口来自定义自己的InvocationHandler
- 通过
Proxy.getProxyClass
获得动态代理类- 通过反射机制获得代理类的构造方法,方法签名为
getConstructor(InvocationHandler.class)
- 通过构造函数获得代理对象并将自定义的
InvocationHandler
实例对象传为参数传入- 通过代理对象调用目标方法
1 | public interface IHello { |
1 | public class HelloImpl implements IHello { |
1 | import java.lang.reflect.InvocationHandler; |
1 | import java.lang.reflect.Constructor; |
JDK静态代理与JDK动态代理不同之处:
在静态代理中我们需要对哪个接口和哪个被代理类创建代理类,所以在编译前就需要代理类实现与被代理类相同的接口,并且直接在实现的方法中调用被代理类相应的方法。
但是动态代理不知道要针对哪个接口、哪个被代理类创建代理类,因为它是在运行时被创建的。
CGLIB
CGLIB包的底层是通过使用一个小而快的字节码处理框架
ASM
,来转换字节码并生成新的类。
CGLIB代理实现如下:
- 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
- 然后在需要使用的时候,通过CGLIB动态代理获取代理对象。
1 | public class HelloService { |
1 | import net.sf.cglib.proxy.MethodInterceptor; |
1 | import net.sf.cglib.core.DebuggingClassWriter; |
JDK代理要求被代理的类必须实现接口,有很强的局限性。
CGLIB
会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。
CGLIB在进行代理的时候都进行了哪些工作
生成的代理类继承被代理类:
- 如果委托类被final修饰,那么它不可被继承,即不可被代理
- 同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理
代理类会为委托方法生成两个方法:
- 一个是与委托方法签名相同的方法,它在方法中会通过
super
调用委托方法- 另一个是代理类独有的方法
当执行代理对象的方法时,会首先判断一下是否存在实现了
MethodInterceptor
接口的CGLIB$CALLBACK_0
,如果存在,则将调用MethodInterceptor
中的intercept
方法。
- 在
intercept
方法中,除了会调用委托方法,还会进行一些增强操作。在Spring AOP中,典型的应用场景就是在某些敏感方法执行前后进行操作日志记录。
在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:
通过FastClass机制对Class对象进行特别的处理。
比如将会用数组保存method的引用,每次调用方法的时候都是通过一个index下标来保持对方法的引用。
Fastclass机制
CGLIB采用了FastClass的机制来实现对被拦截方法的调用。
FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。
三种代理方式之间对比
代理方式 | 实现 | 优点 | 缺点 | 特点 |
---|---|---|---|---|
JDK静态代理 | 代理类与委托类实现同一接口,并且在代理类中需要硬编码接口 | 实现简单,容易理解 | 代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低 | 好像没啥特点 |
JDK动态代理 | 代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke 方法来进行动态代理的,在invoke方法中将对方法进行增强处理 |
不需要硬编码接口,代码复用率高 | 只能够代理实现了接口的委托类 | 底层使用反射机制进行方法的调用 |
CGLIB动态代理 | 代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super 调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor 接口的对象,若存在则将调用intercept方法对委托方法进行代理 |
可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口 | 不能对final 类以及final方法进行代理 |
底层将方法全部存入一个数组中,通过数组索引直接进行方法调用 |
CGlib比JDK快?
使用CGLiB实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK6之前比使用Java反射效率要高。
- 唯一需要注意的是,CGLib不能对声明为Final的方法进行代理, 因为CGLib原理是动态生成被代理类的子类。
在JDK6、JDK7、JDK8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率。
只有当进行大量调用的时候,JDK6和JDK7比CGLIB代理效率低一点,但是到JDK8的时候,JDK代理效率高于CGLIB代理。
- 总之,每一次JDK版本升级,JDK代理效率都得到提升。
Spring如何选择用JDK还是CGLIB?
当Bean实现接口时,Spring就会用JDK的动态代理。
当Bean没有实现接口时,Spring使用CGlib实现。
可以强制使用CGlib。
原型模式
用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
UML类图:
组合模式
将对象组合成树形结构以表示部分-整体的层次结构,它使得客户对单个对象和复合对象的使用具有一致性。
UML类图:
装饰器模式
动态地给一个对象添加一些额外的职责,就扩展功能而言,它比生成子类方式更为灵活。
UML类图:
外观模式
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式Fcade不一定是接口,也可以是类,例如只有FacadeImpl这个类。
Interface1、Interface2、Interface3都是子系统接口。
使用Facade即可实现对3个子系统的调用。
UML类图:
享元模式
运用共享技术来有效地支持大量细粒度对象的复用。
它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
UML类图:
责任链模式
解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。
将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
UML类图:
抽象接口RequestHandler
:
1 | public interface RequestHandler { |
抽象类BaseRequestHandler
:
1 | public abstract class BaseRequestHandler implements RequestHandler { |
具体处理类AHandler
:
1 | public class AHandler extends BaseRequestHandler { |
使用类Client
:
1 | public class Client { |
命令模式
将一个请求封装为一个命令对象,使发出请求、执行请求解耦。
UML类图:
解释器模式
给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
UML类图:
迭代器模式
提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
UML类图:
中介者模式
用一个中介对象来封装一系列的对象交互。
中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
UML类图:
备忘录模式
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 这样以后就可将该对象恢复到保存的状态。
UML类图:
观察者模式
定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
UML类图:
状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它所属的类。
UML类图: