一、责任链模式定义
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.
(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。)
责任链模式,也叫职责链模式、功能链模式、命令链模式等。
顾名思义,责任链就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
二、责任链模式的结构和说明
- Handler 定义职责的接口。 声明处理请求的方法,并持有一个后继职责对象。
- ConcreteHandler 实现职责的类。 在这个类中,实现对在它职责范围内请求的处理,处理完后,可以选择继续转发请求给后继者或者结束。
三、责任链模式示例
考虑这样一个场景:用户在购买商品时,需要做一些校验,如登录校验、商品库存校验、用户余额校验等。必须所有校验都通过后,才能继续执行后边的逻辑,如果其中有任何一个校验不通过,则都会返回失败。
(先假设某种商品单价100,库存还剩10件,某用户还有余额200。)
我们先看下不用状态模式的实现方式
定义请求类,封装用户ID、商品编号、购买数量等。
/**
* 购买商品请求类
*/
@Data
public class BuyGoodsRequest {
/**
* 用户ID
*/
private String userId;
/**
* 商品编号
*/
private String goodsNo;
/**
* 购买商品数量
*/
private int count;
}
定义校验处理器,包含各种校验方法和逻辑。
/**
* 校验处理器
*/
public class CheckProcessor {
/**
* 校验
*/
public boolean check(BuyGoodsRequest buyRequest){
// 校验是否登录
if(!checkLogin(buyRequest)){
return false;
}
// 校验库存是否足够
if(!checkStockNumber(buyRequest)){
return false;
}
// 校验余额是否足够
if(!checkBalance(buyRequest)){
return false;
}
return true;
}
/**
* 校验是否登录
*/
private boolean checkLogin(BuyGoodsRequest buyRequest){
// 简单模拟示意,用户ID为SIGNED_USER即为已登录
if(!"SIGNED_USER".equals(buyRequest.getUserId())){
System.out.println("登录校验未通过");
return false;
}
System.out.println("登录校验通过");
return true;
}
/**
* 校验库存是否足够
*/
private boolean checkStockNumber(BuyGoodsRequest buyRequest){
// 假设库存只有10件
if(buyRequest.getCount() > 10){
System.out.println("库存不足");
return false;
}
System.out.println("库存足够");
return true;
}
/**
* 校验余额是否足够
*/
private boolean checkBalance(BuyGoodsRequest buyRequest){
// 假设用户只有200余额,每件商品的价格为100
if(buyRequest.getCount() * 100 > 200){
System.out.println("余额不足");
return false;
}
System.out.println("余额足够");
return true;
}
}
我们通过客户端来模拟一下效果
public static void main(String[] args) {
BuyGoodsRequest buyRequest = new BuyGoodsRequest();
buyRequest.setUserId("SIGNED_USER");
buyRequest.setGoodsNo("BOOK_1");
buyRequest.setCount(1);
// 购买商品校验
CheckProcessor checkProcessor = new CheckProcessor();
if(!checkProcessor.check(buyRequest)){
// 校验失败,购买流程终止
System.out.println("校验失败,购买流程终止");
return;
}
// 继续执行其它逻辑...
System.out.println("继续执行其它逻辑...");
}
运行后输出如下:
登录校验通过
库存足够
余额足够
继续执行其它逻辑…
再来模拟一下购买数量超过库存数量的场景
public static void main(String[] args) {
BuyGoodsRequest buyRequest = new BuyGoodsRequest();
buyRequest.setUserId("SIGNED_USER");
buyRequest.setGoodsNo("BOOK_1");
buyRequest.setCount(11);
// 购买商品校验
CheckProcessor checkProcessor = new CheckProcessor();
if(!checkProcessor.check(buyRequest)){
// 校验失败,购买流程终止
System.out.println("校验失败,购买流程终止");
return;
}
// 继续执行其它逻辑...
System.out.println("继续执行其它逻辑...");
}
运行后输出如下:
登录校验通过
库存不足
校验失败,购买流程终止
上边的实现很简单,但这么实现有没有什么问题呢?假如在后期我们又遇到:
- 有一款商品是虚拟商品,没有库存限制,不需要校验库存
- 某款商品规定必须用户等级为钻石及以上才能购买,需要加一个用户等级校验
- 还有商品是限时抢购商品,只在规定的时间段内才可以进行购买,需要加一个下单时间校验,等等…
想要实现上边这些功能,就需要多加几个校验流程,
- 流程 1 做登录校验+库存校验+余额校验
- 流程 2 做登录校验+余额校验
- 流程 3 做登录校验+库存校验+余额校验+用户等级校验
- 流程 4 做登录校验+库存校验+余额校验+下单时间校验
- … …
这样做,第一是需要频繁的修改已有代码的逻辑,第二就是灵活性和扩展性太差,等校验逻辑多起来的时候,我们就会发现这种组合也会越来越多。
通过责任链模式的实现方式
那怎么才能比较方便的实现上边的需求呢?一个合理的解决方案,就是使用责任链模式。
接下来,我们用责任链模式实现一下上边的功能。
BuyGoodsRequest 请求类保持不变
CheckProcessor 校验处理器 改为抽象接口类,声明一个抽象校验方法,并持有一个后继的校验处理器对象。
/**
* 校验处理器
*/
@Data
public abstract class CheckProcessor {
/**
* 持有后继的校验处理器对象
*/
protected CheckProcessor checkProcessor;
/**
* 校验
*/
public abstract boolean check(BuyGoodsRequest buyRequest);
}
将之前的登录校验、库存校验、余额校验方法,抽出为 CheckProcessor 校验处理器 的实现类。
/**
* 校验是否登录
*/
public class CheckLogin extends CheckProcessor{
@Override
public boolean check(BuyGoodsRequest buyRequest) {
// 简单模拟示意,用户ID为SIGNED_USER即为已登录
if(!"SIGNED_USER".equals(buyRequest.getUserId())){
System.out.println("登录校验未通过");
return false;
}
System.out.println("登录校验通过");
// 如果有后继的校验处理器对象,则传递给后继处理器对象继续进行校验
if(null != checkProcessor){
return checkProcessor.check(buyRequest);
}
return true;
}
}
/**
* 校验库存是否足够
*/
public class CheckStockNumber extends CheckProcessor{
@Override
public boolean check(BuyGoodsRequest buyRequest) {
// 假设库存只有10件
if(buyRequest.getCount() > 10){
System.out.println("库存不足");
return false;
}
System.out.println("库存足够");
// 如果有后继的校验处理器对象,则传递给后继处理器对象继续进行校验
if(null != checkProcessor){
return checkProcessor.check(buyRequest);
}
return true;
}
}
/**
* 校验余额是否足够
*/
public class CheckBalance extends CheckProcessor{
@Override
public boolean check(BuyGoodsRequest buyRequest) {
// 假设用户只有200余额,每件商品的价格为100
if(buyRequest.getCount() * 100 > 200){
System.out.println("余额不足");
return false;
}
System.out.println("余额足够");
// 如果有后继的校验处理器对象,则传递给后继处理器对象继续进行校验
if(null != checkProcessor){
return checkProcessor.check(buyRequest);
}
return true;
}
}
再来通过客户端来模拟一下使用
public static void main(String[] args) {
BuyGoodsRequest buyRequest = new BuyGoodsRequest();
buyRequest.setUserId("SIGNED_USER");
buyRequest.setGoodsNo("BOOK_1");
buyRequest.setCount(1);
// 组装需要做的校验链条
CheckProcessor checkLogin = new CheckLogin();
CheckProcessor checkStockNumber = new CheckStockNumber();
CheckProcessor checkBalance = new CheckBalance();
checkLogin.setCheckProcessor(checkStockNumber);
checkStockNumber.setCheckProcessor(checkBalance);
if(!checkLogin.check(buyRequest)){
// 校验失败,购买流程终止
System.out.println("校验失败,购买流程终止");
return;
}
// 继续执行其它逻辑...
System.out.println("继续执行其它逻辑...");
}
运行后输出如下:
登录校验通过
库存足够
余额足够
继续执行其它逻辑…
这样就实现了跟上边一样的基础功能。
那它是怎么运行的呢?通过下边这个调用过程示意图,可以清晰的了解一下它的原理。
那如果是一个虚拟商品,不需要做库存校验,我们可以怎么处理呢?
这种处理起来很简单,不需要对后台服务的代码做任何的修改,只要在客户端组装校验链条时,不要组装库存校验对象即可。
public static void main(String[] args) {
BuyGoodsRequest buyRequest = new BuyGoodsRequest();
buyRequest.setUserId("SIGNED_USER");
buyRequest.setGoodsNo("BOOK_1");
buyRequest.setCount(1);
// 组装需要做的校验链条
CheckProcessor checkLogin = new CheckLogin();
CheckProcessor checkBalance = new CheckBalance();
checkLogin.setCheckProcessor(checkBalance);
if(!checkLogin.check(buyRequest)){
// 校验失败,购买流程终止
System.out.println("校验失败,购买流程终止");
return;
}
// 继续执行其它逻辑...
System.out.println("继续执行其它逻辑...");
}
运行后输出如下:
登录校验通过
余额足够
继续执行其它逻辑…
如果又来一个限时抢购商品,只在每天凌晨 01:00 之前开放购买,又该怎么去校验呢?
这个扩展起来也比较简单,原有代码不需要变动,只需要增加一个限时校验的实现类即可。
/**
* 校验限时抢购时间
*/
public class CheckTime extends CheckProcessor{
@Override
public boolean check(BuyGoodsRequest buyRequest) {
// 简单模拟示意,只有在凌晨1点之前的可以购买
if(LocalTime.now().getHour() >= 1){
System.out.println("今天的限时抢购已结束");
return false;
}
System.out.println("限时抢购活动开启中");
// 如果有后继的校验处理器对象,则传递给后继处理器对象继续进行校验
if(null != checkProcessor){
return checkProcessor.check(buyRequest);
}
return true;
}
}
再来模拟下客户端中的使用:
public static void main(String[] args) {
BuyGoodsRequest buyRequest = new BuyGoodsRequest();
buyRequest.setUserId("SIGNED_USER");
buyRequest.setGoodsNo("BOOK_1");
buyRequest.setCount(1);
// 组装需要做的校验链条
CheckProcessor checkLogin = new CheckLogin();
CheckProcessor checkStockNumber = new CheckStockNumber();
CheckProcessor checkBalance = new CheckBalance();
CheckProcessor checkTime = new CheckTime();
checkLogin.setCheckProcessor(checkStockNumber);
checkStockNumber.setCheckProcessor(checkBalance);
checkBalance.setCheckProcessor(checkTime);
if(!checkLogin.check(buyRequest)){
// 校验失败,购买流程终止
System.out.println("校验失败,购买流程终止");
return;
}
// 继续执行其它逻辑...
System.out.println("继续执行其它逻辑...");
}
运行后输出如下:
登录校验通过
库存足够
余额足够
今天的限时抢购已结束
校验失败,购买流程终止
同样的,再有新的校验逻辑时,也只需要对应的增加实现类即可,不需要修改原有的代码逻辑。
四、责任链模式的优缺点
优点:
- 请求者和接受者松散耦合。
- 可以动态组合职责,从而更灵活的给对象分配职责。
- 可以在不更改现有代码的情况下在程序中新增处理者。
缺点:
- 如果职责过多,会产生很多细粒度的对象。
- 如果使用不当,可能会造成循环调用。
- 不能保证请求一定会被处理到。
五、责任链模式的应用场景及案例
- 有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求。
- 如用在审批流程等实际业务场景中。
- Spring 拦截器链。
- servlet 过滤器链。