责任链处理模式

介绍

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

问题

假如你正在开发一个在线订购系统。 你希望对系统访问进行限制, 只允许认证用户创建订单。 此外, 拥有管理权限的用户也拥有所有订单的完全访问权限。

简单规划后, 你会意识到这些检查必须依次进行。 只要接收到包含用户凭据的请求, 应用程序就可尝试对进入系统的用户进行认证。 但如果由于用户凭据不正确而导致认证失败, 那就没有必要进行后续检查了

在接下来的几个月里, 你实现了后续的几个检查步骤。

  • 一位同事认为直接将原始数据传递给订购系统存在安全隐患。 因此你新增了额外的验证步骤来清理请求中的数据。

  • 过了一段时间, 有人注意到系统无法抵御暴力密码破解方式的攻击。 为了防范这种情况, 你立刻添加了一个检查步骤来过滤来自同一 IP 地址的重复错误请求。

  • 又有人提议你可以对包含同样数据的重复请求返回缓存中的结果, 从而提高系统响应速度。 因此, 你新增了一个检查步骤, 确保只有没有满足条件的缓存结果时请求才能通过并被发送给系统

检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当你希望复用这些检查步骤来保护其他系统组件时, 你只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。

系统会变得让人非常费解, 而且其维护成本也会激增。 你在艰难地和这些代码共处一段时间后, 有一天终于决定对整个系统进行重构

与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。

模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理

最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

在我们的订购系统示例中, 处理者会在进行请求处理工作后决定是否继续沿着链传递请求。 如果请求中包含正确的数据, 所有处理者都将执行自己的主要行为, 无论该行为是身份验证还是数据缓存

小案例

假设,目前我要实现一个网站的注册功能,注册的时候肯定要对用户注册的信息进行校验,比如看当前邮箱是否被注册过,注册手机号是否为正确格式,接口在短时间内是否被重复访问,这些都是要通过校验的,通常情况下,一般都使用if-else进行校验判断,但当我们处理稍微复杂的逻辑判断时候,代码就会显得臃肿,一旦需求改变,加上另外的其他校验规则,导致代码不好维护,在这种情况下,我们可以使用责任链模式

伪代码

基础验证接口

public abstract class Middleware {
/**
     * Builds chains of middleware objects.
     */
    private Middleware next;
/*
提供一个可变参数,当前节点校验完指向下一节点校验
*/
    public static Middleware link(Middleware first,Middleware... chain){
        Middleware head=first;
        for (Middleware nextInChain:chain){
            head.next=nextInChain;
            head=nextInChain;
        }
        return first;
    }

    public abstract boolean check(String email,String password,String phone);

    protected boolean checkNext(String email,String password,String phone){
        if (next==null){
            System.out.println("校验通过");
            return true;
        }
        return next.check(email,password,phone);
    }
}

第一层校验,接口访问次数校验

package org.pt.design.chaindesign;

/*
访问接口时长访问次数校验
 */
public class ThrottlingMiddleware extends Middleware{

/*
接口次数
*/
    private int requestPerMinute;
    private int request;
    private long currentTime;
    @Override
    public boolean check(String email, String password,String phone) {
        if (System.currentTimeMillis() > currentTime + 60_000) {
            request = 0;
            currentTime = System.currentTimeMillis();
        }
        request++;
        if (request > requestPerMinute) {
            System.out.println("Request limit exceeded!");
            Thread.currentThread().stop();
        }
        return checkNext(email, password,phone);
    }

    public ThrottlingMiddleware(int requestPerMinute) {
        this.requestPerMinute = requestPerMinute;
        this.currentTime=System.currentTimeMillis();
    }

}

可以看到这边,每次访问这个方法,访问次数加一,当访问次数大于设置访问的次数时,直接返回false,不进行下一次校验,访问次数小于允许访问的次数时,进行下一步校验

第二次校验,当前用户是否注册过校验

package org.pt.design.chaindesign;

/*
用户是否存在校验
 */
public class UserExistsMiddleware extends Middleware {
    private Server server;

    public UserExistsMiddleware(Server server) {
        this.server = server;
    }

    public boolean check(String email, String password,String phone) {
        if (!server.hasEmail(email)) {
            System.out.println("This email is not registered!");
            return checkNext(email, password,phone);
        } else {
            System.out.println("This email is registered!");
            return false;
        }

    }
}

这个server变量可以是数据库查询的,我这边就直接设置了一个server对象,server对象里面有一个map进行校验

package org.pt.design.chaindesign;

import java.util.HashMap;
import java.util.Map;

public class Server {
    private Map<String, String> users = new HashMap<>();
    private Middleware middleware;

    /**
     * Client passes a chain of object to server. This improves flexibility and
     * makes testing the server class easier.
     */
    public void setMiddleware(Middleware middleware) {
        this.middleware = middleware;
    }

    /**
     * Server gets email and password from client and sends the authorization
     * request to the chain.
     */
    public boolean logIn(String email, String password,String phone) {
        if (middleware.check(email, password,phone)) {
            System.out.println("Authorization have been successful!");

            // Do something useful here for authorized users.

            return true;
        }
        return false;
    }

    public void register(String email, String password) {
        users.put(email, password);
    }

    public boolean hasEmail(String email) {
        return users.containsKey(email);
    }

    public boolean isValidPassword(String email, String password) {
        return users.get(email).equals(password);
    }
}

如果map里面有,则表明注册过,直接返回false,反之进行下一步校验,

public class PhoneCheckMiddleware extends Middleware{
    public boolean check(String email, String password,String phone) {
        if (email.equals("123456")) {
            System.out.println(email+"登录成功");
            return true;
        }
        System.out.println("Hello, user!");
        return checkNext(email, password,password);
    }
}

这里可以对手机号进行正则表达式校验

校验通过下一步,我这边之设置了三次校验,下一个next指针指向为null,表明全部校验通过

测试类:

package org.pt.design.chaindesign;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Demo {
    private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private static Server server;

    private static void init() {
        server = new Server();
        server.register("admin@example.com", "admin_pass");
        server.register("user@example.com", "user_pass");

        // All checks are linked. Client can build various chains using the same
        // components.
        Middleware middleware = Middleware.link(
                new ThrottlingMiddleware(2),
                new UserExistsMiddleware(server),
                new PhoneCheckMiddleware()
        );

        // Server gets a chain from client code.
        server.setMiddleware(middleware);
    }

    public static void main(String[] args) throws IOException {
        init();

        boolean success;
        System.out.print("Enter email: ");
        String email = reader.readLine();
        System.out.print("Input password: ");
        String password = reader.readLine();
        System.out.print("Input phone: ");
        String phone = reader.readLine();
        success = server.logIn(email, password,phone);
    }
}

责任链模式应用场景

当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。

该模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。

当必须按顺序执行多个处理者时, 可以使用该模式。

无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。

如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。

如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序

实现方式

  1. 声明处理者接口并描述请求处理方法的签名。

    确定客户端如何将请求数据传递给方法。 最灵活的方式是将请求转换为对象, 然后将其以参数的形式传递给处理函数。

  2. 为了在具体处理者中消除重复的样本代码, 你可以根据处理者接口创建抽象处理者基类。

    该类需要有一个成员变量来存储指向链上下个处理者的引用。 你可以将其设置为不可变类。 但如果你打算在运行时对链进行改变, 则需要定义一个设定方法来修改引用成员变量的值。

    为了使用方便, 你还可以实现处理方法的默认行为。 如果还有剩余对象, 该方法会将请求传递给下个对象。 具体处理者还能够通过调用父对象的方法来使用这一行为。

  3. 依次创建具体处理者子类并实现其处理方法。 每个处理者在接收到请求后都必须做出两个决定:

    • 是否自行处理这个请求。

    • 是否将该请求沿着链进行传递。

  4. 客户端可以自行组装链, 或者从其他对象处获得预先组装好的链。 在后一种情况下, 你必须实现工厂类以根据配置或环境设置来创建链。

  5. 客户端可以触发链中的任意处理者, 而不仅仅是第一个。 请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。

  6. 由于链的动态性, 客户端需要准备好处理以下情况:

    • 链中可能只有单个链接。

    • 部分请求可能无法到达链尾。

    • 其他请求可能直到链尾都未被处理

责任链模式优缺点

  • 你可以控制请求处理的顺序。

  • 单一职责原则。 你可对发起操作和执行操作的类进行解耦。

  • 开闭原则。 你可以在不更改现有代码的情况下在程序中新增处理者

源代码:https://github.com/Breeze1203/JavaAdvanced/tree/main/JavaSE/src/main/java/org/pt/design/chaindesign