同事写了一个责任链模式,bug 无数...

简介: 同事写了一个责任链模式,bug 无数...

背景

最近,我让团队内一位成员写了一个导入功能。他使用了责任链模式,代码堆的非常多,bug 也多,没有达到我预期的效果。


实际上,针对导入功能,我认为模版方法更合适!为此,隔壁团队也拿出我们的案例,进行了集体 code review。


学好设计模式,且不要为了练习,强行使用!让原本 100 行就能实现的功能,写了 3000 行!对错暂且不论,我们先一起看看责任链设计模式吧!


什么是责任链

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

image.png

使用场景

责任链的使用场景还是比较多的:

  • 多条件流程判断:权限控制
  • ERP 系统流程审批:总经理、人事经理、项目经理
  • Java 过滤器的底层实现 Filter

如果不使用该设计模式,那么当需求有所改变时,就会使得代码臃肿或者难以维护,例如下面的例子。


| 反例

假设现在有一个闯关游戏,进入下一关的条件是上一关的分数要高于 xx:

  • 游戏一共 3 个关卡
  • 进入第二关需要第一关的游戏得分大于等于 80
  • 进入第三关需要第二关的游戏得分大于等于 90

那么代码可以这样写:

//第一关
public class FirstPassHandler {
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        return 80;
    }
}
//第二关
public class SecondPassHandler {
    public int handler(){
        System.out.println("第二关-->SecondPassHandler");
        return 90;
    }
}
//第三关
public class ThirdPassHandler {
    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return 95;
    }
}
//客户端
public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
        int firstScore = firstPassHandler.handler();
        //第一关的分数大于等于80则进入第二关
        if(firstScore >= 80){
            int secondScore = secondPassHandler.handler();
            //第二关的分数大于等于90则进入第二关
            if(secondScore >= 90){
                thirdPassHandler.handler();
            }
        }
    }
}

那么如果这个游戏有 100 关,我们的代码很可能就会写成这个样子:

if(第1关通过){
    // 第2关 游戏
    if(第2关通过){
        // 第3关 游戏
        if(第3关通过){
           // 第4关 游戏
            if(第4关通过){
                // 第5关 游戏
                if(第5关通过){
                    // 第6关 游戏
                    if(第6关通过){
                        //...
                    }
                }
            }
        }
    }
}

这种代码不仅冗余,并且当我们要将某两关进行调整时会对代码非常大的改动,这种操作的风险是很高的,因此,该写法非常糟糕。


更多设计模式系列教程:https://www.javastack.cn/


| 初步改造

如何解决这个问题,我们可以通过链表将每一关连接起来,形成责任链的方式,第一关通过后是第二关,第二关通过后是第三关....


这样客户端就不需要进行多重 if 的判断了:

public class FirstPassHandler {
    /**
     * 第一关的下一关是 第二关
     */
    private SecondPassHandler secondPassHandler;
    public void setSecondPassHandler(SecondPassHandler secondPassHandler) {
        this.secondPassHandler = secondPassHandler;
    }
    //本关卡游戏得分
    private int play(){
        return 80;
    }
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        if(play() >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.secondPassHandler != null){
                return this.secondPassHandler.handler();
            }
        }
        return 80;
    }
}
public class SecondPassHandler {
    /**
     * 第二关的下一关是 第三关
     */
    private ThirdPassHandler thirdPassHandler;
    public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {
        this.thirdPassHandler = thirdPassHandler;
    }
    //本关卡游戏得分
    private int play(){
        return 90;
    }
    public int handler(){
        System.out.println("第二关-->SecondPassHandler");
        if(play() >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.thirdPassHandler != null){
                return this.thirdPassHandler.handler();
            }
        }
        return 90;
    }
}
public class ThirdPassHandler {
    //本关卡游戏得分
    private int play(){
        return 95;
    }
    /**
     * 这是最后一关,因此没有下一关
     */
    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return play();
    }
}
public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
        firstPassHandler.setSecondPassHandler(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setThirdPassHandler(thirdPassHandler);//第二关的下一关是第三关
        //说明:因为第三关是最后一关,因此没有下一关
        //开始调用第一关 每一个关卡是否进入下一关卡 在每个关卡中判断
        firstPassHandler.handler();
    }
}

| 缺点

现有模式的缺点:


每个关卡中都有下一关的成员变量并且是不一样的,形成链很不方便

代码的扩展性非常不好

###


| 责任链改造

既然每个关卡中都有下一关的成员变量并且是不一样的,那么我们可以在关卡上抽象出一个父类或者接口,然后每个具体的关卡去继承或者实现。


有了思路,我们先来简单介绍一下责任链设计模式的基本组成:


  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

image.png

public abstract class AbstractHandler {
    /**
     * 下一关用当前抽象类来接收
     */
    protected AbstractHandler next;
    public void setNext(AbstractHandler next) {
        this.next = next;
    }
    public abstract int handler();
}
public class FirstPassHandler extends AbstractHandler{
    private int play(){
        return 80;
    }
    @Override
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        int score = play();
        if(score >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}
public class SecondPassHandler extends AbstractHandler{
    private int play(){
        return 90;
    }
    public int handler(){
        System.out.println("第二关-->SecondPassHandler");
        int score = play();
        if(score >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}
public class ThirdPassHandler extends AbstractHandler{
    private int play(){
        return 95;
    }
    public int handler(){
        System.out.println("第三关-->ThirdPassHandler");
        int score = play();
        if(score >= 95){
            //分数>=95 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}
public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
        // 和上面没有更改的客户端代码相比,只有这里的set方法发生变化,其他都是一样的
        firstPassHandler.setNext(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setNext(thirdPassHandler);//第二关的下一关是第三关
        //说明:因为第三关是最后一关,因此没有下一关
        //从第一个关卡开始
        firstPassHandler.handler();
    }
}

| 责任链工厂改造

对于上面的请求链,我们也可以把这个关系维护到配置文件中或者一个枚举中。我将使用枚举来教会大家怎么动态的配置请求链并且将每个请求者形成一条调用链。

image.png

public enum GatewayEnum {
    // handlerId, 拦截者名称,全限定类名,preHandlerId,nextHandlerId
    API_HANDLER(new GatewayEntity(1, "api接口限流", "cn.dgut.design.chain_of_responsibility.GateWay.impl.ApiLimitGatewayHandler", null, 2)),
    BLACKLIST_HANDLER(new GatewayEntity(2, "黑名单拦截", "cn.dgut.design.chain_of_responsibility.GateWay.impl.BlacklistGatewayHandler", 1, 3)),
    SESSION_HANDLER(new GatewayEntity(3, "用户会话拦截", "cn.dgut.design.chain_of_responsibility.GateWay.impl.SessionGatewayHandler", 2, null)),
    ;
    GatewayEntity gatewayEntity;
    public GatewayEntity getGatewayEntity() {
        return gatewayEntity;
    }
    GatewayEnum(GatewayEntity gatewayEntity) {
        this.gatewayEntity = gatewayEntity;
    }
}
public class GatewayEntity {
    private String name;
    private String conference;
    private Integer handlerId;
    private Integer preHandlerId;
    private Integer nextHandlerId;
}
public interface GatewayDao {
    /**
     * 根据 handlerId 获取配置项
     * @param handlerId
     * @return
     */
    GatewayEntity getGatewayEntity(Integer handlerId);
    /**
     * 获取第一个处理者
     * @return
     */
    GatewayEntity getFirstGatewayEntity();
}
public class GatewayImpl implements GatewayDao {
    /**
     * 初始化,将枚举中配置的handler初始化到map中,方便获取
     */
    private static Map<Integer, GatewayEntity> gatewayEntityMap = new HashMap<>();
    static {
        GatewayEnum[] values = GatewayEnum.values();
        for (GatewayEnum value : values) {
            GatewayEntity gatewayEntity = value.getGatewayEntity();
            gatewayEntityMap.put(gatewayEntity.getHandlerId(), gatewayEntity);
        }
    }
    @Override
    public GatewayEntity getGatewayEntity(Integer handlerId) {
        return gatewayEntityMap.get(handlerId);
    }
    @Override
    public GatewayEntity getFirstGatewayEntity() {
        for (Map.Entry<Integer, GatewayEntity> entry : gatewayEntityMap.entrySet()) {
            GatewayEntity value = entry.getValue();
            //  没有上一个handler的就是第一个
            if (value.getPreHandlerId() == null) {
                return value;
            }
        }
        return null;
    }
}
public class GatewayHandlerEnumFactory {
    private static GatewayDao gatewayDao = new GatewayImpl();
    // 提供静态方法,获取第一个handler
    public static GatewayHandler getFirstGatewayHandler() {
        GatewayEntity firstGatewayEntity = gatewayDao.getFirstGatewayEntity();
        GatewayHandler firstGatewayHandler = newGatewayHandler(firstGatewayEntity);
        if (firstGatewayHandler == null) {
            return null;
        }
        GatewayEntity tempGatewayEntity = firstGatewayEntity;
        Integer nextHandlerId = null;
        GatewayHandler tempGatewayHandler = firstGatewayHandler;
        // 迭代遍历所有handler,以及将它们链接起来
        while ((nextHandlerId = tempGatewayEntity.getNextHandlerId()) != null) {
            GatewayEntity gatewayEntity = gatewayDao.getGatewayEntity(nextHandlerId);
            GatewayHandler gatewayHandler = newGatewayHandler(gatewayEntity);
            tempGatewayHandler.setNext(gatewayHandler);
            tempGatewayHandler = gatewayHandler;
            tempGatewayEntity = gatewayEntity;
        }
    // 返回第一个handler
        return firstGatewayHandler;
    }
    /**
     * 反射实体化具体的处理者
     * @param firstGatewayEntity
     * @return
     */
    private static GatewayHandler newGatewayHandler(GatewayEntity firstGatewayEntity) {
        // 获取全限定类名
        String className = firstGatewayEntity.getConference();
        try {
            // 根据全限定类名,加载并初始化该类,即会初始化该类的静态段
            Class<?> clazz = Class.forName(className);
            return (GatewayHandler) clazz.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }
}
public class GetewayClient {
    public static void main(String[] args) {
        GetewayHandler firstGetewayHandler = GetewayHandlerEnumFactory.getFirstGetewayHandler();
        firstGetewayHandler.service();
    }
}

设计模式有很多,责任链只是其中的一种,我觉得很有意思,非常值得一学。设计模式确实是一门艺术,仍需努力呀!



image.png

相关文章
|
缓存 编解码 网络协议
一文带你由浅入深Netty异步非阻塞世界
一文带你由浅入深Netty异步非阻塞世界
一文带你由浅入深Netty异步非阻塞世界
|
机器学习/深度学习 人工智能 自动驾驶
深度学习中的卷积神经网络(CNN)及其在图像识别中的应用
【9月更文挑战第19天】在人工智能的浩瀚星海中,卷积神经网络(CNN)如同一颗璀璨的星辰,照亮了图像处理的天空。本文将深入CNN的核心,揭示其在图像识别领域的强大力量。通过浅显易懂的语言和直观的比喻,我们将一同探索CNN的奥秘,并见证它如何在现实世界中大放异彩。
|
弹性计算 运维 自然语言处理
开发者评测|操作系统智能助手OS Copilot
OS Copilot 是阿里云针对Linux操作系统推出的一款智能助手,它利用大模型技术提供自然语言问答、辅助命令执行和系统运维调优等功能,旨在提高Alibaba Cloud Linux的使用效率。OS Copilot在新人上手时并不简单,文档指引不够清晰,存在一些步骤无法顺利执行的问题,比如环境配置和命令执行。此外,产品目前仅支持Alibaba Cloud Linux,限制了其应用范围。虽然OS Copilot在一些基本功能上表现尚可,如解答问题和编写简单脚本,但在处理复杂任务或自动化运维时显得不足。总体而言,OS Copilot对新手和阿里云环境有一定的帮助,但功能和使用体验仍有改进空间。
|
人工智能 自然语言处理 JavaScript
微软开源课程!21节课程教你开发生成式 AI 应用所需了解的一切
微软推出的生成式 AI 入门课程,涵盖 21 节课程,帮助开发者快速掌握生成式 AI 应用开发,支持 Python 和 TypeScript 代码示例。
1334 15
|
机器学习/深度学习 人工智能 自然语言处理
AI与法律行业:智能法律咨询
在科技飞速发展的今天,人工智能(AI)正逐渐渗透到法律行业,特别是在智能法律咨询领域。本文探讨了AI在智能法律咨询中的应用现状、优势及挑战,并展望了其未来发展前景。AI技术通过大数据、自然语言处理等手段,提供高效、便捷、低成本且个性化的法律服务,但同时也面临数据隐私、法律伦理等问题。未来,AI将在技术升级、政策推动和融合创新中,为用户提供更加优质、便捷的法律服务。
1008 3
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
弹性计算 Linux 网络安全
阿里云ECS健康诊断产品详解
介绍阿里云ECS健康诊断产品详情和使用案例,包括各个产品概念、控制、OpenAPI、SDK等。
1383 8
|
人工智能 Kubernetes Cloud Native
阿里云容器服务,智算时代云原生操作系统
今年是Kubernetes十周年,在这10年间。我们已经看到其成长为云原生操作系统,向下高效调度多种算力资源,屏蔽基础设施差异,向上提供统一编程接口,支持多样化工作负载。阿里云容器服务产品已经覆盖了从公共云、边缘云、到本地数据中心的各个场景。让所有需要云能力的地方,都有统一的容器基础设施。
阿里云容器服务,智算时代云原生操作系统
|
人工智能 PyTorch 算法框架/工具
AI计算机视觉笔记二十二:基于 LeNet5 的手写数字识别及训练
本文介绍了使用PyTorch复现LeNet5模型并检测手写数字的过程。通过搭建PyTorch环境、安装相关库和下载MNIST数据集,实现了模型训练与测试。训练过程涉及创建虚拟环境、安装PyTorch及依赖库、准备数据集,并编写训练代码。最终模型在测试集上的准确率达到0.986,满足预期要求。此项目为后续在RK3568平台上部署模型奠定了基础。
|
存储 安全 算法
服务器数据恢复—Raid磁盘阵列的安全性分析及常见故障
出于尽可能避免数据灾难的设计初衷,RAID解决了3个问题:容量问题、IO性能问题、存储安全(冗余)问题。从数据恢复的角度讨论RAID的存储安全问题。 常见的起到存储安全作用的RAID方案有RAID1、RAID5及其变形。基本设计思路是相似的:当部分数据异常时,可通过特定算法将数据还原出来。以RAID5为例:如果要记录两个数字,可以通过再多记录这两个数字的和来达到记录冗余性的目的。例如记录3和5,同时再记录这2个数字的和8。在不记得到底是几和5的情况下,只需要用8-5就可以算出这个丢失的数字了,其余情况依此类推。