设计模式学习(十一)责任链模式

news/2024/5/19 15:05:46 标签: 设计模式, 学习, 责任链模式

目录

    • 一、定义
      • 1.1 主要成员
      • 1.2 优点
      • 1.3 缺点
    • 二、使用场景
      • 2.1 Spring Security 中的应用
    • 三、代码示例
      • 3.1 场景及思路
      • 3.2 实体类
      • 3.3 抽象处理者
      • 3.4 具体处理者
        • 1)责任链容器
        • 2)校验-用户名
        • 3)校验-手机号
        • 4)校验-密码
      • 3.5 客户端(测试类)
      • 3.6 测试结果
    • 四、补充:SpringBoot 的 @Order 注解实现
      • 4.1 实体类
      • 4.2 抽象处理者
      • 4.3 具体处理者
        • 1)责任链容器
        • 2)校验-用户名
        • 3)校验-手机号
        • 4)校验-密码
      • 4.4 客户端(测试类)
      • 4.5 测试结果

一、定义

责任链模式 是一种行为设计模式,它可以将请求从一个对象传递到另一个对象,知道找到能够处理该请求的对象为止。

责任链模式中,每个对象代表一个处理请求的节点,并持有一个指向下一个节点的引用。当一个请求进入责任链时,第一个节点会尝试处理该请求,如果该节点无法处理请求,则将请求传递给下一个节点。这个过程会一直持续下去,直到找到一个能够处理请求的节点或者整个链结束。

1.1 主要成员

  • 抽象处理者(Abstract Handler): 定义了处理请求的接口,并持有一个指向下一个处理者的引用。
  • 具体处理者(Concrete Handler): 实现抽象处理者接口,具体处理请求的逻辑。如果它无法处理请求,可以将请求传递给下一个处理者。
  • 客户端(Client): 创建责任链并将请求发送到责任链的第一个节点。

1.2 优点

1)解耦发起者和处理者: 发起者不需要知道处理请求的具体者,只需要将请求发送给责任链的第一个节点即可,而具体的处理者由责任链自动决定。

2)提高灵活性和扩展性:可以随时添加、修改或删除处理者节点,以满足不同的需求和业务场景。

3)可以动态地改变处理顺序:可以根据具体情况来灵活地调整节点的顺序,以适应不同的处理逻辑。

1.3 缺点

1)性能问题:由于责任链模式需要依次传递请求给每个节点,可能会导致处理时间比较长,特别是当责任链中的节点数量很大时。此外,节点的处理顺序也可能影响性能,如果节点的处理逻辑和顺序设计不好,可能会导致性能瓶颈。

2)无法保证请求被处理责任链模式中,请求被传递给责任链中的节点,直到找到能够处理请求的节点为止。如果整个责任链都无法处理请求,那么请求可能会被无视或丢失。

3)可能导致调试困难:由于责任链模式将请求传递给多个节点进行处理,当出现问题时,可能会难以追踪到具体是那个节点处理出了问题。

4)责任链的长度和复杂性责任链模式中,整个链路可能包含很多节点,特别是在复杂的业务需求下, 责任链的长度和复杂性可能会变得很高。这会使责任链的创建、维护和理解变得困难。

因此,在应用责任链模式时,需要权衡其优点和缺点,根据具体需求和场景来决定是否使用责任链模式以及如何进行设计和使用。


二、使用场景

模板模式策略模式责任链模式 这三种模式具有相同的作用:复用和扩展。在日常开发中,主要用于替换复杂的 if-else 分支判断。

2.1 Spring Security 中的应用

例如:Spring Security 中的过滤器链可以看作是一种责任链模式的实现。

在 Spring Security 中,存在一个特殊的过滤器链,用于处理 Web 请求的安全认证和授权。这个过滤器链由一系列的过滤器组成,每个过滤器都扮演者特定的角色和功能,如身份验证、授权处理、会话管理等。

当一个 Web 请求进入 Spring Security 的过滤链式,请求会依次经过每个过滤器进行处理。每个过滤器会根据自己的功能进行处理,并决定是否将请求传递给下一个过滤器。 如果当前过滤器无法处理请求,可以将请求传递给下一个过滤器,直到找到能够处理该请求的过滤器为止。

在这个过程中,每个过滤器都持有一个指向下一个过滤器的引用,形成了一个链式结构。这种方式可以 有效地解耦和组织处理逻辑,使得责任链中的每个过滤器都只需要关注自己的功能,而不需要关注整个过滤器链的处理过程


三、代码示例

3.1 场景及思路

需求:

  • 账号注册时进行校验,先后校验姓名、密码、手机号等。

使用责任链默认实现上述需求,可以消除很多 if-else 分支,增加功能的扩展性。
如果在责任链中增加一个校验,只需新建一个类即可,这个类就是责任链中的请求元素,可以选择性使用一个、多个或所有的请求对象。

责任链的具体实现方式有两种:

  • 链表式: 将下一个节点保存到当前节点的属性中,每次调用前通过 add() 设定下一个节点。
  • 数组式: 将所有节点保存到数组中,每次调用遍历数组。

我们这里以数组式为例,包结构如下:

在这里插入图片描述

3.2 实体类

UserInfo.java

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserInfo {

    /**
     * 姓名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 电话号码
     */
    private String phoneNumber;
}

3.3 抽象处理者

Verify.java

import com.demo.entity.UserInfo;

public interface Verify {

    /**
     * 验证过程
     *
     * @param userInfo  用户信息
     * @param chain     下一个验证节点
     */
    void doVerify(UserInfo userInfo, VerifyChain chain);

}

3.4 具体处理者

1)责任链容器

VerifyChain.java(用 ThreadLocal 做线程隔离)

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;

import java.util.ArrayList;
import java.util.List;

public class VerifyChain {

    /**
     * 验证节点集合
     */
    private List<Verify> verifyList = new ArrayList<>();

    /**
     * 验证节点索引
     */
    private ThreadLocal<Integer> index = ThreadLocal.withInitial(() -> 0);

    /**
     * 添加验证节点
     */
    public VerifyChain addVerify(Verify verify) {
        verifyList.add(verify);
        return this;
    }

    /**
     * 开始验证
     * @param userInfo  用户信息
     */
    public void doVerify(UserInfo userInfo) {
        if (index.get() == verifyList.size()) {
            // 重置索引
            index.set(0);
            return;
        }

        System.out.println("当前线程:" + Thread.currentThread().getName() +
                ",索引:" + index.get() +
                ",验证节点:" + verifyList.get(index.get()).getClass().getSimpleName() +
                ",验证节点数量:" + verifyList.size());

        Verify verify = verifyList.get(index.get());
        index.set(index.get() + 1);
        verify.doVerify(userInfo, this);
    }
}
2)校验-用户名

UserNameVerify.java

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class UserNameVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getUserName())) {
            System.out.println("用户名不能为空");
            return;
        }
        System.out.println("用户名验证通过");
        chain.doVerify(userInfo);
    }
}
3)校验-手机号

PhoneNumberVerify.java

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class PhoneNumberVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPhoneNumber()) || userInfo.getPhoneNumber().length() != 11) {
            System.out.println("手机号码格式不正确");
            return;
        }
        System.out.println("手机号码验证通过");
        chain.doVerify(userInfo);
    }
}
4)校验-密码

PasswordVerify.java

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class PasswordVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPassword())) {
            System.out.println("密码不能为空");
            return;
        }
        System.out.println("密码验证通过");
        chain.doVerify(userInfo);
    }
}

3.5 客户端(测试类)

import com.demo.chain.PasswordVerify;
import com.demo.chain.PhoneNumberVerify;
import com.demo.chain.UserNameVerify;
import com.demo.chain.VerifyChain;
import com.demo.entity.UserInfo;

public class MainTest {

    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("ACGkaka", "123456", "12345678901");
        VerifyChain verifyChain = new VerifyChain();
        // 校验顺序:用户名 -> 密码 -> 电话号码
        verifyChain.addVerify(new UserNameVerify())
                .addVerify(new PasswordVerify())
                .addVerify(new PhoneNumberVerify());
        verifyChain.doVerify(userInfo, verifyChain);
    }
}

3.6 测试结果

单线程执行,可以看到,校验顺序与预期保持一致:用户名 -> 手机号码 -> 密码

在这里插入图片描述

可以调整代码中 addVerify 的顺序:手机号码 -> 用户名 -> 密码。

在这里插入图片描述

打开 thread2 注释多线程执行,可以看到,ThreadLocal生效,保证线程安全。

在这里插入图片描述

四、补充:SpringBoot 的 @Order 注解实现

需求:

  • 账号注册时进行校验,先后校验姓名、密码、手机号等。

4.1 实体类

UserInfo.java (保持不变)

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserInfo {

    /**
     * 姓名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 电话号码
     */
    private String phoneNumber;
}

4.2 抽象处理者

Verify.java(保持不变)

import com.demo.entity.UserInfo;

public interface Verify {

    /**
     * 验证过程
     *
     * @param userInfo  用户信息
     * @param chain     下一个验证节点
     */
    void doVerify(UserInfo userInfo, VerifyChain chain);

}

4.3 具体处理者

1)责任链容器

VerifyChain.java(用 ThreadLocal 做线程隔离,并且利用 Spring 自动注入责任链)

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

@Component
public class VerifyChain {

    /**
     * 验证节点集合(spring会根据 @Order 注解顺序注入)
     */
    @Resource
    private List<Verify> verifyList;

    /**
     * 验证节点索引
     */
    private ThreadLocal<Integer> index = ThreadLocal.withInitial(() -> 0);

    /**
     * 开始验证
     * @param userInfo  用户信息
     */
    public void doVerify(UserInfo userInfo) {
        if (index.get() == verifyList.size()) {
            // 重置索引
            index.set(0);
            return;
        }

        System.out.println("当前线程:" + Thread.currentThread().getName() +
                ",索引:" + index.get() +
                ",验证节点:" + verifyList.get(index.get()).getClass().getSimpleName() +
                ",验证节点数量:" + verifyList.size());

        Verify verify = verifyList.get(index.get());
        index.set(index.get() + 1);
        verify.doVerify(userInfo, this);
    }
}
2)校验-用户名

UserNameVerify.java(增加 @Component、@Order注解,来注入责任链并控制顺序)

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(1) // 数字越小,越先执行
@Component
public class UserNameVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getUserName())) {
            System.out.println("用户名不能为空");
            return;
        }
        System.out.println("用户名验证通过");
        chain.doVerify(userInfo);
    }
}
3)校验-手机号

PhoneNumberVerify.java(增加 @Component、@Order注解,来注入责任链并控制顺序)

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(2) // 数字越小,越先执行
@Component
public class PhoneNumberVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPhoneNumber()) || userInfo.getPhoneNumber().length() != 11) {
            System.out.println("手机号码格式不正确");
            return;
        }
        System.out.println("手机号码验证通过");
        chain.doVerify(userInfo);
    }
}
4)校验-密码

PasswordVerify.java(增加 @Component、@Order注解,来注入责任链并控制顺序)

package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(3) // 数字越小,越先执行
@Component
public class PasswordVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPassword())) {
            System.out.println("密码不能为空");
            return;
        }
        System.out.println("密码验证通过");
        chain.doVerify(userInfo);
    }
}

4.4 客户端(测试类)

package com.demo;

import com.demo.test.chain.VerifyChain;
import com.demo.test.entity.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootDemoApplicationTests {

    @Autowired
    private VerifyChain verifyChain;

    @Test
    void testVerify() {
        UserInfo userInfo = new UserInfo("ACGkaka", "123456", "12345678901");
        // 校验顺序:用户名 -> 手机号码 -> 密码
        Runnable runnable = () -> verifyChain.doVerify(userInfo);
        Thread thread1 = new Thread(runnable);
//        Thread thread2 = new Thread(runnable);
        thread1.start();
//        thread2.start();
    }

}

4.5 测试结果

单线程执行,可以看到,校验顺序与预期保持一致:用户名 -> 手机号码 -> 密码

在这里插入图片描述

可以调整 @Order 中的顺序:手机号码 -> 用户名 -> 密码。

在这里插入图片描述

打开 thread2 注释多线程执行,可以看到,ThreadLocal生效,保证线程安全。

在这里插入图片描述

整理完毕,完结撒花~ 🌻





参考地址:

1.责任链模式,https://zhuanlan.zhihu.com/p/509058039

2.【设计模式责任链模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 ),https://blog.csdn.net/shulianghan/article/details/118188083

3.SpringBoot中filter的使用详解及原理,https://blog.csdn.net/u014627099/article/details/84565603

4.用spring boot的@Order注解实现责任链模式,https://blog.csdn.net/qq_44993268/article/details/131020677?spm=1001.2014.3001.5501


http://www.niftyadmin.cn/n/5077652.html

相关文章

Fourier变换的乘积定理及其详细证明过程

Fourier变换的乘积定理及其证明过程 Fourier变换的乘积定理是进行有关Fourier相关运算的有力工具。借助它&#xff0c;可以让有的频域乘积计算或时间域计算有效转化&#xff0c;避开复杂的基于定义的计算过程&#xff0c;使得计算过程快捷方便&#xff0c;本博文在此&#xff…

三点式振荡器

相关说明 http://www.360doc.com/content/19/0527/16/61619294_838545271.shtml 高频信号发生器设计—电容三点式振荡电路_电容三点式振荡电路工作原理_北辰-尘的博客-CSDN博客 如上图所示&#xff0c;典型的Colpitts振荡电路。首先忽略所有的电阻&#xff0c;他们是用来设置…

编程之路:C++旅程回顾

编程之路&#xff1a;C旅程回顾 1. 函数与参数1.1 传值参数1.2 模板函数1.3 引用参数1.4 常量引用参数1.5 返回值1.6 重载函数 2. 异常2.1 抛出异常2.2 处理异常 3. 动态存储空间分配3.1 操作符new3.2 一维数组3.3 异常处理3.4 操作符delete3.5 二维数组 1. 函数与参数 1.1 传…

HP打印机一点击打印就出现Windows资源管理器已停止工作问题解决

本次处理的打印机型号是HP Officejet 200 移动便携式打印机&#xff0c;不过其他型号如果出现类似现象&#xff0c;解决方法应该是一致的。 在弹出Windows资源管理器已停止工作的报错提示框后&#xff0c;点击左下角的详细信息&#xff0c;看到的内容显示是KernelBase.dll崩溃…

git常用命令和开发常用场景

git命令 git init 创建一个空的git仓库或者重新初始化已有仓库 git clone [url] 将存储库克隆到新目录 git add 添加内容到索引 git status 显示工作树状态 git commit -m "" 记录仓库的修改 git reset 重置当前HEAD到指定的状态 git reset –-soft&#xff1a;…

P4147 玉蟾宫

题目 P4147 玉蟾宫 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 方法 悬线法 1.确定每行每个元素能够取到的左右边界 2.确定每行每个元素能够取到的上边界 代码 //悬线法 #include<iostream> #include<algorithm> using namespace std; const int N 10…

教资成绩什么时候出来 2023教资笔试成绩查询时间介绍

上半年教资笔试成绩查询开放时期为2023年4月13日&#xff0c;面试成绩查询开放时间在6月14日。而下半年教资笔试成绩查询开放时间为2023年11月8日&#xff0c;2023下半年教资面试时间是2023年12月9日-10日。 值得一提的是如果考生对成绩有异议的话&#xff0c;还可以在成绩公布…

利用闭包的特点来实现一个简单的缓存

备忘模式就是应用闭包的特点的一个典型应用。比如下面函数&#xff1a; 当多次执行 add() 时&#xff0c;每次得到的结果都是重新计算得到的&#xff0c;如果是开销很大的计算操作的话就比较消耗性能了。这里可以对已经计算过的输入做一个缓存。 function add(a) {return a1;} …