设计模式笔记

参考教程 https://www.bilibili.com/video/BV1GW4y1e7wC/

概述

基本概念

软件设计模式,又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重复代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

分类

设计模式从大的维度来说,可以分为三大类:创建型模式结构型模式行为型模式,这三大类下又有很多小类

创建型模式是指提供了一种对象创建的功能,并把对象创建的过程进行封装隐藏,让使用者只关注具体的使用而并非对象的创建过程。它包含的设计模式有单例模式工厂模式抽象工厂模式建造者模式原型模式

结构型模式关注的是对象的结构,它是使用组合的方式将类结合起来,从而可以用它来实现新的功能。它包含的设计模式是代理模式组合模式装饰模式门面模式

行为型模式关注的是对象的行为,它是把对象之间的关系进行梳理划分和归类。它包含的设计模式有模板方法模式命令模式策略模式责任链模式

UML设计

UML相关概念

定义

UML作为一种模型语言,它使开发人员专注于建立产品的模型和结构,而不是选用什么程序语言和算法实现;UML不是一种编程语言,但工具可用于生成各种语言的代码中使用UML图

虽然UML用于非软件系统,重点是面型对象的软件应用建模。大多数的UML图到目前为止讨论的用于模拟静态、动态等不同的方面,如现在各方面的构件是对象

如果我们观察到类图、对象图、协作图、交互图,将基本上基于对象的设计

因此,面向对象的设计和UML之间的关系是非常重要的理解。根据要求,面向对象的设计转化为UML图

UML基本元素

三个基本模块:事务关系

四种事务:

  1. 结构事务:类,接口,协作,用例,活动类,组件,节点
  2. 行为事务:交互,状态机
  3. 分组事务:包
  4. 注释事务:注释

四种关系:依赖关联实现泛化

十种图:用例图、类图、对象图、包图、部署图、活动图、状态图、序列图、协作图、组件图

UML图

用例图

用例图捕捉了系统中的动态行为,并且描述了用户、需求以及系统功能单元之间的关系

用例图展示了一个外部用户能够观察到的系统功能模型图

用例图由主角用例它们之间的关系组成

用例图的目的:用来收集系统的要求、用于获取系统的外观图、识别外部和内部因素影响系统

如何画用例图:参与者可以是人的用户,一些内部的应用程序,或可能会有一些外部应用程序。因此在一个简短的,当我们正计划绘制一个用例图中应该有以下项目:功能被表示为一个用例、参与者、用例和参与者之间的关系

类图

类图是面向对象系统建模中最常用和最重要的图,是定义其它图的基础

类图主要是用来显示系统中的类、接口以及它们之间的静态结构和关系的一种静态模型

类图不仅用于可视化描述和记录系统的不同方面,类图显示集合的类、接口、关联、协作和约束,它也被成为结构图

作用:类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;类图是帮助分析和设计阶段的重要产物,是系统编码和测试的重要模型

类一般包含类名、属性、方法

属性和方法书写规范:修饰符[描述信息] 属性、方法名[参数] [: 返回类型:类型]

属性和方法之前可附加的可见性修饰符:

  • -:public,公用的,对所有类可见
  • +:private,私有的,只对该类本身可用
  • #:protected,受保护的,对该类的子孙可见
  • ~:package,包的,只对同一包生命的其他类可见

省略修饰符表示具有package级别的可见性

如果属性或方法具有下划线,则说明它是静态的。描述信息使用<<开头,使用>>结尾

类图基本上是一个系统的静态视图的图形表示,代表不同方面的应用。因此,集合类图表示整个系统

画图时注意:

  • 类图中的名称应该是有意义的描述,并且是面向系统的
  • 画类图前应先确定每个元素之间的关系
  • 类图汇总的每个类职责(属性和方法)应该清晰标明
  • 对于每个类的属性的最小数量应符合规定,不必要的属性将使图表复杂
  • 最后,在最终版本之前,该图应绘制在普通纸上尽可能多次,使其纠正和返工
对象图

UML对象图显示某时刻对象和对象之间的关系。一个UML对象图可看成一个类图的特殊用例,实例和类可在其中显示。UML对象图是类图的实例,几乎使用与类图完全相同的标识。由于对象存在生命周期,因此UML对象图只能在系统某一时间段存在

对象图可用于:使一个系统的原型;逆向工程;捕捉实例和链接;详细描述瞬态图

活动图

活动图是描述满足用例要求所进行的活动以及活动间的约束关系,有利于识别并进行活动。它对于系统的功能建模特别重要,强调对象间的流程控制,活动图在本质上是一种流程图

作用:

  • 描述一个操作执行过程中所完成的工作,说明角色、工作流、组织和对象是如何工作的
  • 活动图对用例描述尤其有用,它可建模用例的工作流、显示用例内部和用例之间的路径
  • 显示如何执行一组相关的动作,以及这些动作图和影响他们周围的对象
  • 活动图对理解业务处理过程十分有用,活动图可以画出工作流用以描述业务,有利于领域专家进行交流
  • 描述复杂过程的算法,在这种情况下使用的活动图和流程图的功能是相似的

组件:

名称 解释
初始节点 标记业务流程的开始,有且只有一个初始状态,用实现的圆点表示 实心圆
终止节点 表示业务流程的终止,可以有一个或多个,用一个实心圆外加一个圆圈表示 带圈的实心圆
活动 业务流程中的执行单元 圆角矩形
判断/合并 根据某个条件进行决策,执行不同的流程分支。合并指的是两个或多个控制路径在此汇合的情况。合并和判断常常成对使用。在任何执行中每次只走一条,不同路径间互斥 菱形
分叉/结合 分叉用于表示将一个控制流分为两个或者多个并发运行的分支;结合用来表示并行分支在此得到同步,先完成的控制流需要再次等待,只有当所有的控制流都到达结合点,控制才能继续向下进行 矩形
转换 当一个活动结束时,控制流会马上传递给下一个活动节点,在活动图中称之为"转换",用一条带箭头的直线来表示 带箭头的实线
泳道 代表了一个特定的类、人、部分、层次等等对象的职责区,每个泳道代表特定含义的状态职责的部分。在活动图中,每个活动只能明确的属于一个泳道,泳道明确的表示了哪些活动是由哪些对象进行的

活动图的主要用途:使用业务建模工作流程、建模的业务需求、高层次的理解系统的功能、调查在某一阶段的业务需求

状态图

状态图是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对不同的时间做出反应。通常我们创建一个UML状态图是为了以下的研究目的:研究类、角色、子系统或组件的复杂行为

绘制状态图之前,应明确以下几点:识别对象,以进行分析;识别状态;识别的事件

序列图(时序图)

序列图是用来显示参与者如何以一系列顺序的步骤与系统的对象交互的模型。顺序图可以用来展示对象之间是如何进行交互的。顺序图将显示的重点放在消息序列上,即强调消息是如何在对象之间被发送和接收的

协作图

和序列图相似,显示对象间的动态合作关系。可以看成是类图和顺序图的交集,协作图建模对象或者角色,以及它们彼此之间是如何通信的。如果强调时间和顺序,则使用序列图;如果强调上下级关系,则选择协作图;这两种图合称为交互图

部署图

部署图描述的是系统运行时的结构,展示了硬件的配置及其软件如何部署到网络结构中

部署图通常用来帮助理解分布式系统,一个系统模型只有一个部署图

部署图以可视化的方式展示系统中物理组件的拓扑结构

作用:用来建模系统的物理部署。例如,计算机和设备,以及它们之间是如何连接的。部署图用于表示一组物理节点的集合及节点间的相互关系,从而建立了系统物理层明的模型

部署图主要用于系统工程师。这些图用来描述的物理组件(硬件)以及它们的分布和关联

使用部署图可以描述如下:

  • 为了模拟一个系统的硬件拓扑
  • 嵌入式系统建模
  • 为了模拟一个客户机/服务器系统的硬件的详细信息
  • 为了模拟硬件的分布式应用程序的细节
  • 正向和逆向工程

类之间关系

类之间的关系
  • 设计一个类中的信息和行为要高内聚
  • 设计多个类,类之间要低耦合

面对对象是符合人们对现实世界的思维模式,利用面向对象设计,特别是采用各种设计模式来解决问题时,会设计多个类,然后创建多个对象,一个设计良好的类,应该是兼顾信息和行为并且高内聚。而不同的类之间,应该做到低耦合

当面对应用系统或者需要解决的问题经常是复杂的、高度抽象的,我们创建的多个对象往往是有联系的,通常对象之间的关系可以分为以下几类:

  • 泛化关系,类的继承
  • 实现关系,类实现接口
  • 依赖关系
  • 关联关系
  • 聚合关系
  • 组合关系

对于继承(泛化)实现(实现)这两种关系比较简单,它们体现的是一种类与类、或者类与接口之间的纵向关系,其它四种关系则体现的是类与类、或者类与接口之间的引用/横向关系

图形 关系
----------> 依赖关系
----------▷ 实现
------------▷(实线) 泛化(继承)
------------◇(实线) 聚合
------------◆(实线) 组合
类之间关系详解

泛化表示一般和特殊的关系,是is a的关系,表示的是一种继承关系。UML画法:带空心三角箭头的实线,箭头指向父类

实现是一种类与接口的关系,表示类是接口特征和行为的实现。UML画法:带空心三角箭头的虚线,箭头指向接口

关联描述了不同类的对象之间的结构关系,使一个类知道另一个类的属性和行为。关联关系有单向关联、双向关联、自关联

聚合是一种特殊的关联关系,是一个 整体和部分的关系,部分可以离开整体而单独存在。聚合是一种语义关系,需要分析逻辑关系

组合是一种特殊的关联关系,是一个整体和部分的关系,但是部分不能离开整体单独存在。组合和聚合一样,也是一种语义关系,需要分析逻辑关系

依赖是一个类A使用到了另一个类B,而这种使用关系是具有偶然性、临时性、非常弱的,但是B类的变化会影响到A,是一种使用关系

七大设计原则

设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础

设计模式常用的七大原则有:单一职责原则、接口隔离原则、依赖倒转(倒置)原则、里氏替换原则、开闭原则、迪米特法则、合成复用原则

单一职责原则

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性

如何判断类的职责是否足够单一?不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这个类的设计不满足单一职责原则

  • 类中的代码行数、函数或者属性过多
  • 类依赖的其它类过多,或者依赖类的其它类过多
  • 私有方法过多
  • 比较难给类起一个合适的名字
  • 类中大量的方法都是集中操作类中的某几个属性

实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构

单一职责原则注意事项和细节:

  • 降低类的复杂度,一个类只负责一项职责
  • 提高类的可读性,可维护性
  • 降低变更引起的风险

通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则

接口隔离原则

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。其实通俗来理解就是,不要在一个接口里面放很多的方法,这样会显得这个接口很臃肿不堪。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加轻便灵活。

接口隔离原则和单一职责原则对比:接口隔离原则和单一职责原则的审视视角是不同的,单一职责原则要求类和接口职责单一,注重的是职责,是业务逻辑上的划分,而接口隔离原则要求方法要尽可能的少,是在接口设计上的考虑

依赖倒转原则

依赖倒转原则是指:高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象

依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定得多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象指的是接口或者抽象类,细节就是具体的实现类

使用接口或抽象类的目的是指定好规范,而不涉及具体的操作 ,把展现细节的任务交给它们的实现类去完成

依赖倒转原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:

  • 每个类尽量提供接口或抽象类,或者两者都具备
  • 变量的声明类型尽量是接口或者抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要覆写基类的方法
  • 使用继承时结合里氏替换原则

里氏替换原则

里氏替换原则通俗来讲就是:子类对象能够替换程序中的父类对象,并且还能保证原来的处理逻辑不变,以及程序不被破坏。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法

如何避免违反里氏替换原则:基于里氏替换原则的定义,与期望行为一致的替换。

  • 从行为出发来设计
  • 基于契约设计

开闭原则

开闭原则是编程中最基础、最重要的设计原则。一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化

开闭原则的最用:开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下:

  1. 对软件测试的影响。软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行
  2. 可以提高代码的可复用性。粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性
  3. 可以提高代码的可维护性。遵守开闭原则的软件,其稳定性高和延续性强,,从而易于扩展和维护

迪米特法则

迪米特法则定义是:只与你的直接朋友交谈,不跟陌生人说话。其含义是:如果两个软件实体无需直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性

迪米特法则还有个更简单的定义:只与直接的朋友通信

什么叫做直接的朋友呢?每个对象都必然会和其它对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系有很多,比如组合、聚合、依赖等等。包括以下几类:

  1. 当前对象本身(this)
  2. 当前对象的方法参数(以参数形式传入到当前对象方法中的对象)
  3. 当前对象的成员对象
  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
  5. 当前对象所创建的对象

迪米特法则的特点:降低了类之间的耦合度,提高了模块的相对独立性;由于亲和度降低,从而提高了类的可复用率和系统的扩展性

但是,过渡使用迪米特法则会是系统产生大量的中介类,从而增加系统的复杂性,是模块之间的通信效率降低。所以,在采用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰

合成复用原则

合成复用原则是指尽量使用对象组合/聚合而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少

继承叫作白箱复用,相当于把所有的实现细节暴露给子类

组合/聚合称为黑箱复用,我们是无法获取类以外的对象的实现细节的。虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型

继承复用缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为白箱复用
  2. 子类与父类的耦合度高。父类实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化

组合或聚合复用优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为黑箱复用
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象

创建型设计模式

单例模式

介绍

单例模式是Java中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类

特点:可以减少系统内存开支,减少系统性能开销,避免对资源的多重占用、同时操作

缺点:扩展很困难,容易引发内存泄漏,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致的问题

使用场景:

  1. 产生某对象会消耗过多的资源,为避免频繁地创建与销毁对象对资源的浪费
  2. 某种类型的对象应该有且只有一个。如果制造出多个这样的实例,可能导致程序行为异常、资源使用过量、结果不一致等问题

常见的单例模式应用和使用的解决方法:饿汉式初始化、懒汉式初始化、同步信号、双重锁定、使用ThreadLocal

饿汉式

在只考虑一个类加载器的情况下,"饿汉方式"实现的单例(在系统运行起来装载类的时候就进行初始化实例的操作,由JVM虚拟机来保证一个类的舒适化方法在多线程环境中被正确加锁和同步,所以)是线程安全的

特点:代理对象,要求初始化速度快&占用内存小

重点:都需要构造方法私有化

方式一:

java 复制代码
public class Singleton {
    public static final Sinleton INSTANCE = new Singleton();
    private Singleton(){}
}

方式二:

java 复制代码
public class Singleton {
    private static final Sinleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

方式三:

java 复制代码
public class Singleton {
    private static final Sinleton INSTANCE = null;
    static {
        INSTANCE = new Singleton();
    }
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

这三种方式差别不大,都依赖JVM在类装载时就完成唯一对象的实例化,基于类加载的机制,它们天生就是线程安全的,所以都是可行的,第二种更易于理解也比较常见

懒汉式

第一种:

java 复制代码
public class Singleton {
    private static final Sinleton sinleton;
    private Singleton(){}
    public static Singleton getInstance(){
        if (sinleton == null) {
            sinleton = new Sinleton();
        }
        return sinleton;
    }
}

这种方法只能在单线程下使用,多线程下有可能多个线程同时进入if语句从而创建出多个实例。

方式二:

原理:使用同步锁synchronized锁住创建单例的方法,防止多个线程同时调用,从而避免造成单例被多次创建

java 复制代码
public class Singleton {
    private static final Sinleton sinleton;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if (sinleton == null) {
            sinleton = new Sinleton();
        }
        return sinleton;
    }
}

保证了线程安全,但是因为锁造成了效率低下,所以不推荐这种方式

双重校验

在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化,同时在调用getInstance()方法时不进行同步锁,效率高。这种实现方法既保证了其高效性,也保证了其线程安全性

volatile关键字:Java编译器允许处理器乱序执行,volatile禁止重排序,加上就不乱序了

java 复制代码
public class Singleton {
    private static volatile Sinleton sinleton;
    private Singleton(){}
    public static Singleton getInstance(){
        if (sinleton == null) {
            synchronized(Singleton.class){
        		if (sinleton == null) {
                	sinleton = new Sinleton();
                }
            }
        }
        return sinleton;
    }
}
静态内部类

原理:根据静态内部类的特性,同时解决了按需加载、线程安全的问题,同时实现简洁,属于懒汉模式

在静态内部类里创建单例,在装载该内部类时才会创建单例

线程安全:类是由JVM加载,而JVM只会加载一遍,保证只有一个单例

java 复制代码
public class Singleton {
    private Singleton(){}
    private static class SingletonHolder{
        private static final Singleton SINGLETON = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.SINGLETON;
    }
}
枚举方式

特点:满足单例模式所需的创建单例、线程安全、实现简洁的需求

java 复制代码
public enum EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

好处:保证只有一个实例(即使使用反射)、线程安全

ThreadLocal方式

ThreadLocal会为每一个线程提供一个独立的对象副本,从而解决了多个线程对数据的访问冲突问题。正因为每一个线程都拥有自己的对象副本,也就省去了线程之间的同步操作

对单例模式的破坏和解决方法

除了多线程,序列化也可能破坏单例模式一个实例的方式。序列化一是可以将一个单例的实例对象写到磁盘,实现数据的持久化;二是实现对象数据的远程传输。当单例对象有必要实现Serializable接口时,即使将其构造函数设为私有,在它反序列化时依然会通过特殊的途径再创建类的一个新的实例,相当于调用了该类的构造函数有效地获取看一个新实例

解决办法:在单例类中设置读取解析方法

java 复制代码
	private Object readResolve(){
        return singleton;
    }

除了多线程、反序列化以外,反射也会对单例造成破坏。反射可以通过setAccessible(true)来绕过private限制,从而调用到类的私有构造函数创建对象

解决办法:在单例类中创建int变量,判断构造函数调用几次,超过1次就抛出异常,从而阻止多次创建

如果单例对象已经将构造方法声明为private,并且重写了构造方法,那么暂时无法调用到构造方法。但是还有一种情况,那就是拷贝,拷贝的时候是不需要经过构造方法的。但是想拷贝,必须实现Clonable方法,并且需要重写clone方法

建造者模式

将复杂对象的构造与其表示分离,以便同一构造过程可以创建不同的表示

建造者模式主要包含四个角色

  • Product:代表最终构建的对象,比如汽车类
  • Builder:代表建造者的抽象类(可以使用接口来代替)。它定义了构建Product的步骤,它的子类(或接口实现类)需要实现这些步骤。同时,它还需要包含一个用来返回最终对象的方法getProduct()
  • ConcreteBuilder:代表Builder类的具体实现类
  • Director:代表代表需要建造最终对象的某种算法。这里通过使用构造函数Construct(Builder builder)来调用Builder的创建方法创建对象,等创建完成后,再通过getProduct()方法来获取最终的完整对象

总结来说,就是先创建一个建造者,然后给建造者指定一个构建算法,建造者按照算法中的步骤步完成对象的创建,最后获取最终对象

使用建造者模式的场景

  • 需要生成的对象包含多个成员属性
  • 需要生成对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类
  • 需要隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

使用建造者模式能更方便地帮助我们按需进行对象的实例化,避免写很多不同参数的构造函数,同时还能解决同一类型参数智能写一个构造函数的弊端

为什么使用构造着模式?第一,分阶段、分步骤的方法更适合多次运算结果类创建场景;第二,不需要关心特定类型的建造者的具体算法实现

工厂模式

工厂模式分为三种:简单工厂、工厂方法和抽象工厂

简单工厂

简单工厂模式定义为:简单工厂模式又称为静态工厂方法模型,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂专门定义了一个类来负责创建其它类的实例,被创建的实例通常都具有共同的父类

简单工厂中的角色:

  • Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有实例的内在逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它返回一个抽象产品类Product,所有的具体产品都是抽象产品的子类
  • Product(抽象产品角色):抽象产品角色是简单工厂模式所创建的所有对象的父类,负责描述所有实例所共有的公共接口,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个工厂方法,因为所有创建的具体产品对象都是其子类对象
  • ConcreteProduct(具体产品类):具体产品角色是简单工厂模式的创建目标,所有创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现定义在抽象产品中的抽象方法

简单工厂的优点:封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新厂品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展

简单工厂的缺点:增加新产品时还是需要修改工厂类的代码,违背了开闭原则

工厂方法模式

简单工厂模式违背了开闭原则,而工厂方法模式是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则

工厂方法模式中,工厂父类负责提供创建产品对象的公共接口,而工厂子类负责生成具体的产品对象,换而言之,调用方只需要知道产品的类名或者某个标识就可以了,不需要知道产品对象的详细创建过程,将具体类的实例化操作延迟到工厂子类中完成,降低模块之间的耦合性。提供代码结构的扩展性,屏蔽每个功能类中的具体实现逻辑,减少开发维护成本

工厂方法模式的目的很简单,就是封装对象创建的过程,提升创建对象方法的可复用性

工厂方法模式的主要角色

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法newProduct()来创建产品
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特征和功能
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应

工厂方法模式特点:

  • 优点:
    • 用户只需要关心所需产品对应的工厂,无需关心创建细节
    • 加入新产品符合开闭原则,提高可扩展性
  • 缺点:
    • 类的个数容易过多,增加复杂度
    • 增加了系统的抽象性和理解难度

简单工程与工厂犯法模式的区别:简单工厂把全部的事情,在一个地方都处理完了,而工厂方法确实创建一个框架,让子类决定如何实现具体对象的创建。简单工厂可以将对象创建处理,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品

使用场景:

  • 需要使用很多重复代码创建对象时,比如,DAO层的数据对象、API层的VO对象等
  • 创建对象需要访问外部信息或资源时,比如,读取数据库字段、获取访问授权token信息、配置文件等
  • 创建需要统一管理生命周期的对象时,比如,会话信息、用户网页浏览轨迹对象等
  • 创建池化对象时,比如,连接池对象、线程池对象、日志对象等。这些对象的特征是:有限、可重用,使用工厂方法模式可以有效节约资源
  • 希望隐藏对象的真实类型时,比如,不希望使用者知道对象的真实构造函数参数等

为什么使用工厂方法模式:第一,为了吧对象的创建和使用过程分开,降低代码的耦合性;第二,减少重复代码;第三,统一管理创建对象的不同实现逻辑

抽象工厂模式

抽象工厂模式是对工厂方法模式的增强,解决了一个抽象工厂只能生产一个产品的问题。抽象工厂模式在工厂方法模式中的抽象工厂和具体工厂中新增生产产品的方法,从而在进行调用的时候能够拿到多个产品。注意一个工厂生产的多个产品应当是同一类的产品,即产品间有关联(至少在业务逻辑中是有关系的),这样才能够符合设计初衷

抽象工厂模式中包含了四个关键角色:

  • 抽象工厂
  • 抽象产品(通用的一类对象或接口)
  • 具体工厂
  • 具体产品(继承通用对象或接口后扩展特有属性)

其中最为关键的角色并不是抽象工厂本身,而是抽象产品。抽象产品的好坏才是直接决定了抽象工厂和具体工厂能否发挥最大作用的关键所在

为什么要使用抽象工厂模式?第一,对于不同产品系列有比较多共性特征时,可以使用抽象工厂模式,有助于提成组件的复用性;第二,当需要提升代码的扩展性并降低维护成本时,把对象的创建和使用过程分开,能有效地将代码统一到一个级别上;第三解决了跨平台带来的兼容问题

源码应用:spring的BeanFactory、MyBatis的SqlSessionFactory

原型模式

原型模式:是用于创建重复对象,同时又能保证性能。这种类型的设计模式属于创建型设计模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式

工作原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实现创建,即对象的clone()

原理结构图说明:

  • IPrototype:原型类,声明一个克隆自己的接口clone
  • IPrototypeA与IPrototypeB:具体的原型类,实现一个克隆自己的操作
  • Client让一个原型对象克隆自己,从而创建一个新的对象(相当于属性)

实现原型抽象类时,需要注意三点:

  1. 实现Cloneable接口:Cloneable接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone()方法。在JVM中,只有实现了Cloneable接口的类才能被拷贝,否则会抛出CloneNotSupportedException异常
  2. 重写Object类中的clone()方法:在Java中,所有类的父类都是Object类,而Object类中有一个clone()方法,作用是返回对象的一个拷贝
  3. 在重写clone()方法中调用super.clone():默认情况下,类不具备复制对象的能力,需要调用super.clone()来实现

深拷贝与浅拷贝,有的地方也叫深克隆与浅克隆。浅拷贝是创建一个新对象,这个对象有个原始对象属性值的一份精确拷贝,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象 。深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

源码应用:ArrayList和HashMap都使用了原型模式的浅拷贝

在Java中,原型模式指的是,不通过new关键字来创建对象,而是通过对象复制的方式来实现对象的创建的方式

原型模式的注意事项和细节:

  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  2. 不用重新初始化对象,而是动态地获得对象运行时的状态
  3. 如果原始对象发生变化(增加或者减少属性),其它克隆对象也会发生对象,无需修改代码
  4. 在实现深克隆的时候可能需要比较复杂的代码
  5. 需要为每一个配置类修改一个克隆方法,这对全新的类来说不是很难,但是对已有的类进行改造时,需要修改其源代码,违背了ocp原则(开闭原则)

结构型设计模式

结构型设计模式一共包括了七种:适配器模式、桥接模式、组合模式、装饰模式、门面模式、享元模式和代理模式

适配器模式

适配器模式的原始定义是:将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作

适配器是使用场景主要有两大类:第一类就是原有接口功能不满足现有要求,需要在兼容老接口的同时做适当的扩展;第二类是有相似性的多个不同接口之间做功能的统一。

使用场景具体有:原有接口无法修改时、原有接口功能太老旧时、统一多个类的接口设计时、需要过渡升级旧接口时、需要依赖外部系统时、适配不同数据格式时、不同接口协议转换时

适配器模式包含三个关键角色:

  • 目标类,适配器类即将要进行适配的抽象类或接口
  • 适配器类,可以是类或接口,是作为具体适配器类的中间类来使用
  • 具体适配者类,可以是内部的类或服务,也可以是外部对象或服务

源码应用:InputStreamReader充当了适配器的角色,使用new关键字来创建InputStreamReader(System.in)对象时,实际上最终生成了能够让BufferedReader进行读取的Reader输入流,这样便实现了适配器模式

适配器模式的使用场景侧重于将不适用的功能转换到期望可用的功能

为什么使用适配器模式:第一,原有接口无法修改但又必须快速兼容部分新功能;第二,需要使用外部组件组合成新组件来提供功能,而又不想重复开发部分功能;第三,不同数据格式、不同协议需要转换

桥接模式

桥接模式的主要作用就是通过将抽象部分与实现部分分离,把多种可匹配的使用进行组合。说白了核心实现也就是在A类中含有B类接口,通过构造函数传递B类的实现,这个B类就是设计的桥

使用场景:JDBC多种驱动程序的实现、同品牌类型的台式机和笔记本平板。这些场景都比较适合使用桥接模式进行实现,因为在一些组合内如果每一个类都实现不同的服务可能会出现笛卡尔积,而是用桥接模式就可以非常简单

源码应用:MySQL的Driver就是实现类

桥接模式的优点:

  1. 抽象和实现的分离
  2. 优秀的扩展能力
  3. 经常遇到一些可以通过两个或多个维度划分的事物,第一种解决方式是多层继承,但是复用性比较差,同时类的个数也会很多,桥接模式是改进其的更好办法
  4. 桥接模式增强了系统的扩展性,在两个维护中扩展任意一个维度都不需要修改原有代码,负荷开闭原则

桥接模式的缺点:桥接模式的引入会增加系统的理解和设计难度,由于聚合关系关联建立在抽象层,要求开发者针对抽象进行设计和编程,能正确地识别出系统中两个独立变化的维度

组合模式

自核模式也称之为整体-部分模式。组合模式的核心是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得单个对象和组合对象的使用具有一致性

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,最顶层的节点称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点

组合模式包含三个关键角色:

  • 抽象组件:定义需要实现的统一操作
  • 组合节点:代表一个可以包含多个节点的复制对象,意味着在它下面还可以有其它组合节点或叶子节点
  • 叶子节点:代表一个原子对象,意味着在它下面不会有其它节点了

组合模式最常见的是树形结构,除此之外还有环形结构和双向结构

组合模式有两种写法:透明模式安全模式

透明组合模式:

  1. 先建立一个顶层的抽象科目类,这个类中定义了三个通用操作方法,但是均不支持操作,这个类是抽象类,但是在这里并没有将这些方法定义为抽象方法,而是默认都抛出异常。这么做的原因是假如定义为抽象方法,那么所有的子类都必须重写父类方法。但是这种通过抛异常的方式,如果子类需要用到的功能就重写覆盖父类方法即可,不需要用到的方法就无需重写
  2. 新建一个普通科目类继承通用科目抽象类,这个类作为叶子节点,没有重写addChild方法,也就是这个类属于叶子节点,不支持添加子节点
  3. 建立一个具有层级的节点,三个方法都重写了,支持添加子节点,这个类里面为了方便打印的时候检出层级关系,所以定义了一个层级属性

透明组合模式的特点就是将组合对象所有的公共方法都定义在了抽象组件内,这样做的好处是客户端无需分辨当前对象是属于树枝节点还是叶子节点,因为它们具备了完全一致的接口,不过缺点就是叶子节点得到了一些不属于它的方法,比如上面的assChild方法,这违背了接口隔离性原则

安全组合模式:安全组合模式只是规定了系统各个层次的最基础的一致性行为,而组合(树节点)本身的方法(如树枝节点管理子类的addChild等方法)放到自身当中

步骤:

  1. 首先还是建立一个顶层的抽象根节点(这里面只定义了一个通用的抽象info方法)
  2. 建立叶子节点
  3. 建立树枝节点,添加树枝节点专有的方法addChild和属性level

使用场景:

  • 处理一个树形结构,比如,公司人员组织架构、订单信息等
  • 跨越多个层次结构聚合数据,比如,统计文件夹下文件总数
  • 统一处理一个结构中的多个对象,比如,遍历文件夹下所有XML类型文件内容

组合模式就是专门为需要反复计算或统计的场景而生的。假如我们要创建一个可以生成树形对象的功能,在面向对象编程中,组合模式能够很好地适用于解决树形结构的应用场景

源码应用:Map接口和HashMap类的putAll方法

为什么使用组合模式:

  1. 希望一组对象按照某种层级结构进行管理,比如,管理文件夹和文件,管理订单下的商品等。树形结构天然有一种层次的划分特性,能够让我们自然地理解多个对象间的结构
  2. 需要按照统一的行为来处理复杂结构中的对象,比如,创建文件,删除文件,移动文件等。在使用文件时,我们其实并不关心文件夹和文件是如何被组织和存储的,只要我们能够正确操作文件即可,这时组合模式就能够很好地帮助我们组织复杂的结构,同时按照定义的统一行为进行操作
  3. 能够快速扩展对象组合。比如,订单下的手机商品节点可以自由挂接不同分类的手机,并且还可以按照商品的特性再自由地挂接新的节点组合,而查找时可以从手机开始查找,不断增加节点类型,直到找到合适的手机商品

装饰模式

装饰模式:允许动态地向一个现有的对象添加新的功能,同时又不改变其结构,相当于对现有的对象进行了一个包装

因为不能直接修改原有对象的功能,只能在外层进行功能的添加,所以装饰模式又叫包装器模式

特点:

  1. 装饰对象和真实对象有相同的接口。这样客户端对象就能以和真实对象相同的方式和装饰对象交互
  2. 装饰对象包含一个真实对象的引用
  3. 装饰对象接受所有来自客户端的请求。它把这些请求转发给真实的对象
  4. 装饰对象可以在转发这些请求以前或以后增加一些附加功能。这样就确保了在运行时,不同修改给定对象的结构就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展

装饰模式的四个关键角色:

  1. 抽象组件Component类
  2. 组件具体实现ConcreteComponent类。也就是被装饰的对象
  3. 抽象装饰类Decorator,内部持有一个组件对象的引用,职责就是装饰ConcreteComponent类。之所以是抽象的,就是为了方便不同的装饰"风格"子类的自定义实现
  4. 具体装饰类ConcreteDecorator

源码应用:java的IO中InputStream、MyBatis中的Cache

装饰模式应用场景:

  • 快速动态扩展和撤销一个类的功能场景
  • 可以通过顺序组合包装的方式来附加扩张功能的场景
  • 不支持继承扩展类的场景

装饰模式使用于一个通用功能需要做扩展而又不想继承原有类的场景,同时还适合一些通过顺序排列组合就能完成扩展的场景

装饰模式优点:

  1. 装饰者是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用
  2. 通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同的效果
  3. 装饰着完全遵守开闭原则

装饰模式缺点:

  1. 会出现更多的代码,更多的类,增加程序复杂性
  2. 动态装饰以及多层装饰时会更加复杂

门面模式

门面模式(facade),又称之为外观模式。门面模式提供了一个统一的接口,这个接口可以用来访问相同子系统或者不同子系统之中的一群接口,门面模式使得系统更加容易调用。门面模式的本质就是统一多个接口的功能。换句话说,当我们需要用更统一的标准方式来与系统交互时,就可以采用门面模式

门面模式主要包含2种角色:

  • 外观角色:也成门面角色,系统对外的统一接口;
  • 子系统角色:可以同时有一个或多个子系统。每个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道外观角色的存在,对于子系统而言,外观角色只是另一个客户端

源码应用:Tomcat中RequestFacade.class

使用场景:

  • 简化复杂系统
  • 减少客户端处理的系统数量
  • 让一个系统(或对象)为多个系统(或对象)工作
  • 联合更多的系统来扩展原有系统
  • 作为一个简洁的中间件

小结:

  • 子系统内部之间往往存在着一定的关联的。使用了Facade,**可以降低客户端和子系统之间的耦合。**客户端无需关注子系统内部的关系和细节。提高了子系统的独立性和可移植性
  • 引入Facade后,客户端只要维护Facade的版本,而不需要维护各个子系统的版本,维护和升级变得简洁
  • Facade的抽象可以有多个,Facade提供的服务其实是通过调用子系统服务来协作完成的。Facade提供给用户的服务是客户需要的,而不是全部的子系统的能力
  • 引入Facade也符合迪米特法则。迪米特法则又叫作最少知识原则,也就是说一个对象应当对其它对象尽可能少的了解,不和陌生人说话

享元模式

享元模式的原始定义是:摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,从而让我们能在有限的内存容量中载入更多对象

享元模式要解决的核心问题就是节约内存空间,使用的方法是找出相似对象之间的共有特征,然后复用这些特征

享元模式包含的关键角色有四个:

  • 享元类(Flyweight):定义了享元对象需要实现的公共操作方法。在该方法中会使用一个状态作为输入参数,也叫外部状态,由客户端保存,在运行时改变。
  • 享元工厂类(Flyweight Factory):管理一个享元对象类的缓存池。它会存储享元对象之间需要传递的共有状态,比如,按照大写英文字母来作为标识状态,这种只在享元对象之间传递的方式就叫内部状态。同时,它还提供了一个通用方法getFlyweight(),主要通过内部状态标识来获取享元对象
  • 可共享的具体享元类(ConcreteFlyweight):能够复用享元工厂内部状态并实现享元类公共操作的具体实现类
  • 非共享的具体享元类(UnsharedConcreteFlyweight):不服用享元工厂内部状态,但实现享元类的具体实现类

内部状态:不会随环境改变而改变的状态,俗称不可变对象。比如,在Java中Integer对象初始化就是缓存-127到128的值,无论怎么使用Integer,这些值都不会变化

外部状态:随环境改变而变化的状态。通常是某个对象所独有的,不能被共享,因此,也只能由客户端保存。之所以需要外部状态就是因为客户端需要不同的定制化操作

享元模式封装的变化有:

  • 对象内部状态的定义规则,比如,是通过字母共享状态,还是通过固定的数字来共享状态
  • 具体享元对象所实现的公共操作的逻辑

所以说,享元模式本质上是通过创建更多的可复用对象的共有特征来尽可能地减少创建重复对象的内存消耗

使用场景:

  • 系统中存在大量重复创建的对象
  • 可以使用外部特定的状态来控制使用的对象
  • 相关性很高并且可以复用的对象

在Java中,享元模式一个常用的场景就是,使用数据类的包装类对象valueOf()方法。比如,使用Integer.valueOf()方法时,实际的代码实现中有一个叫IntegerCache的静态类,它就是一直缓存了-127到128范围内的数值

享元模式本质上在使用时就是找到不可变的特征,并缓存起来,当类似对象使用时从缓存中读取,已达到节省内存空间的目的。比如,在需要承接大流量的系统中使用图片,我们都知道高清图片即便是压缩后占用的内存空间也很大,那么在使用图片时节省内存空间就是首要考虑的设计因素,而享元模式可以很好地帮助我们解决这类问题场景

为什么使用享元模式:减少内存消耗,节省服务器成本;聚合同一类的不可变对象,提高对象复用性。

小结:

享元模式为共享对象定义了一个很好的结构范例。不过,用好享元模式的关键在于找到不可变对象,比如常用数字、字符等。之所以做对象共享而不是对象复制的一个很重要的原因,就在于节省对象占用的内存空间大小。缓存模式和享元模式最大的区别就是:享元模式强调的是空间效率,比如,一个很大的数据模型对象如何尽量少占用内存空间并提供可复用的能力;而缓存模式强调的是时间效率,,比如缓存秒杀的活动数据和库存数据等,数据可能占用很多内存和磁盘空间,但是得保证在大促活动开始时要能及时响应用户的购买需求。也就是说,两者本质上解决的问题类型时不同的。虽然享元模式的应用不如缓存模式多,但是对于超大型数据模式来说,它却是非常有效的优化方法之一。特别是对于现在越来越多的数据系统来说,共享变得更加重要,因为复制虽然时间效率高,但是空间上可能完全不够

代理模式

代理模式(Proxy Pattern)是指为其它对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用。提供了对目标对象另外的访问方式,即通过代理访问目标对象。这样好处:可以在目标对象实现的基础上,增强额外的功能操作。

代理模式可以分为静态代理和动态代理两种类型,而动态代理又分为JDK动态代理和CGLIB动态代理两种

静态代理

标准的静态代理模式只需要定义一个接口,然后被代理对象与代理对象均需要重写接口方法,被代理对象本身只需要实现真正的业务逻辑,而代理对象中的方法需要调用被代理对象的方法,且可以在调用前后新增一些其他逻辑处理。代理对象中需要显式声明被代理对象,也就是需要持有被代理对象的引用,一般通过代理对象的构造器传入被代理对象,以达到持有被代理对象的目的

JDK动态代理

JDK动态代理主要是Java提供的类和接口

Proxy类

Proxy类的静态方法可以创建代理对象

java 复制代码
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

参数1:ClassLoader loader 类加载器,用类加载代理对象

参数2:Class<?>[] interfaces 目标类的字节码对象数组,因为代理的是接口,需要知道接口中所有的方法

参数3:InvocationHandler h 执行句柄,代理对象处理的核心逻辑就在该接口中

接口InvocationHandler

有一个方法

java 复制代码
Object invoke(Object proxy, Method method, Object[] args)

当代理对象调用原来的目标方法的时候就会执行InvocationHandler中的invoke方法

参数1:Object proxy 代理对象

参数2:Method method 当前调用方法的对象

参数3:Object[] args 实际传递的参数

JDK内置了一种动态代理的实现方式,主要有以下两个条件

  • 定义的代理对象必须要实现java.lang.reflect.InvocationHandler接口
  • 被代理对象必须要显式的实现至少一个接口
使用步骤
  1. 创建接口,定义目标类要完成的功能

    java 复制代码
    public interface service {
        public void reduceStock();
    }
  2. 创建目标类实现接口

    java 复制代码
    public class ServiceImpl implements service {
        //业务方法
        @Override
        public void reduceStock() {
            System.out.println("扣减库存开始");
        }
    }
  3. 创建InvocationHandler接口的实现类,在invoke方法中完成代理类的功能,包括调用目标方法和增强功能

    java 复制代码
    public class Dynamicproxy implements InvocationHandler {
        private Object targetObject;
    
        public Dynamicproxy(Object targetObject) {
            this.targetObject = targetObject;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("日志开始");
            Object invoke = method.invoke(targetObject, args);
            System.out.println("日志结束");
            return invoke;
        }
    }
  4. 使用Proxy类的静态方法,创建代理对象。并把返回值转为接口类型。方法:静态方法 newProxyInstance(),作用是创建代理对象,等同于静态代理中的TaoBao taoBao = new TaoBao();

    java 复制代码
            //生成代理类文件 在根目录的同级目录,com下
            System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
            //实现了接口的业务类
            ServiceImpl iservice = new ServiceImpl();
            //获取Class对象
            Class<?> iserviceClass = iservice.getClass();
            //代理类 实现需要实现InvocationHandler接口,重写invoke方法 传入业务实现类对象
            Dynamicproxy dynamicproxy = new Dynamicproxy(iservice);
    
            //创建代理类对象
            service so = (service)Proxy.newProxyInstance(iserviceClass.getClassLoader(),
                    iserviceClass.getInterfaces(), dynamicproxy);
  5. 调用接口方法

    java 复制代码
    so.reduceStock();
原理分析

动态代理最核心的就是如何生成代理类,所以最核心的逻辑是在JdkTravelAgency中的getInstance方法调用Proxy.newProxyInstance方法

我们知道代理类是通过Proxy类的ProxyClassFactory工厂生成的,这个工厂类会去调用ProxyGenerator类的generateProxyClass()方法来生成代理类的字节码。ProxyGenerator这个类存放在sun.misc包下,我们可以通过OpenJDK源码来找到这个来,该类generateProxyClass()静态方法的核心内容就是去调用generateClassFile()实例方法来生成Class文件

JDK动态代理分为以下几步:

  1. 拿到被代理对象的引用,并且通过反射获取到它的所有的接口
  2. 通过JDK Proxy类重新生成一个新的类,同时新的类要拿到被代理类所实现的所有的接口
  3. 动态生成Java代码,把新加的业务逻辑方法由一定的逻辑代码去调用
  4. 编译新生成的Java代码.class
  5. 把新生成的.class文件重新加载到JVM中运行

所以说JDK动态代理的核心是通过重写被代理对象所实现的接口中的方法来重新生成代理类来实现的,那么假如被代理对象没有实现接口呢?那么这时候就需要CGLIB动态代理了

CGLIB动态代理

JDK动态代理是通过重写被代理对象实现的接口中的方法来实现,而CGLIB是通过继承被代理对象来实现,和JDK动态代理需要实现指定接口一样,CGLIB也要求代理对象必须要实现MethodInterceptor(方法拦截器)接口,并重写其唯一的方法intercept

因为CGLIB是通过继承目标类来重写其方法来实现的,故而如果是final和private方法则无法被重写,也就是无法被代理

实现步骤

  1. 导入依赖cglib-nodep或cglib

  2. 创建被代理对象的类,定义方法

  3. 创建代理对象的类,实现MethodInterceptor接口定义事务拦截器,实现获取代理对象的方法

    java 复制代码
    public class TransactionInterceptor implements MethodInterceptor {
        Object target;
    
        public TransactionInterceptor(Object target) {
            this.target = target;
        }
    
        /**
         * proxy:代理对象,CGLib动态生成的代理类实例
         * method:目标对象的方法,上文中实体类所调用的被代理的方法引用
         * args:目标对象方法的参数列表,参数值列表
         * methodProxy:代理对象的方法,生成的代理类对方法的代理引用
         */
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("开启事务..." + proxy.getClass().getSimpleName());
            Object objValue = null;
            try {
                // 反射调用目标类方法
                objValue = method.invoke(target, args);
                System.out.println("返回值为:" + objValue);
            } catch (Exception e) {
                System.out.println("调用异常!" + e.getMessage());
            } finally {
                System.out.println("调用结束,关闭事务...");
            }
            return objValue;
        }
    
        /**
         * 获取代理实例
         */
        public Object getTargetProxy() {
            // Enhancer类是cglib中的一个字节码增强器,它可以方便的为你所要处理的类进行扩展
            Enhancer eh = new Enhancer();
            // 1.将目标对象所在的类作为Enhancer类的父类
            eh.setSuperclass(target.getClass());
            // 2.通过实现MethodInterceptor实现方法回调
            eh.setCallback(this);
            // 3. 创建代理实例
            return eh.create();
        }
    }
  4. 使用

    java 复制代码
    public static void main(String[] args) {
        // 1. 创建目标实例
        UserService userService = new UserService();
        // 2. 创建事务拦截器
        TransactionInterceptor transactionInterceptor = new TransactionInterceptor(userService);
        // 3. 创建代理实例
        UserService userServiceProxy = (UserService) transactionInterceptor.getTargetProxy();
        // 4. 使用代理实例调用目标方法
        userServiceProxy.getUserName(6L);
    }
应用场景
  • 虚拟代理,适用于延迟初始化,用小对象表示大对象的场景
  • 保护代理,使用于服务端对客户端的访问控制场景
  • 远程代理,适用于需要本地执行远程服务代码的场景
  • 日志记录代理,适用于需要保存请求对象历史记录的场景
  • 缓存代理,适用于缓存客户请求结果并对缓存生命周期进行管理的场景
代理模式小结

jdk和cglib动态代理对比

  1. JDK动态代理是实现了被代理对象所实现的接口,CGLIB是继承了被代理对象
  2. JDK和CGLIB都是在运行期生成字节码,JDK是直接写CLass字节码,CGLIB使用ASM框架写字节码,CGLIB代理实现更复杂,生成代理类的效率比JDK代理低
  3. JDK调用代理方法,是通过反射机制调用,CGLIB是通过FastClass机制直接调用方法,CGLIB执行效率高

代理模式的优点

  • 代理模式能将代理对象与真实被调用的目标对象分离
  • 一定程度上降低了系统的耦合度,扩展性好
  • 可以起到保护目标对象的作用
  • 可以对目标对象的功能增强

代理模式的缺点

  • 代理模式会造成系统设计中类的数量增加
  • 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢

行为型设计模式

行为型设计模式目的是将多个类或对象相互协作,共同完成单个类或对象无法单独完成的任务。行为型设计模式共11种,分别为解释器模式、模板方法模式、责任链模式、命令模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、访问者模式

行为型模式还可以分为以下两种:

  • 类行为型模式:使用继承的方式来关联不同类之间的行为
  • 对象行为型模式:使用组合或聚合方式来分配不同类之间的行为

模板模式

模板方法模式在一个方法中定义了一个算法框架,并将某些步骤推迟到子类实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的"算法",我们可以理解为广义上的"业务逻辑",并不特指数据结构和算法中的"算法"。这里的算法骨架就是"模板",包含算法骨架的方法就是"模板方法",这也是模板方法模式名字的由来

从这个定义看中,我们能看出模板方法模式的定位很清楚,就是为了解决算法框架这类特定的问题,同时明确表示需要使用继承的结构

关键角色:

  • 抽象父类:定义一个算法所包含的所有步骤,并提供一些通用的方法逻辑

  • 具体子类:继承自抽象父类,根据需要重写父类提供的算法步骤中的某些步骤

实现步骤:

  1. 创建抽象类,模板方法设置为final
  2. 子类继承抽象类,实现抽象方法
  3. 利用多态new子类创建出抽象类的对象,执行模板方法

使用模板方法模式的原因:

  • 期望在一个通用的算法或流程框架下进行自定义开发
  • 避免同样的代码逻辑进行重复编码

源码应用:HttpServlet、spring中的JdbcTemplate、RedisTemplate、RestTemplate

模板模式有两大作用:复用和扩展。复用指的是所有的子类可以复用父类中提供的模板方法的代码;扩展指的是框架通过模板模式提供扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能

小结

  • 通过以上的实现可以看到模板模式在定义统一结构也就是执行标准上非常方便,也就很好的控制了后续的实现者不关心调用逻辑,按照传统方法执行。那么类的继承者只需要关心具体的业务逻辑实现即可
  • 另外模板模式也是为了解决子类通用方法,放到父类中设计的优化。让每个子类只做子类需要完成的内容,而不需要关心其它逻辑。这样提取公共代码,行为由父类管理,扩展可变部分,也就非常有利于开发拓展和迭代
  • 但每一种设计模式都有自己的特定场景,如果超过场景完的建设就需要额外考虑其他模式的运用。而不是非要生搬硬套,否则自己不清楚为什么这么做,也很难让后续者继续维护代码。而想要活学活用就需要多加练习

访问者模式

范围访问者设计模式的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。

访问者要解决的核心事项是,在一个稳定的数据结构下,例如用户信息,雇员信息等,增加易变的业务访问逻辑。为了增强扩展性,将这两部分的业务解耦的一种设计模式。

访问者模式在单个类中定义了一组操作:它为每个类型的对象定义一个方法,该方法来自它必须操作的结构。

只需创建另一个访问者即可添加一组新操作。

访问者模式包含的关键角色有四个:

  • 访问者类(Visitor):这是一个接口或抽象类,定义声明所有可访问类的访问操作。
  • 访问者实现类(VisitorBehavior):实现在访问者类中声明的所有访问方法。
  • 访问角色类(Element):顶一个一个可以获取访问操作的接口,这是使客户端对象能够"访问"的入口点。
  • 访问角色实现类(ElementA等):实现访问角色类接口的具体实现类,将访问者对象传递给此对象作为参数。

访问者模式常用场景:

  • 当对象的数据结构相对稳定,而操作却经常变化的时候
  • 需要将数据结构与不常用的操作进行分离的时候
  • 需要在运行时动态决定使用哪些对象和方法的时候

所以说,访问者模式重点关注不同类型对象在运行时动态进行绑定,以及对多个对象增加统一操作的场景。

访问者模式可以将对象和对象的操作进行分离,尤其是在操作变化比较频繁的时候,提高了扩展性,可维护性。在需要操作的对象中定义一个钩子方法,该方法接收访问者实例,调用访问者的visit方法,并将当前对象引用传递给访问者,由访问者进行操作。但是影响了可读性,且访问者模式使用场景比较少,不推荐使用。

策略模式

策略模式在实际的开发中很常用,最常见的应用场景是利用它来替换过多的if-else嵌套的逻辑判断。策略模式的原始定义是:定义一系列算法,封装每个算法,并使它们可以互换。在这个定义中,策略模式明确表示应当由客户端自己决定在什么样的情况下使用哪些具体的策略。也就是说,服务端作为一个策略的整体调控者,具体选择运行哪些策略其实是要交给客户端来决定。比如,压缩文件的时候,你应该会提供一系列的不同压缩策略,比如gzip、zip等,至于客户在什么时候使用gzip,由客户端自行决定。同时,gzip可以被替换为其它的压缩策略。

策略模式包含三个关键角色:

  • 上下文信息类(Context):用于存放和执行需要使用的具体策略类以及客户端调用的逻辑。
  • 抽象策略类(Strategy):定义策略的共有方法。
  • 具体策略类(StrategyA等):实现抽象策略类定义的共有方法。

为什么使用策略模式

  • 为了提升代码的可维护性
  • 为了动态快速地替换更多的算法
  • 为了应对需要频繁更换策略的场景

小结

  • 策略模式的案例相对来说并不复杂,主要的逻辑都是体现在关于不同种类优惠券的计算折扣策略上。结构相对来说也比较简单,在实际的开发中这样的设计模式也是非常常用的。另外这样的世纪模式与命令模式、适配器模式结构类似,但是思路是有差异的
  • 通过策略世纪模式的使用可以把我们方法中的if语句优化掉,大量的if语句使用会让代码难以扩展,也不好维护,同时在后期遇到各种问题也很难维护。在使用这样的设计模式后可以很好的满足隔离性与扩展性,对于不断新增的需求也非常方便承接
  • 策略模式、适配器模式、组合模式等,在一些结构上是比较相似的,但是每一个模式都有自己的逻辑特点,在使用的过程中最佳的方式是经过较多的实践来吸取经验

状态模式

状态模式的应用场景非常广泛,比如,线上购物订单、手机支付、音乐播放器、游戏、工作流引擎等场景。状态模式设计的初衷是应对同一个对象里不同状态变化时的不同行为的变化

状态模式的原始定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样

这个定义确实有点抽象,简单来说,状态模式就是让一个对象通过一系列状态的变化来控制行为的变化。比如,给购买的物品定义几个包裹运送状态,已下单、运送中、已签收等,当"已下单"状态变为"运送中"状态时,物流货车会把包装好的包裹运送到指定地址,也就是说,当包裹的状态发生改变时,就会触发相应的外部操作

状态模式包含的关键角色有三个:

  • 上下文信息类(Context):实际上就是存储当前状态的类,对外提供更新状态的操作
  • 抽象状态类(State):可以是一个接口或抽象类,用于定义声明状态更新的操作方法有哪些
  • 具体状态类(StateA等):实现抽象状态类定义的方法,根据具体的场景来指定对应状态改变后的代码实现逻辑

状态模式设计的核心点在于找到合适的抽象状态以及状态之间的转移关系,通过改变状态来达到改变行为的目的

状态模式常见的使用场景:

  • 对象根据自身状态的变化来进行不同行为的操作,比如,购物订单状态
  • 对象需要根据自身变量的当前值改变行为,不期望使用大量if-else语句时,比如商品库存状态
  • 对于某些确定的状态和行为,不想使用重复代码,比如,某一个会员当天的购物浏览记录

为什么使用状态模式

  • 当要设计的业务具有复杂的状态变迁时,我们期望通过状态变化来快速进行变更操作,并降低代码耦合性
  • 避免增加代码的复杂性

观察者模式

观察者模式是一种非常流行的设计模式,也常被叫做订阅-发布模式。观察者模式在现在的软件开发中应用非常广泛,比如,商品系统、物流系统、监控系统、运营数据分析系统等。现在我们常说的基于事件驱动的架构,其实也是观察者模式的一种最佳实践。当我们观察某一个对象时,对象传递出的每一个行为都被看成一个事件,观察者通过处理每一个事件来完成自身的操作处理。

定义:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。一般情况下,被依赖的对象叫做被观察者,依赖的对象叫做观察者。

相关角色:发布者-订阅者、生产者-消费者、事件发布-事件监听,不管怎么叫,这些模式在本质上都是观察者模式

观察者模式包含四个关键角色:

  • 发布者(Publisher):也被叫做主题、被订阅者、被观察者等,通常是指观察者关心的相关对象集合,比如,将GitLab上的Git库作为发布者,我们关心Git库的代码发生变更后做特定的操作
  • 具体发布者(PublishImpl):实现了发布者定义的方法的具体实现类
  • 订阅者(Observer):也叫作观察者,它会存储一个注册列表,用于存放订阅者。当发布者发布消息或事件时,会通知订阅者进行处理
  • 具体订阅者(ObserverImpl):实现具体定义的方法操作

观察者模式常见的使用场景:

  • 当一个对象状态的改变需要改变其他对象时
  • 一个对象发生改变时只想要发送通知,而不需要知道接受者是谁
  • 需要创建一种链式触发机制
  • 微博或微信朋友圈发送的场景
  • 需要建立基于事件触发的场景

为什么使用观察者模式

  1. 为了方便捕获观察对象的变化并及时做出相应的操作
  2. 为了提升代码扩展性

备忘录模式

相较于其它的设计模式,备忘录模式不算太常用,但好在这个模式理解、掌握起来并不难,代码实现也比较简单,应用场景就更是比较明确和有限,一般应用于编辑器或会话上下文中防丢失、撤销、恢复等场景中

备忘录模式用于在不破坏对象封装的前提下保存对象的内部状态,并在后面某个阶段恢复对象的这个状态

备忘录模式包含两个关键角色

  • 原始对象(Originator):除了创建自身所需要的属性和业务逻辑外,还通过提供方法create()和restore(memento)来保存和恢复对象副本
  • 备忘录(Memento):用于保存原始对象的所有属性状态,以便在未来进行撤销操作

使用场景分析:模拟系统发布上限的过程中记录线上配置文件用于紧急回滚

为什么使用备忘录模式:

  • 为了记录多个时间点的备份数据
  • 需要快速撤销当前操作并恢复到某对象状态

中介者模式

中介者模式(Mediator Pattern)又称为调节者模式或者调停者模式,使用来降低多个对象和类之间的通信复杂性

中介者模式中用一个中介对象封装一系列的对象交互,从而使各个对象不需要显式的相互作用,达到松耦合的目的,使得维护更加容易。让某些对象之间的作用发生改变时,不会立刻影响其他的一些对象之间的作用,保证了对象之间的相互作用可以独立的变化

中介者模式包含四个关键角色:

  • 抽象中介者(Mediator):定义中介者需要执行的方法操作
  • 具体中介者(MediatorImpl):实现抽象中介者定义的方法操作,同时可以包含更多逻辑
  • 抽象组件类(Component):定义组件需要执行的方法操作
  • 具体组件类(ComponentA等):继承自抽象组件类,实现具体的组件业务逻辑

使用场景:

  • 系统中对象之间存在复杂的引用关系时
  • 通过一个中间对象来封装多个类中的共有行为时
  • 不想生成太多子类时

为什么使用中介者模式

  • 解决对象之间直接耦合的问题,避免"一处修改多处"的连锁反应出现
  • 在结构上作为中转,解耦两个服务或系统之间的直接耦合关系
  • 为了更便捷地统一协同对象之间的通信

迭代器模式

迭代器模式是我们学习一个设计时很少用到的、但编码实现时却经常使用到的行为型设计模式。在绝大多数编程语言中,迭代器已经成为一个基础的类库,直接用来遍历集合对象。在平时开发中,我们更多的是直接使用它

迭代器模式又叫游标(Cursor)模式,它的原始定义是:迭代器提供一种对容器对象中的各个元素进行访问的方法,而又不需要暴露该对象的内部细节。

  • 迭代器模式可以为不同容器提供一致的遍历行为,而且不用关心内部元素的组成结构
  • 迭代器模式的本质就是抽离集合对象迭代行为到迭代容器中,并提供统一的访问接口
  • 迭代器模式可以遍历不同的数据结构元素,这些数据结构包括:数组、链表、树等,而用户在使用遍历的时候并不需要去关心每一种数据结构的遍历处理逻辑,从而让使用变得统一易用

迭代器的四个关键角色:

  • 抽象集合类(Aggregate):创建和抽象迭代器类相关联的方法,同时可以添加其它集合类需要的方法
  • 具体集合类(ConcreteAggregate):实现抽象集合类声明的所有方法,在具体使用集合类时会创建对应具体的迭代器类
  • 抽象迭代器类(Iterator):定义统一的迭代器方法hasNext()和next(),用于判断当前集合中是否还有对象以及按顺序读取集合中的当前对象
  • 具体迭代器类(ConcreteIterator):实现了抽象迭代器类声明的方法,处理具体结合中对对象位置的偏移以及具体对象数据的传输

迭代器模式的使用场景:

  1. 访问一个聚合对象的内容而无须暴露它的内部表示
  2. 需要为聚合对象提供多种遍历方式
  3. 为遍历不同的聚合结构提供一个统一的接口

为什么使用迭代器模式

  1. 减少程序中重复的遍历代码
  2. 为了隐藏统一遍历集合的方法逻辑

解释器模式

解释器模式通常用来描述如何构建一个简单"语言"的语法解释器。它只在一些非常特定的领域被用到,比如编译器、规则引擎、正则表达式、SQL解析等。不过了解它的实现原理同样很重要,能帮助你思考如何通过更简洁的规则来表示复杂的逻辑

解释器模式(Interpreter Pattern)是指给定一门语言,定义它的文法的一种表示(如:加减乘除表达式和正则表达式等),然后再定义一个解释器,该解释器用来解释我们的文法表示(表达式)

语法也称文法,在语言学中指任意自然语言中句子、短语以及词等语法单位的语法结构与语法意义的规律。比如,在编程语言中,if-else用作条件判断的语法,for用于循环语句的语法标识。

解释器模式包含的关键角色有四个:

  • 抽象表达式(AbstractExpression):定义一个解释器有哪些操作,可以是抽象类或接口,同时说明只要继承或实现的子节点都需要实现这些操作方法
  • 终结符表达式(TerminalExpression):用于解释所有终结符表达式
  • 非终结符表达式(NonterminalExpression):用于解释所有非终结符表达式
  • 上下文(Context):包含解释器之外的全局信息。一般用来存放文法中各个终结符所对应的具体值

终结符表达式(Terminal Expression):实现文法中与终结符有关的解释操作。文法中每一个终结符都有一个具体的终结符表达式与之相对应。比如我们的R=M+N运算,M和N就是终结符,对应的解析M和N的解释器就是终结符表达式

非终结符表达式(Nonterminal Expression):实现文法中与非终结符有关的解释操作。文法中的每一条规则都对应了一个非终结符表达式。非终结符表达式一般是文法中的运算符或者关键字,如上面公式:R=M+N中的"+"号就是非终结符,解析"+"号的解释器就是一个非终结符表达式

解释器模式使用场景:

  • 当语言的语法较为简单并且对执行效率不高时。比如,通过正则表达式来寻找IP地址,就不需要对四个网段都进行0-255的判断,而是满足dIP地址格式的都能被找出来
  • 当问题重复出现时,且可以使用一种简单的语言来进行表达时。比如,使用if-else来做条件判断语句,当代码中出现if-else的语句块时都统一解释为条件语句而不需要每次都重新定义和解释
  • 当一个语言需要解释执行时。如XML文档中<>括号表示的不同的节点含义

为什么使用解释器模式

  1. 将领域语言(即问题表征)定义为简单的语言语法。这样做的目的是通过多个不同规则的简单组合来映射复杂的模型。比如,在中文语法中会定义主谓宾这样的语法规则,当我们写了一段文字后,其实是可以通过主谓宾这个规则来进行匹配的。如果只是一个汉字一个汉字地解析,解析效率会非常低,而且容易出错。同理,在开发中我们可以使用正则表达式来快速匹配IP地址,而不是将所有可能的情况都用if-else来进行编写
  2. 更便捷地提升解释数学公式这一类场景的计算效率。我们都知道,计算机在计算加减乘除一类的数学运算时,和人类计算的方式是完全不同的,需要通过一定的规则运算才能得出最后的结果。比如,3+2-(4×5),如果我们不告诉计算机要运算括号中的表达式,计算机则只会按照顺序进行计算,这显然是错误的。而使用解释器模式,则能很好地通过预置的规则来进行判断和解释

解释器模式优点:扩展性比较强。从上面的示例中可以看出来,每一个表达式就是一个类,所以如果需要修改某一个负责只需要修改对应的表达式就可以了,扩展的时候新增一个新类就可以

解释器模式缺点:当文法规则比较复杂时,会引起类膨胀,比较难维护;当文法规则比较复杂时,如果出错了,吊事比较困难;执行效率比较低下,因为当表达式比较复杂时,结果层层依赖的话会采用递归方式进行解析

命令模式

命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

命令模式解耦了请求方和接收方,请求方只需要发送命令而不需要关心命令是如何被接收的,不关心命令怎么操作,也不关心命令是否被执行等

主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适

命令模式中几个重要的角色:

  • 抽象命令类:声明执行命令的接口和方法
  • 具体的命令实现类:接口类的具体实现,可以是一组相似的行为逻辑
  • 实现者:也就是为命令做实现的具体实现类
  • 调用者:处理命令、实现的具体操作者,负责对外提供命令服务

命令模式使用场景分析:

  • 需要通过操作来参数化对象时。比如,当我们将鼠标移动到网页的下拉菜单时,获取下拉列表的同时还能点菜单项
  • 想要将操作放入队列、按顺序执行脚本操作或者执行一些远程操作命令时。比如,先SSH登录远程服务器,再tail查询某个目录下的日志文件,并将日志打印回显到网页的某个窗口中
  • 实现操作回滚功能的场景时。虽然备忘录模式也能够实现,但是命令模式能够更好地记录命令操作的顺序和相关的上下文信息

命令模式的作用:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开

JDK中的线程java.lang.Thread,使用了命令模式

为什么使用命令模式:

  1. 只关心具体的命令和动作,不想知道具体的接受者是谁以及如何操作。在实际的开发中,有时我们经常需要向某些对象发送请求,但又不知道请求的接受者是谁。
  2. 为了围绕命令的维度来构建功能。比如,可以构建上传、下载、打开、关闭这样的命令,这样更符合人类自然的思考逻辑,同时避免了使用者需要了解大量的代码实现逻辑,起到隐藏代码逻辑的作用。
  3. 为了方便统计跟踪行为操作。比如,对于数据的排序、序列化、跟踪、日志记录等操作。使用命令模式能够便捷地记录相关操作,例如,执行撤销和重做操作时,可以从执行的命令列表中快速找到相关操作进行重置,弥补了备忘录模式的缺点。像一些需要读取大量数据的场景中,使用命令模式来读取上下文信息,还能避免内存移除的风险。

责任链模式

责任链模式的原始定义是:通过为多个对象提供处理请求的机会,避免将请求的发送者与其接收者耦合。链接接受对象并沿着链传递请求,直到对象处理它

这个定义读起来还是有点抽象难懂,实际上它只说了一个关键点:通过构建一个处理流水线来对一次请求进行多次的处理

责任链模式只有两个关键角色:

  • 处理类(Handler):可以是一个接口,用于接收请求并将请求分派到处理程序链条中,其中,会先将链中的第一个处理程序放入开头来处理
  • 具体处理类(HandlerA、B、C):按照链条顺序对请求进行具体处理

SpringMVC中的拦截器也用到了责任链模式,使用时,拦截器类需要实现HandlerInterceptor接口,同时使用配置文件进行配置

责任链模式使用场景分析:

  • 在运行时需要动态使用多个关联对象来处理同一次请求时。比如,请假流程、员工入职流程、编译打包发布上线流程等
  • 不想让使用者知道具体的处理逻辑时。比如,做权限校验的登录拦截器
  • 需要动态更换处理对象时。比如,工单处理系统、网关API过滤规则系统等

为什么使用责任链模式

  1. 解耦使用者和后台庞大的流程化处理。我们都知道,在线购物订单里包含了物流、商品、支付、会员等多个系统的处理逻辑,如果让使用者一一和它们对接,势必会造成使用困难、系统之间调用混乱的情况发生,而通过订单系统建立一个订单的状态变更流程,就能将这些系统很好地串联在一起,这不仅能够让使用者只需要关注订单流程这一个入口,同时还能够让不同的系统按照各自的职责来发挥作用。比如,在订单未完成支付前,商品系统是不会通知物流系统进行商品发货的
  2. 为了动态更换流程处理中的处理对象。比如,在请假流程中,申请人一般会提交申请给直接领导审批,但有时直接领导可能无法进行审批操作,这时系统就可以更换审批人到其他审批人,这样就不会阻塞请假流程的审批
  3. 为了处理一些需要递归遍历的对象列表。比如,权限的规则过滤。对于不同部门不同级别人员的权限,就可以采用一个过滤链条来进行权限的管控
相关推荐
程序员Xu4 小时前
【LeetCode热题100道笔记】前 K 个高频元素
笔记·算法·leetcode
TechNomad4 小时前
设计模式:备忘录模式(Memento Pattern)
设计模式·备忘录模式
智者知已应修善业4 小时前
【51单片机8*8点阵显示箭头动画详细注释】2022-12-1
c语言·经验分享·笔记·嵌入式硬件·51单片机
匈牙利认真的小菠萝5 小时前
Windows环境下实现GitLab与Gitee仓库代码提交隔离
笔记
尘鹄9 小时前
go 初始化组件最佳实践
后端·设计模式·golang
悠哉悠哉愿意11 小时前
【机器学习学习笔记】线性回归实现与应用
笔记·学习·机器学习
大筒木老辈子11 小时前
Linux笔记---计算机网络概述
linux·笔记·计算机网络
沐小侠14 小时前
软件设计师——软件工程学习笔记
笔记·学习·软件工程
汇能感知15 小时前
光谱相机在手机行业的应用
经验分享·笔记·科技