设计模式——2_0 职责链(Chain of Responsibility)

news2024/11/18 23:33:38

楼下一个男人并得要死,那家隔壁的一家唱着留声机,对面是弄孩子。楼上有两人狂笑;还有打牌声,河中的船上有女人哭她死去的母亲。人类的悲欢并不相通,我只觉得他们吵闹

——鲁迅



定义

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理她为止

你玩过那种过关式的动作游戏吗?当你和小伙伴进入一个副本之后,必须按照副本的地图通过各个房间,最终干掉boss完成副本。这时候如果把玩家看作 请求,则副本就是一个职责链。所有的房间都有机会干掉玩家, 而玩家也有可能通过所有房间完成副本;而各个房间之间是不存在什么关联的,你可以把他们拆开另外组合



图纸

在这里插入图片描述



一个例子:如何把苹果放到合适的篮子里

无论是工厂、批发商还是其他的产品产出方,在自己的产品从成品到正式出货之前都要经过一系列的流程。哪怕是农民伯伯卖白菜,也会把烂叶子摘掉后再往外卖。现在如果我们要用代码来展示出货前的流程,我们该怎么做呢?

准备好了吗?这次的例子开始了


假定 生产苹果的大型基地A 需要根据采摘到的苹果的外观把苹果分成 X/Y/Z 三种类型的苹果(当然每种类型苹果价格不同) 。现在 A 为了推行全自动的出货模块,购入了一台 苹果外观打分器,这台机器可以根据输入的苹果的外观输出一个 外观分数,你的任务是根据这个 外观分数 和 你拿到的苹果,决定他应该被放到哪个篮子里,整个流程就像这样:

在这里插入图片描述


苹果分配器

为了完成这个任务,我们会创建一个 苹果分配器 让这个苹果分配器根据苹果的外观分数把他们放到不同的篮子里去,就像这样:

在这里插入图片描述

/**
	苹果bean
**/
public class Apple {

    /**
     * 根据苹果外貌评分器写入的外貌分数
     */
    private final float appearance;

    public Apple(float appearance) {
        this.appearance = appearance;
    }

    public float getAppearance() {
        return appearance;
    }
}

/**
 * 苹果分配器
 */
public class AppleDispatcher {

    /**
     * 根据苹果的外貌分数把他们放入不同的篮子里
     */
    public void dispatch(Apple apple) {
        float appearance = apple.getAppearance();

        if (appearance >= 75) {
            //放入X类型的盒子中
        } else if (appearance >= 45) {
            //放入Y类型的盒子里
        } else if (appearance > 0) {
            //放入Z类型的盒子里
        } else {
            //外貌分<=0,证明出现了异常
            throw new RuntimeException("输入的苹果的外貌分不合法,appearance=" + appearance);
        }
    }
}

我们创建了一个 AppleDispatcher 作为苹果分配器。其中有一个 dispatch 方法,接收一个苹果对象作为参数,然后根据里面的外貌分把他们分到不同的篮子里


不同的标准

上文的 AppleDispatcher 很好的完成了他的使命,但是系统运行一段时间后公司收到了一堆投诉,说是Z类型的盒子里出现了 被虫蛀的痕迹和摔烂的情况。经过QA部1同事夜以继日的加班,我们发现问题是出在外观分上:

我们的 苹果外观打分器 并不能识别出虫洞和掉到地上摔坏的痕迹,所以就算被检测的苹果被虫蛀了、烂了,也只是评分低一点,并不会不给他外貌分


于是我们决定修改现有的出货模块,新增以下规则:

  1. 当苹果的外观分<=45时,需要对其进行 外观检查,以判断是否被虫蛀
  2. 当苹果经过 外观检查 后发现没有被虫蛀,而是摔坏了,则输送到牧场喂猪

也就是说,我们的流程要变成这样:

在这里插入图片描述

修改后的流程中新增了两个全新的分支,每一个苹果在进入 AppleDispatcher 的时候都有可能被分配到任何一个分支中,要怎么实现这样的效果呢?


组合对象

显而易见的一种方法,是直接在 AppleDispatcher 中添加逻辑,把所有的可能性都写出来。更进一步,你甚至可以定义一个验证器,用于抽象这个检查的过程,就像这样:

在这里插入图片描述

public class Apple {

    /**
     * 根据苹果外貌评分器写入的外貌分数
     */
    private final float appearance;
    /**
     * 是否有虫
     */
    private boolean haveWorm;
    /**
     * 是否摔伤
     */
    private boolean isFall;

    public Apple(float appearance) {
        this.appearance = appearance;
    }

    public float getAppearance() {
        return appearance;
    }

    public boolean isHaveWorm() {
        return haveWorm;
    }

    public void setHaveWorm(boolean haveWorm) {
        this.haveWorm = haveWorm;
    }

    public boolean isFall() {
        return isFall;
    }

    public void setFall(boolean fall) {
        isFall = fall;
    }
}


public interface Verifier {

    /**
     * 验证
     * @param apple 被验证的苹果
     * @return true:通过验证
     */
    boolean verify(Apple apple);
}

public class WormVerifier implements Verifier{

    @Override
    public boolean verify(Apple apple) {
        return !apple.isHaveWorm();//返还这个苹果是否有虫
    }
}

public class AppearanceVerifier implements Verifier{

    @Override
    public boolean verify(Apple apple) {
        return !apple.isFall();//返还这个苹果是否摔伤
    }
}

/**
 * 苹果分配器
 */
public class AppleDispatcher {

    /**
     * 根据苹果的外貌分数把他们放入不同的篮子里
     */
    public void dispatch(Apple apple) {
        float appearance = apple.getAppearance();

        if (appearance >= 75) {
            //放入X类型的盒子中
        } else if (appearance >= 45) {
            //放入Y类型的盒子里
        } else if (appearance > 0) {
            if(new WormVerifier().verify(apple)){
                //没有虫子
                if(new AppearanceVerifier().verify(apple)){
                    //没有摔伤
                    //放入Z类型的盒子里
                }else {
                    //喂猪
                }
            }else {
                //直接报废
            }

        } else {
            //外貌分<0,证明出现了异常
            throw new RuntimeException("输入的苹果的外貌分不合法,appearance=" + appearance);
        }
    }
}

在这个实现里,我们通过建立 Verifier(验证器) 类簇把不同的验证方式封装到不同的类中,然后在 AppleDispatcher 中通过这些验证器的结果把苹果送到不同的结果分支中去,而且将来如果出现新的验证方式,我不需要修改已有的验证器,只需要新增子项。


但是这依然是很糟糕的设计

你可能发现了,我并没有组合 AppleDispatcherVerifier,而是在 dispatch 方法中做紧耦合。这并不是我不想组合他们,而是我没办法组合他们,就算 Verifier 被解耦出来了,但是 AppleDispatcher 依然需要根据参数苹果在哪个验证器中被识别成异常,来决定苹果的走向

也就是说,AppleDispatcher 依然需要知道整个外观检查流程中的全部信息,只要我修改流程,还是要修改已经封装好的 dispatch 方法,这是我不愿意看到的


我希望有这样一个类的对象,可以在检测的同时直接对苹果做一个流向判断,而且不需要了解上下文此时的状态。而我在外部会给他提供一个调用环境,来调度当前的请求

当然有这样的写法,答案就是本文的主题,职责链


职责链

如果用职责链实现这个需求的话,是这样的:

在这里插入图片描述

AppleCheckExecutor

/**
 * 苹果检查执行器
 */
public abstract class AppleCheckExecutor {

    protected AppleHandler root;//根操作者

    public AppleCheckExecutor() {
        initHandleChain();
    }

    public void addAppleHandler(AppleHandler handler) {
        if (root == null) {
            root = handler;
        } else {
            addAppleHandler(handler, root);
        }
    }

    private void addAppleHandler(AppleHandler handler, AppleHandler lastHandler) {
        AppleHandler nextHandler = lastHandler.getNextHandler();

        if (nextHandler == null) {
            //没有下级Handler
            lastHandler.setNextHandler(handler);
        } else {
            addAppleHandler(handler, nextHandler);
        }
    }

    public void executor(Apple apple) {
        if (root == null) {
            //如果第一个处理器都没有写入,则直接执行默认处理方式
            defaultHandle(apple);
        } else {
            Apple result = root.handle(apple);
            if (result != null) {
                //没有任何处理器相应
                defaultHandle(apple);
            }
        }
    }

    /**
     * 初始化处理器链
     */
    protected abstract void initHandleChain();

    /**
     * 默认处理方式
     */
    protected abstract void defaultHandle(Apple apple);
}

/**
 * 高分执行器
 */
public class HeightExecutor extends AppleCheckExecutor{

    @Override
    protected void initHandleChain() {
        //什么都不写入
    }

    @Override
    protected void defaultHandle(Apple apple) {
        //放入X类型的盒子中
    }
}

/**
 * 中等分数执行器
 */
public class MinExecutor extends AppleCheckExecutor{

    @Override
    protected void initHandleChain() {
        //什么都不写入
    }

    @Override
    protected void defaultHandle(Apple apple) {
        //放入Y类型的盒子里
    }
}

/**
 * 低分执行器
 */
public class LowExecutor extends AppleCheckExecutor{

    @Override
    protected void initHandleChain() {
        //写入外观检查流程
        addAppleHandler(new WormHandler());
        addAppleHandler(new AppearanceHandler());
    }

    @Override
    protected void defaultHandle(Apple apple) {
        //放入Z类型盒子中
    }
}

AppleHandler

/**
 * 苹果验证控制器
 */
public abstract class AppleHandler {

    /**
     * 下一级处理器
     */
    protected AppleHandler nextHandler;

    public AppleHandler() {
    }

    public AppleHandler getNextHandler() {
        return nextHandler;
    }

    public void setNextHandler(AppleHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    /**
     * 验证是否通过这个控制器
     */
    protected abstract boolean verify(Apple apple);

    /**
     * 未通过验证时需要执行的操作
     */
    protected abstract void cannot(Apple apple);

    /**
     * 处理苹果
     */
    public Apple handle(Apple apple) {
        if (verify(apple)) {
            //通过验证,把请求发送到下一级控制器中
            if (nextHandler == null) {
                //请求结束
                return apple;
            } else {
                return nextHandler.handle(apple);
            }
        } else {
            //没有通过验证
            cannot(apple);
            return null;
        }
    }
}

/**
 * 虫子处理器
 */
public class WormHandler extends AppleHandler {

    public WormHandler() {
    }

    @Override
    protected void cannot(Apple apple) {
        //报废
    }

    @Override
    protected boolean verify(Apple apple) {
        return !apple.isHaveWorm();//返还这个苹果是否有虫
    }
}

/**
 * 外观处理器
 */
public class AppearanceHandler extends AppleHandler {

    public AppearanceHandler() {
    }

    @Override
    protected boolean verify(Apple apple) {
        return !apple.isFall();
    }

    @Override
    protected void cannot(Apple apple) {
        //喂猪
    }
}

dispatcher

/**
 * 苹果分配器
 */
public class AppleDispatcher {

    private final AppleCheckExecutor heightExecutor = new HeightExecutor();//高分执行器
    private final AppleCheckExecutor minExecutor = new MinExecutor();//中分执行器
    private final AppleCheckExecutor lowExecutor = new LowExecutor();//低分执行器

    /**
     * 根据苹果的外貌分数把他们放入不同的篮子里
     */
    public void dispatch(Apple apple) {
        float appearance = apple.getAppearance();

        if (appearance >= 75) {
            heightExecutor.executor(apple);
        } else if (appearance >= 45) {
            minExecutor.executor(apple);
        } else if (appearance > 0) {
            lowExecutor.executor(apple);
        } else {
            //外貌分<0,证明出现了异常
            throw new RuntimeException("输入的苹果的外貌分不合法,appearance=" + appearance);
        }
    }
}

在职责链的实现中,我们把对 苹果的安排的实现 委托给了 AppleCheckExecutor(苹果检查执行器)AppleDispatcher 只对苹果的外观分数敏感,而不再关心苹果的命运。也就是说,除非以后公司修改对分数的划分,否则再也不需要修改 AppleDispatcher 里面的内容了

而在 AppleCheckExecutor 中,他又把具体的行为抛给了 AppleHandler(苹果处理器),他把苹果当作一个请求,让 AppleHandler 之间自己建立连接,他只关心如果没有处理器处理苹果的情况

也就是说在 AppleCheckExecutor 里面,苹果是这样流通的:

在这里插入图片描述


这种写法是有意义的,事实上 AppleCheckExecutor 根本不关心一个苹果到底经历了多少检测,又或者是在哪一个检测里被刷掉的;他关心的只有这个苹果最终有没有通过检测

也就是说,先检查外观,还是先检查虫洞,对 AppleCheckExecutor 来说完全无所谓。甚至你往里面添加新的检查内容,他都无所谓,你只需要新增 AppleHandler 的子类,然后再修改对应的执行链里面的构成就可以了

但这也被认为是职责链的缺陷之一,调用者对执行链的内容太不关心,导致对请求到底在哪里被拦截一无所知

而这正是一个标准的职责链实现



碎碎念

职责链和事件响应

现在几乎所有的GUI框架都是通过 事件监听器 的模式

即 一个事件在某个组件上发生后,组件会发布一条信息出来,当有监听器在监听这个事件时就会被调用(这是一种观察者思想的实现,观察者会在后续文章中登场,敬请期待

但问题是当 某个按钮和他身后的面板都有 单击事件监听器 的时候 ,我单击这个按钮,要怎么保证一定是最上方的按钮上的事件监听器被调用呢?

JavaScript中的事件响应处理就是这种思路,而他的解决方案就是职责链。把所有可以监听到当前事件的组件上注册的事件监听器组成一个职责链,然后把事件对象当作请求发送到这条职责链中。只要在职责链里被截获了, 那么后面的事件监听器就接收不到请求了(就像上例中的 WormHandler 截断请求后,AppearanceHandler 不会再进行处理一样)


职责链和组合

相信在上例的职责链实现中,你肯定对 AppleCheckExecutor 中的 root 产生了既视感

是的,这里就是用到了组合模式


事实上在实战中,职责链和组合基本都是成对出现的。因为职责链里面多个处理器需要被组合成前后项之间存在关联的一个整体,那这就可以被看成是一种树状结构

而树状结构用组合模式来实现是再合适不过的了


清晰的结构和复杂的代码

无论多牛的设计模式,都没逃过相同的宿命,即:

任何设计模式在让系统的结构更清晰的同时,也让代码变得更复杂

即使是结构最简单的单例模式或原型模式,也需要你书写比 A a = new A() 跟复杂的代码出来;但是我们在何时的情况下依然要使用他们,因为他们虽然让我们多写几行代码,但是他们对我们的系统友善,让我们把代码交给后辈时可以少挨两句骂

设计模式帮助你解耦,可是为了解耦,就一定要抽象一部分内容出来。在面向对象的环境下,抽象的最低粒度是接口

也就是说,原本可以写在一个函数里面的事情,现在你需要新建至少一个接口,并串联起他们。在这个过程中,代码的维护成本变高了,我们需要记住越来越多的类,理解越来越多对业务来说并没有什么卵用的——用于维护对象之间关系的代码

所以请让这些复杂的代码变得有意义,不要滥用设计模式

并不是什么时候都需要设计模式的,你可以为【Hello World】设计出什么模式呢?

但是如果就此认为我们研究设计模式毫无意义,又未免过于偏激。以前看过一本关于重构的书,书名已经记不起了,但是里面有段话印象深刻,大意如下:

你知道吗,其实大师级程序员和你之间的区别并不大。仅仅在于大师在开发系统的时候是在描述一个故事,他在描述这个系统应该具有什么样的行为;而你是在把脑海里的蓝图具体化成建筑。讲故事需要优雅的叙事手法,还有渊博的见闻作为佐料。而砌墙,需要的只是重复的搬砖

代码是艺术品,如果比喻成雕塑,设计模式就是你的板凿斧锯,没有这些东西,你做出来的作品注定称不上优雅

我只是希望你在某些应该用锉刀雕刻的细节上,不要用斧头去砍他




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容


  1. QA,即 品质保证(Quality Assurance) ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1419204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VScode注释快捷键,RStudio注释快捷键, Texmaker注释快捷键

VScode&#xff1a;一款界面简单的代码编辑器&#xff1b;RStudio: R语言的IDE&#xff0c;包含代码编辑器&#xff0c;运行&#xff0c;绘图窗口等&#xff1b;Texmaker&#xff1a;Latex编译器&#xff08;编辑编译&#xff09;&#xff0c;界面简单&#xff0c;个人认为比Te…

sqli-labs第一关

1.判断是否存在注入&#xff0c;注入是字符型还是数字型? ?id1 and 11 ?id1 and 12 因为输入and 11与and 12 回显正常&#xff0c;所以该地方不是数字型。 ?id1 ?id1-- 输入单引号后报错&#xff0c;在单引号后添加--恢复正常&#xff0c;说明存在字符注入 2.猜解SQL查…

安全 输入输出类 XSS CSRF

输入输出类&#xff08;留言板&#xff09; 执行JS语句&#xff08;XSS漏洞&#xff09; XSS漏洞&#xff08;有输入框就可能-见框就X&#xff09; 反射型 语句植入并执行 存储型 语句植入到数据库&#xff0c;调用数据库就执行 UA头判断访问者浏览器信息 可以XSS php…

亚组分析、P交互、P趋势是什么?如何计算呢?

亚组分析、P交互、P趋势是什么&#xff1f;如何计算呢&#xff1f; &#xff08;1&#xff09;亚组分析如何计算&#xff1f; &#xff08;2&#xff09;P交互作用的计算方法&#xff1f; &#xff08;3&#xff09;P趋势如何计算&#xff1f; &#xff08;1&#xff09;亚组…

SNP干货分享:SAP数据脱敏的具体实施步骤

随着信息技术的飞速发展&#xff0c;大数据时代的到来使得数据成为国家经济、企业竞争力和个人隐私的重要载体。在这种背景下&#xff0c;数据安全问题日益凸显&#xff0c;各国政府纷纷出台相关法规以保护数据安全。我国也不断完善数据安全法规体系&#xff0c;以确保国家利益…

十分钟发布自己的NFT

概述 本文将以一个例子来说明如何在opensea快速发布自己的NFT智能合约&#xff08;ERC721&#xff09;。本着DRY&#xff08;Dont Repeat Yourself&#xff09;原则&#xff0c;我们需要站在巨人的肩膀上来搭建自己的应用&#xff0c;使用经过社区审计和实践检验的代码可以有效…

python统计分析——样本方差的分布

参考资料&#xff1a;用python动手学统计学 1、导入库 import numpy as np import pandas as pd import scipy as sp from scipy import statsfrom matplotlib import pyplot as plt import seaborn as sns 2、数据准备 建立一个平均数为4&#xff0c;标准差为0.8的正态分布…

腾讯云4核16G服务器价格,用于幻兽帕鲁Palworld专用

腾讯云幻兽帕鲁服务器4核16G、8核32G和16核64G配置可选&#xff0c;4核16G14M带宽66元一个月、277元3个月&#xff0c;8核32G22M配置115元1个月、345元3个月&#xff0c;16核64G35M配置580元年1个月、1740元3个月、6960元一年&#xff0c;腾讯云百科txybk.com分享腾讯云幻兽帕鲁…

自然语言处理 TF-IDF

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

Java强训day10(选择题编程题)

选择题 public class Test01 {public static void main(String[] args) {try{int i 100 / 0;System.out.print(i);}catch(Exception e){System.out.print(1);throw new RuntimeException();}finally{System.out.print(2);}System.out.print(3);} }编程题 题目1 import jav…

重构改善既有代码的设计-学习(六):处理继承关系

1、函数上移&#xff08;Pull Up Method&#xff09; 无论何时&#xff0c;只要系统内出现重复&#xff0c;你就会面临“修改其中一个却未能修改另一个”的风险。通常&#xff0c;找出重复也有一定的难度。 所以&#xff0c;某个函数在各个子类中的函数体都相同&#xff08;它们…

leetcode hot100跳跃游戏Ⅱ

本题和上一题还是有不一样的地方&#xff0c;这个题中&#xff0c;我们需要记录我们跳跃的步数并尽可能的满足最小的跳跃步数到达终点。 那么我们还是采用覆盖范围的概念&#xff0c;但是我们需要两个&#xff0c;一个是在当前位置的覆盖范围&#xff0c;另一个是下一步的覆盖…

Linux的 .bashrc 有什么作用?

一、.bashrc 是什么? 有什么用&#xff1f; .bashrc是一个存储在你的home目录下的隐藏文件&#xff0c;它用来配置和自定义你的终端环境和行为。 每次你启动一个新的终端时&#xff0c;.bashrc文件就会被执行&#xff0c;加载你设置的环境变量&#xff0c;别名&#xff0c;函数…

深度学习-自注意力机制

文字编码 one-hot编码&#xff0c;让模型自己去学习怎么进行编码 常见的输出 1.每个词都有一个输出值 2.每个句子输出一个值&#xff0c;情感分类 3.输入与输出长度不对应&#xff0c;翻译任务&#xff0c;生成任务。 RNN最早的语言处理 RNN解决的是模型去考虑前面的输入…

数字图像处理(实践篇)二十八 使用OpenCV Python中的K-means对图像进行颜色量化处理

目录 1 颜色量化 2 实践 在某些时候,不可避免的某些设备只能生成有限数量的颜色。因此需要执行颜色量化。选择使用cv2.kmeans()函数对颜色量化应用k-means聚类。 1 颜色量化 使用K-means聚类在图像中实现颜色量化的步骤如下: ① 导入依赖库

js实现动漫拼图2.0版

比较与1.0版&#xff0c;2.0版就更像与华容道类似的拼图游戏&#xff0c;从头到尾都只能控制白色块移动&#xff0c;而且打乱拼图和求助的实现与1.0都不相同 文章目录 1 实现效果2 实现思路2.1 打乱拼图2.2 求助功能2.3 判赢 3 代码实现 js实现动漫拼图1.0版 https://blog.csdn…

【千亿生意】一张眼底图,浮现你未来十年身体1000多种疾病风险

【千亿生意】一张眼底图&#xff0c;浮现你未来十年身体1000多种疾病风险 一眼看全身鹰瞳思路眼底看全身论文眼底成像技术眼底看肝脏眼底看多囊卵巢综合征眼底看肺部眼底看贫血眼底看少肌症眼底看神经退行眼底看心血管眼底看肾脏 鹰瞳视网膜论文 一眼看全身 眼病&#xff0c;是…

Redisson分布式锁介绍及实战应用(防止缓存击穿)

本地锁 浏览器把100w请求由网关随机往下传&#xff0c;在集群情况下&#xff0c;每台服务都放行10w请求过来&#xff0c;这时候每台服务都用的是本地锁是跨JVM的&#xff0c; 列如这些服务都没有49企业&#xff0c;此时有几个服务进行回原了打击在DB上面&#xff0c;那后期把这…

fastapi报错

初始化报错&#xff0c;非常低级错&#xff0c;扇自己10八张 app FastApi()

如何在Shopee越南站点进行有效的选品

在如今的电商市场中&#xff0c;选品是卖家们取得成功的关键之一。在Shopee越南站点进行选品时&#xff0c;卖家可以采取一些策略来提高产品的竞争力和销售业绩。本文将介绍一些有效的选品策略&#xff0c;帮助卖家在Shopee越南站点取得成功。 先给大家推荐一款shopee知虾数据…