你看那代码,好像一条链哎

简介:

就如星爷多年前说的那样“你看那代码,好像一条链哎”。什么?他没说过吗,或许我记错了。你应该已经猜到了,这篇文章,我们来讨论一下责任链设计模式。这个模式并不流行,至少在 Gang of Four定义的模式中是这样。但现代依赖注入框架让我们可以用巧妙的新奇的方式去实现这个模式,我们来看看。

介绍

声明:这种模式并没有新东西。我的一个同事刚刚前几天使用过,我也曾用过很多次。这篇文章的灵感来源于我最近遇到的问题,我们下面来说说,我之前也没有意识到这个问题可以用这种模式来解决。

传统模式

责任链模式是一种行为设计模式,它首次在Gang of Four写的Design Patterns这本书中提及。模式的目的是:

避免请求的发送者与接收者耦合,为多个对象提供处理请求的机会.将接收对象串联成链,请求在链上传递,直到被一个对象处理.

类的关系图如下所示:

通过定义一个可以用来响应客户端请求的标准接口,来实现松耦合。在上面的图中,表现为Handler抽象类型。可以通过创建链式的类,继承上面的接口来实现多个类响应请求的能力。每一个类在链中拥有下一个节点的实例。successor属性满足作用域。

当调用时,每一个handler确定自己是否有能力处理请求。如果有,它执行请求的操作,在这,我们可以根据请求的转发规则实现许多不同的处理方式。一旦一个ConcreteHandler声明可以处理这个请求,我们可以实现规则用于停止请求在链中传递。这种情况下,handleRequest方法的实现方式如下所示:

if (/* The request can be successfully handled */) {
    // Handle the request
} else {
    successor.handleRequest(request);
}

另一方面,我们可以将请求转发到链中的下一个handler,无论当前的handler是否能处理。

if (/* The request can be successfully handled */) {
    // Handle the request
}
successor.handleRequest(request);

构建链的操作应该和下面差不多。

Handler chain = new ConcreteHandler1(new ConcreteHandler2(new ConcreteHandler3()));
chain.handleRequest(request);

在JDK内部实现中,至少有两个地方用到了这种模式:

  • logging机制的实现:java.util.logging.Logger#log()
  • http请求过滤器机制和Servlet响应规范的实现:javax.servlet.Filter#doFilter()

依赖注入的出现

正如许多其他的情况一样,依赖注入模式的出现改变了一切。让我们看看依赖注入特性如何使责任链模式现代化。

首先,我们需要一个所有依赖注入库都实现的特性:multibindings。基本上,它可以提供一个类型的所有子类型的实例,仅仅通过注入这个类型的集合。

比如下面这个类型系统:

interface Shop {}
class PetShop implements Shop {}
class Grocery implements Shop {}
class Drugstore implements Shop {}
// And so on...

现在,我们定义一个新类型ShoppingCenter,它拥有Shop每个子类型的实例。使用依赖注入,我们可以通过在ShoppingCenter注入一个Shop集合来实现这一目标。

class ShoppingCenter {
    private final Set<Shop> shops;
    @Inject
    public void ShoppingCenter(Set<Shop> shops) {
        this.shops = shops;
    }
    // Class body using shops
}

真TM简单!显然,每一个依赖注入库都有自己的配置来解决这种情况。在Spring中,使用auto-discovery特性,你只需要一点小小的配置。在Guice,稍稍复杂,但最终结果一样。

责任链模式的现代化实现

简单总结一下:我们已经看到了责任链模式的典型形式;我们看到了依赖注入库提供的multibinding特性;最后,我们看到了如何把这两个概念搭配使用。

首先,我们需要一个与原始的责任链设计模式稍有不同的实现。让我们引入一个新的类型ChainHandler。这个类型的职责就是拥有整个链,并暴露出一个接口,用于访问链提供给客户端的操作函数。

class ChainHandler {
    private final Set<Handler> handlers;
    @Inject
    public void ChainHandler(Set<Handler> handlers) {
        this.handlers = handlers;
    }
    // Pass the request to each handler of the chain
    public void handle(final Request request) {
        handlers.forEach(h -> h.handle(request));
    }
}

利用依赖注入的优势,在不改变已有代码的基础上增加一个Handler的实现。这意味着实际上我们不需要执行回归测试。另一方面,将Handler的执行放入链中有一点困难(但并不是不能)

警告

正如很多其他的模式一样,专注于构造模式的每个类的角色是什么很重要。你会给每个具体的Handler什么功能?你会把应用的业务逻辑直接放在Handler里面吗?

首先,我们很多人都会提供上面的解决方案,这并不完全错误。然而,这种设计限制了代码的复用并违反了单一职责原则(Single Responsibility Principle)。

举个例子,我们需要实现一个系统,用来在金融业务中补全信息,补全操作使用责任链模式。一个可能要插入的补全信息就是根据IBAN(国际银行账号)或BIC码(银行代码)导出的收款人国家。然后我们来定义一个CountryPayeeEnricher。

首先看一下,我们可以在CountryPayeeEnricher中直接编写代码用来补全国家信息。但如果我们需要在我们应用的其他位置(或其他应用)复用这个功能呢?遵循组合原则是一个更好地解决方案,将代码放进一个专有的类中,比如PayeeService:

class PayeeService {
    public Country deriveCountryFromPayee(String payee) {
        // Code that extract the country information from the
        // input payess
    }
    // Omissis...
}
class CountryPayeeEnricher implements Enrichment {
    private PayeeService payeeService;
    @Inject
    public void CountryPayeeEnricher(PayeeService payeeService) {
        this.payeeService = payeeService;
    }
    public void handle(Transaction tx) {
        Country country = payeeService.deriveCountryFromPayee(tx.getPayee());
        tx.setCountry(country);
        // ...or something like this
    }
}

通过这种方式,我们最终有了两个拥有不同职责的类型:PayeeService类型,提供可复用的直接联系收款人信息的服务。CountryPayeeEnricher类型,代替之前类型提供服务的标准入口。

Scala方式

为了完美,我也想讨论一下用Scala语言实现责任链模式。正如很多其他设计模式一样,这门语言内部已经实现了责任链模式:偏函数(partial functions)。在理论层面,偏函数是定义了域里的一部分值的函数。在Scala中,这种函数有一个特别的类型——PartialFunction[T, V]

在Scala中使用模式匹配(pattern matching)声明来定义偏函数,在下面这个例子中,fraction的默认值是0。

val fraction: PartialFunction[Int, Int] = {
  case d: Int if d != 0 =>  42 / d
}

如果有多个定义集合,你可以有多个case子句。如果你为了应用函数,把每个case子句作为满足的情况(责任链里的handler,记得吗?),你就再次用到了责任链:

case class Request(val value: String) { /* ... */ }
val someStupidFunction: PartialFunction[Request, String] = {
  case Request(42) => "The final answer"
  case Request(0) => "You know nothing, John Snow"
  case Request(666) => "Something strange is going on in here"
  //. ..
}

紧接着,一个偏函数可以当做好多handler构成的链。显然,通过这种方式使用责任链模式,你必须遵守一些额外的约束。事实上:

  • 你不能在每个handler中储存元数据
  • 你不能从链中移除handler
  • 你不能显示检查handler或美观的打印它

如果你确实不需要做上面这些事情,模式匹配偏函数(pattern-matching PartialFunctions)用起来相当棒。


作者: 孙腾

来源:51CTO

相关文章
|
机器学习/深度学习 安全
深度学习McCulloch-Pitts模型
深度学习McCulloch-Pitts模型
782 0
|
9月前
|
存储 监控 安全
RFID车辆识别技术精准提升通行效率
RFID车辆识别技术通过车载标签与读写器联动,实现车辆信息自动采集与管理,广泛应用于停车场、园区安防、物流运输等领域,提升通行效率与安全性,助力智慧交通与城市管理智能化升级。(238字)
|
4月前
|
人工智能 运维 安全
云原生 - AI Native 多智能体数字人架构实践
我们以云原生应用部门为试验田,用商业化产品 AgentTeams 落地一支"数字员工小分队",让它们承接日常研发、工单答疑、开源维护与运营等业务,把原本人肉串联的协作流程,做成 AI Native 的工作方式。
606 0
|
6月前
|
数据采集 网络协议 算法
IPv6地理库如何建设?从被动探测到主动订阅的实践解析
全球IPv4枯竭加速IPv6部署,我国IPv6活跃用户已超8亿。本文解析IPv6地理定位三大挑战,揭示IP数据云等平台如何依托Geofeed订阅、BGP解析等主动溯源技术,构建高精度、日更级IPv6地理库。
|
11月前
|
开发工具 芯片 异构计算
可编程逻辑器件学习(day1):技术演进与应用解析
每日更新可编程逻辑器件(PLD)教程,涵盖基本概念、发展历史、分类体系、开发流程及应用展望,评论区答疑,助你从入门到精通!
|
存储 文件存储 Windows
小白尖叫!DeepSeek安装竟偷占C盘?这样做路径配置 直接根治存储焦虑
惊! 完蛋了! DeepSeek占满了我的C盘~~~~ DeepSeek让我C盘爆炸~~~再见了,DeepSeek
小白尖叫!DeepSeek安装竟偷占C盘?这样做路径配置 直接根治存储焦虑
|
数据可视化 uml
UML图讲解(关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系)
UML图讲解,关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系。
7087 0
UML图讲解(关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系)
|
消息中间件 负载均衡 Kafka
MQ消息路由大揭秘!从菜鸟到高手,一文带你玩转消息传递的‘高速公路’,轻松实现订单秒级响应!
【8月更文挑战第24天】在现代分布式系统中,消息队列(MQ)作为系统间解耦的核心工具,支持异步处理、负载均衡及高可用性。消息路由是MQ中的关键环节,决定消息从生产者到消费者的路径。主流MQ产品如RabbitMQ、Kafka等采用相似的路由机制,涉及交换器、队列、路由键等概念。常见的路由模式包括直接交换、主题交换及发布/订阅模式。以RabbitMQ为例,通过直接交换模式,可以根据订单类型(如“普通订单”、“紧急订单”)将消息路由至相应的处理队列。这一过程展示了MQ系统如何基于路由键和队列绑定关系实现消息的有效传递。
670 2
|
缓存 前端开发 安全
开发者必读:GET和POST请求的终极对比
大家好,我是小米,今天我们来聊聊HTTP协议中的GET和POST请求。它们在数据传输方式、安全性和应用场景上有不同特点。本文将详细解析它们的区别和特点,帮助你更好地理解和运用这两种请求方式。让我们一起学习吧!
1099 4
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
538 0

热门文章

最新文章