浅谈责任链设计模式

GoF 定义

  • Chain Of Responsibility Design Pattern

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

借由<设计模式之美>中王争的通俗解释就是:

"在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。"

传统意义上的责任链模式有几个特点:

  • 有且仅有一个处理器处理,随着请求结束链的滑动随之终止。
  • 数据结构可以是数组 or 链表。
  • 遍历方式可以是递归 or 迭代
  • 链存在order顺序,可以排列优先级
  • 处理器选择策略可以由"钩子"方法释放。

先看一个简单的例子,然后再继续聊

public abstract class AbstractHandler {
    private AbstractHandler nextHandler;

    public final Response handleMessage(Request request) {
        Response response = null;
        //判断是否是自己的处理级别
        if (this.getHandlerLevel() == request.getBizCodeEnum()) {
            response = this.echo(request);
        } else {
            //不属于自己的处理级别,判断是否有下一个处理者
            if (this.nextHandler != null) {
                response = this.nextHandler.handleMessage(request);
            } else {
                //没有适当的处理者,再想点办法吧
            }
        }
        return response;
    }

    public void setNext(AbstractHandler _handler) {
        this.nextHandler = _handler;
    }

    protected abstract BizCodeEnum getHandlerLevel();

    protected abstract Response echo(Request request);

    public enum BizCodeEnum {
        A,
        B
    }

    @Builder
    @Getter
    public static class Request {
        //请求的等级
        BizCodeEnum bizCodeEnum;
        String name;
        String age;

    }

    @Builder
    @Getter
    public static class Response {
        boolean success;
        String code;
        String message;
    }
}

public class HandlerA extends AbstractHandler {

    @Override
    protected BizCodeEnum getHandlerLevel() {
        return BizCodeEnum.A;
    }

    @Override
    protected Response echo(Request request) {
        System.out.println("A");
        return Response.builder().code("A").build();
    }
}

public class HandlerB extends AbstractHandler {

    @Override
    protected BizCodeEnum getHandlerLevel() {
        return BizCodeEnum.B;
    }

    @Override
    protected Response echo(Request request) {
        System.out.println("B");
        return Response.builder().code("B").build();
    }
}


public class Client {

    public static void main(String[] args) {
        //声明所有的处理节点
        AbstractHandler handler1 = new HandlerA();
        AbstractHandler handler2 = new HandlerB();
        //设置链中的阶段顺序1-->2
        handler1.setNext(handler2);
        //提交请求,返回结果
        Response response = handler1.handleMessage(Request.builder().bizCodeEnum(BizCodeEnum.A).build());
        System.out.println(response.getCode() == "A");

        Response response2 = handler1.handleMessage(Request.builder().bizCodeEnum(BizCodeEnum.B).build());
        System.out.println(response2.getCode() == "B");
    }
}



如上图,一个非常正经的责任链模式。
首先定义一个抽象基类AbstractHandler,将选择策略和递归的动作封装,并且暴露抽象方法定义节点的业务身份,以及节点的处理逻辑,数据结构属于链表。

从这个例子我们可以延伸一下,首先整体解耦思想和实现方式、效果上来看,比如AbstractHandler这个类,定义method的方式是一个典型的模版设计模式(final & abstract & hook method )。其次用打标的方式选择是哪个节点处理,本质上是一个策略思想。然后,如果是单节点handle这种场景,其实就相当于观察者模式变种的Pub-Sub模式,想象一样用EventBus、MQ实现的话,对不同的consumer打tag标也差不多是这个能力。但是以上几种模式的侧重点不同,只是聊起来感觉很多东西套路类似。

所以本质要解决的问题是:

  • 将请求的发送和接收解耦
  • 无感知的拓展接收者、并且使职责单一细化
    也就是单一原则、开闭原则等。

传统责任链的拓展延伸

首先谈一下维持单节点响应设计的变种

  • 单节点响应模式变种
    就以上例子来说,如果我们维持只需要一个节点响应就终止链的话。可以看到,上面的例子,有几点可以玩的。

· 优先级
如果整体是一个链表遍历的话,那么我们可以在主动挂链表的时候,将优先级更靠前的往前放。比如几率比较大的节点放在前面,可以减少链表遍历的次数,有点类似平均复杂度,你可以把绝大多数碰到的情况放在第一个节点。

· 挂链
我们可以将所有节点打上@Component注解,注入Spring环境。然后通过依赖查找的方式,从Spring环境中获取List,再通过stream流,经过filter之后执行,filter算法就是钩子方法equals一下。那么链数据结构也变成了数组。

@Component
public class Client {
    private final List handlerList;

    private Client(List handlerList) {
        this.handlerList = handlerList;
    }

    public void test() {
        Request request = Request.builder().bizCodeEnum(BizCodeEnum.A).build();

        handlerList.stream().filter(f -> f.getHandlerLevel() == request.getBizCodeEnum())
            .forEach(abstractHandler -> abstractHandler.echo(request));
    }

  
}

细节一点的话,可以把filter提取一个方法深化语义,然后filter之后把流终止拿到一个Optional对象。
Preconditions.checkArgument(size>1,"节点不唯一");
Preconditions.checkArgument(isEmpty,"没有符合条件的节点");
以上判断,或者进行特殊逻辑,或者findAny终止判断一次之类的。

如果要保持链表数组结构也可以,在Spring ApplicationRunner等生命周期进行setNext操作即可,等于说将挂链的操作自动化处理一下,不需要我们去主动挂链了。这个就是属于spring生命周期做一些init操作范畴了,有点类似eventbus使用中,去做一些regist操作。

但是仍然是一个O(N)遍历,也可以改成用Map维护节点和业务身份的关系,通过"查表"这个老套路找到责任节点,不过这样就有点偏策略模式了。
个人觉得没必要太极端,一般来说链的节点个数本身就不会太多,实际上也基本上没有什么性能问题,责任链本身就是链的特性。

  private static final Map CONTAINER =
        ImmutableMap.of(
            "A",new HandlerA(),
            "B",new HandlerB()
        );

    public static void main(String[] args) {

        Request request = Request.builder().bizCodeEnum(BizCodeEnum.A).build();
        //提交请求,返回结果
        Response response = CONTAINER.get("A").echo(request);
        System.out.println(response.getCode() == "A");

    }

当判断条件比较模糊的时候,还有自由组合这种场景上,责任链模式更适合。查表也只适合比较固定code这种判断。后面我们再详细看如何组合。

· 判断
上面的钩子方法属于比较明确的策略分支,也可以不使用钩子方法,用boolean变量做。

    public final void handle() {
        boolean handled = doHandle();
        if (successor != null && !handled) { successor.handle(); }
    }

    protected abstract boolean doHandle();



子类去复写doHandle,只要有一个节点执行过,链就终止了。基于这种设计,我们可以复用节点,串成不同的链,在不同的场合使用。而不必强行给节点标识业务身份,链可以无状态一些,具体要看业务需求。

  • 非单节点变种
    在很多框架中,以及日常开发需求中,责任链一般都是所有节点都跑一遍的,或者一直跑到某个节点的终止动作。比如拦截器、函数式组合等。
    这种链就比较多了,比如OkHttp、Tomcat、Rpc、Spring Interceptor等。
    我们简单看几个。


    image.png

    image.png

    image.png

我想主要介绍一下OkHttp源码中的实现,还是比较好玩的。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }

  public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
      throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.exchange != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

可以看到Okhttp中,首先是一个双向责任链,也就是执行前按顺序执行所有拦截器,然后执行后再反过来执行一遍。想要实现双向,其实有两种方案,递归或者透传对象。

public interface Interceptor {
  Response intercept(Chain chain) throws IOException;

  interface Chain {
    Request request();

    Response proceed(Request request) throws IOException;

    /**
     * Returns the connection the request will be executed on. This is only available in the chains
     * of network interceptors; for application interceptors this is always null.
     */
    @Nullable Connection connection();

    Call call();

    int connectTimeoutMillis();

    Chain withConnectTimeout(int timeout, TimeUnit unit);

    int readTimeoutMillis();

    Chain withReadTimeout(int timeout, TimeUnit unit);

    int writeTimeoutMillis();

    Chain withWriteTimeout(int timeout, TimeUnit unit);
  }
}

Okhttp的设计是用传递Chain这个接口,进行递归。每一层拦截器,都会去执行
chain.proceed();proceed内部又是通过index + 1再找下一个拦截器执行。于是整个过程就跟二叉树的前中后序遍历一样,那么在chain.proceed()之前执行的代码就是前置Interceptor逻辑,后面执行的就是后置Interceptor逻辑。

在Okhttp整体设计中,是将自定义的拦截器先加入List,后面依次追加,这就是前面那张图的执行顺序由来。

    List interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));

然后Interceptor也是可以提前退出链的,比如CacheInterceptor如果命中了,就不会再去走网络拦截器了,可以看到源码里return response了,彼时还没有走到proceed。

整体来说,Chain的设计比较优雅,其次双向的实现也可以借鉴,还有就是list循环用index进行推进。

Tomcat源码也是通过pos下标去跑的,定位链执行到了哪个节点。

Spring Interceptor实现的简单一些,直接平铺了几个方法。

在DispatcherServlet 的 doDispatch() 方法来分发请求,它在真正的业务逻辑执行前后,执行 HandlerExecutionChain 中的 applyPreHandle() 和 applyPostHandle() 函数,用来实现拦截的功能。

以上就是几种常见的框架责任链的玩法了。

职责链模式常用在框架的开发中,为框架提供扩展点,让框架的使用者在不修改框架源码的情况下,基于扩展点添加新的功能。

责任链与函数式编程

其实工作中很多时候遇到比较简单的场景,我们可以借由java.util.function包里面的函数式接口,结合传统的设计模式,进行简化。

像java 8推出的很多函数式接口,本身就是推荐我们使用组合等方式进行编程的,最早应该是google一些类库先推的一些api,8借鉴过来了,java也常常干这种事,比如ListenableFuture -> CompleableFuture ,LocalDate之类的。挺好的,最终java版本越来越演进,我们也可以更使用更好的范式。

比如现在有这样一个需求,在客户发起一个操作之前,我们会判断这个客户是否可以进行这个动作。比方说客户要注销某个账户,而这个账户还有未结清的账单,或者这个账户仍然有一些在途的操作,此时我们应该返回相应的提示,让客户结清账单或者取消在途订单再来注销。

我们首先定义三个Predicate

 private Predicate onGoingCreditQuitPredicate() {
        return creditQuit -> {
            CreditQuitFindParam creditQuitFindParam = assemblerCreditQuitFindParam(creditQuit);
            CreditQuitSDO existOrder = creditQuitRepository.find(creditQuitFindParam);
            if (existOrder.isUnFinishCreditQuit) {
                throw new CreditQuitRuleCheckException("ON_GOING_SERVICE_CLOSE_EXIST",
                    Lists.newArrayList("ON_GOING_SERVICE_CLOSE_EXIST"), "重复关闭请求");
            }
            return true;
        };
    }

    private Predicate onGoingLoanPredicate() {
        return creditQuit -> {
            List existOrderList = loanRepository.findAll(assemblerLoanUserQuery(creditQuit));

            Optional.ofNullable(existOrderList).ifPresent(list -> list.forEach(existOrder -> {
                if (existOrder.isUnContractFinished()) {
                    throw new CreditQuitRuleCheckException("ON_GOING_LOAN_EXIST",
                        Lists.newArrayList("ON_GOING_LOAN_EXIST"), "您有未结清或已申请未放款业务,不能申请退出服务");
                }
            }));

            return true;
        };
    }

    private Predicate onGoingCreditPredicate() {
        return creditQuit -> {
            CreditFindParam creditFindParam = assemblerCreditFindParam(creditQuit);
            CreditSDO existOrder = creditRepository.find(creditFindParam);
            if (belongSuitableStatus(existOrder.getStatus())) {
                throw new CreditQuitRuleCheckException("ON_GOING_CREDIT_EXIST",
                    Lists.newArrayList("ON_GOING_CREDIT_EXIST"), "在途授信流程未结束,无法进行服务关闭");
            }
            return true;
        };
    }

然后我们继续使用tmf添加应用层拓展的更多准入条件,不同的业务身份添加的自定义拓展准入条件可以不同。

  @AbilityExtension(code = EXT_POINT_CHECK_RULES,
        name = "服务关闭校验规则定义",
        desc = "服务关闭校验规则定义")
    default List> getCheckPredicates(CreditQuitSDO creditQuit) {
        return Lists.newArrayList();
    }
    public void check(CreditQuitSDO creditQuit) {
        List> checkPredicates =
            Lists.newArrayList(onGoingCreditPredicate(), onGoingCreditQuitPredicate(), onGoingLoanPredicate());

        CreditQuitExtPoints extension = getExtensionPoints(creditQuit.getBizCode());
        List> extPredicates = extension.getCheckPredicates(creditQuit);

        if (CollectionUtils.isNotEmpty(extPredicates)) {
            checkPredicates.addAll(extPredicates);
        }

        checkPredicates.forEach(predicate -> predicate.test(creditQuit));
    }

以上我们就相当于得到了小业务中台复用的准入条件,加上tmf中自定义的准入条件,结合到一起的Predicate集合,然后进行判断即可,在上层捕捉异常即可返回命中的那条失败信息,比如说onGoingCreditPredicate没有通过,就会返回用户:"在途授信流程未结束,无法进行服务关闭"。

如果不想抛异常,想提高性能,也可以返回false,然后通过别的方式透传失败话术,视场景而定,我这里性能可接受,所以选择最方便的方式上抛校验话术。

有一些场景我们也可以直接用static修饰predicate,在编译期就通过静态map编排,查表进行策略,或者switch case编排之类的。如果说在静态static predicate中有spring依赖无法引入的问题(就是static作用域引用autowared的对象,编译报错),也比较简单。

@Component
public class InstanceLocator extends SpringInitializingBean {

    public static  T getInstance(Class requiredType) {
        return applicationContext.getBean(requiredType);
    }

    public static  T getInstance(String name, Class requiredType) {
        return applicationContext.getBean(name, requiredType);
    }

    public static  Map getBeansOfType(Class type) {
        return applicationContext.getBeansOfType(type);
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}


在static修饰的predicate中使用InstanceLocator.getBean即可,因为运行期间已经能够从spring容器中获取bean了,绕过这个限制即可。其实底层都是从DefaultListableBeanFactory获取的,ApplicationContext这个bean工厂也是通过DefaultListableBeanFactory委托管理bean的,他们是同一个接口实现类。

以上也是比较简单的玩法,当然Predicate也可以组合着玩。
比如:

onGoingCreditQuitPredicate()
            .and(onGoingLoanPredicate())
            .and(onGoingCreditPredicate()).test(creditQuit);

这也是一种挂链的方式。

如果我们进一步封装,可以将Predicate都放在PredicateFactory中维护,由Factory与上层交互,面向新增就在factory继续铺predicate。其实没有必要很纠结原则,比如说Predicate没有单独放一个类,设计模式是比较灵活的,在函数名和函数内部方法名很合理的情况下,可读性完全没有问题。并且有一个优势在于,多个predicate公用的方法可以提取在一个类的作用域,而私有的方法可以直接包裹在predicate内部。
比如:

    private Predicate onGoingCreditPredicate() {
        return new Predicate() {
            @Override
            public boolean test(CreditQuitSDO creditQuit) {
                CreditFindParam creditFindParam = assemblerCreditFindParam(creditQuit);
                CreditSDO existOrder = creditRepository.find(creditFindParam);
                if (belongSuitableStatus(existOrder.getStatus())) {
                    throw new CreditQuitRuleCheckException("ON_GOING_CREDIT_EXIST",
                        Lists.newArrayList("ON_GOING_CREDIT_EXIST"), "在途授信流程未结束,无法进行服务关闭");
                }

                return true;
            }

            private  boolean belongSuitableStatus(CreditStatus creditStatus) {
                return !ENABLE_QUIT_STATUS.contains(creditStatus);
            }

            private  CreditFindParam assemblerCreditFindParam(CreditQuitSDO creditQuit) {
                CreditFindParam creditFindParam = new CreditFindParam();
                creditFindParam.setCustomerId(creditQuit.getCustomer().getCustomerId());
                return creditFindParam;
            }
        };
    }

我们可以把belongSuitableStatus和assemblerCreditFindParam两个仅属于这个predicate私有的方法内置,然后将多个predicate公用的方法外置,来进一步细腻,可以和前面一张图对比一下,其实就已经把Predicate当一个类用了,比较简洁,替代传统设计模式的类。

回顾

  • 责任链模式官方定义
    将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

  • 实际业务实现
    可以单节点处理,也可以全部过一遍,可以数组也可以链表。

  • Spring、Functional结合
    可以通过Spring实现节点的自动注册发现、自动串联等。
    可以通过Predicate等函数,进行简易的责任链实现,Predicate也是很适合作为tmf ExtendPoint定义的。
    关于更多Spring和函数式小技巧单独再写一篇分享,还是有很多设计模式或者框架二次封装,可以通过Spring和Functional方式玩一些的。

你可能感兴趣的:(浅谈责任链设计模式)