网易乐得技术团队

基于责任链的活动模板引擎

一、设计模式之责任链模式

1.责任链模式介绍

维基百科中有一段对责任链模式的描述:

责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。

责任链模式中有两个角色:

  1. 抽象处理者(Handler)角色:定义一个请求的接口。如果需要,可以定义一个方法用来设定和返回下家对象的引用。
  2. 具体处理者(ConcreteHandler)角色:如果可以处理就处理请求,如果不能处理,就把请求传给下家,让下家处理。也就是说它处理自己能处理的请求且可以访问它的下家。

责任链模式分为纯责任链模式和不纯的责任链模式:

  • 如果一个类要么承担责任处理请求要么将请求踢给下一个皮球,则被称为纯责任链模式
  • 如果一个类承担了一部分责任,还将请求踢给下一个皮球,则被称为不纯的责任链模式

一般来说,日常开发中不纯的责任链模式用的比较多一点。

2.责任链模式的优缺点

优点是:调用者不需知道具体谁来处理请求,也不知道链的具体结构,降低了节点域节点的耦合度;可在运行时动态修改链中的对象职责,增强了给对象指派职责的灵活性;

缺点是:没有明确的接收者,可能传到链的最后,也没得到正确的处理。

3.责任链模式使用场景

责任链模式在现实中使用的很多,常见的就是OA系统中的工作流。假如员工直接上司为小组长,小组长直接上司项目经理,项目经理直接上司部门经理,部门经理直接上司总经理。公司规定请假审批如下(请假时间为t,时间单位day,简写d):

  1. t<0.5d,小组长审批;
  2. t>=0.5d&&t<2,项目经理审批;
  3. t>=2&&t<5,部门经理审批;
  4. t>=5,总经理审批;

审批时序图如下:

审批时序图

代码实现如下:

1.审批处理抽象类

package chainOfResp.example;
/**
 *描述:审批处理抽象类
 */
public abstract class Handler {    
    protected Handler handler;   
    /**
     *描述:审批
     */
    public abstract boolean approve(double day);
    public Handler getHandler() {
        return handler;
    }
    public void setHandler(Handler handler) {
        this.handler = handler;
    }    
}

2.小组长处理类

package chainOfResp.example;   
public class GroupLeader extends Handler {
    @Override
    public boolean approve(double day) {
        if(day<0.5){
            System.out.println("小组长审批通过");
            return true;
        }else {
            System.out.println("小组长传给了他的上司");
            return getHandler().approve(day);
        }
    } 
}

3.项目经理处理类

package chainOfResp.example;
public class ProjectManager extends Handler {
    @Override
    public boolean approve(double day) {
        if(day<2){
            System.out.println("项目经理审批通过");
            return true;
        }else {
            System.out.println("项目经理传给了他的上司");
            return getHandler().approve(day);
        }
    }
}

4.部门经理处理类

package chainOfResp.example;
public class CEO extends Handler {
    @Override
    public boolean approve(double day) {
            System.out.println("部门经理审批通过");
            return true;
    }
}

5.测试类

package chainOfResp.example;
/**    
 *描述:测试类,首先来创建责任链,然后发出请求模拟员工来请假
 */
public class Client {
    public static void main(String[] args) {  
        //创建节点
        GroupLeader gl = new GroupLeader();
        ProjectManager pm = new ProjectManager();
        DepartmentManager dm = new DepartmentManager();
        //建立责任链
        gl.setHandler(pm);
        pm.setHandler(dm);
        dm.setHandler(ceo);

        //向小组长发出申请,请求审批4天的假期
        gl.approve(4D);
    }
}

运行结果:

小组长传给了他的上司
项目经理传给了他的上司
部门经理审批通过

在java中的实际应用有Servlet中的过滤器(Filter),Struts2的拦截器(Interceptor)。Struts2本身在Servlet中也是以Filter的形式出现的,所以Struts2的结构图中,也可以明显看出Filter和Interceptor这两条链的存在。

Struts2Architecture

可以看出它们每个节点都可以做一些事情,所以不算一个纯的责任链。

二.各式各样的运营活动

做过运营活动开发的同学都知道,运营活动是各式各样的,种类之繁多,形式之多变,让运营活动的开发变得很繁重。

举几个简单的例子:

  1. 新用户领取资格后返红包
  2. 用户充值后返红包
  3. 一周未登录用户返红包,并发短信提醒

这边只是举了一些非常简单的运营活动的例子,真正开发中遇到的运营活动远比这些复杂得多。

这些运营活动看起来复杂多变,但是仔细研究,发现是有规律可循的,我总结了一下,发现了以下规律:

  1. 活动的整体流程是有规律可循的,用一句话概括起来就是:满足某些条件的对象,会得到某个最终结果。
  2. 满足某些条件的对象,这里面的对象包括:用户、设备或者手机号。
  3. 需要满足的条件,也是可以总结得出来的。比如说:用户是否是新用户,用户是否领取了活动资格等等条件。
  4. 活动的最终结果有规律,无非是发红包、发推送、发短信,或者这三者的组合。

下面的流程图就是一个典型的运营活动的流程图:

activityExample

从流程图可以看出,整个活动主要分为三步:

  1. 进入活动页,判断活动状态(活动是否存在,活动是否在活动期)
  2. 判断用户资格
  3. 给用户派发红包

第二步中的判断用户资格,依次需要判断用户的以下资格:

(1)用户是否登录
(2)是否是垃圾用户
(3)是否用于该用户参与该活动
(4)用户是否已经参与过该活动
(5)用户是否是A支付用户
(6)用户是否是B支付用户
(7)用户是否是新用户

对于第二步中的判断用户资格,是一个很明显的责任链的模式。

从第1个资格到第7个资格,依次判断下去,只要其中一个资格不满足,链路结束,直接返回。如果当前资格满足,则进行下一个节点的判断,直到整个链路结束。

三、运营活动中的责任链模式

对于不同的活动,需要判断的资格是不同的,判断资格的流程是一样的,都是满足某一个条件,才会继续判断是否满足下一个条件,否则直接返回。

举一个详细的例子,比如说另外一个活动,需要满足的条件是:

(1)用户是否登录
(2)是否是垃圾用户
(3)用户是否已经参与过该活动
(4)用户是否是新用户
(5)是否已经进行过语音验证
(6)手机号是否已经参与过该活动

跟上面的需要满足的条件相比,整体流程上没有变化。会有一些重复的判断项,也会有一些之前没有的判断项。

对于这些判断项,是有限可穷举的。随着活动的不断增加,一旦这些资格判断项都列举出来,那么对责任链上的每一个节点就是可配置的。

1.责任链实现

(1)责任链处理者抽象类–抽象处理者(Handler)角色

在运营活动中,就是校验的抽象类:

public abstract class CheckHandler
{
    protected final static Log LOG = LogFactory.getLog(CheckHandler.class);

    public abstract int check(HttpServletRequest request, HttpServletResponse response, String activityId, String accountId, Iterator<CheckHandler> iterator, CheckInfoDto checkInfoDto);
}

(2)责任链具体的实现类–具体处理者(ConcreteHandler)角色

在运营活动中,就是校验类的具体实现类。下面是一个校验用户是否是新用户的具体实现类的例子:

/**
 * 验证用户是新用户还是老用户
 * @author 
 *
 */
@Component("oldUserCheckHandler")
public class OldUserCheckHandler extends CheckHandler
{
    @Autowired
    UserPayActService userPayActService;

    @Override
    public int check(HttpServletRequest request, HttpServletResponse response, String activityId, String accountId, Iterator<CheckHandler> iterator, CheckInfoDto checkInfoDto)
    {
        LOG.info("验证用户是新用户还是老用户,activityId:" + activityId + ",accountId:" + accountId);
        if (StringUtils.isNotBlank(accountId) && !userPayActService.isNewPayUserByDays(accountId, 0))
        {
            return ActivityReturnCodeEnum.OLD_USER.getValue();
        }
        return CheckChain.doCheck(request, response, activityId, accountId, iterator, checkInfoDto);
    }
}

(3)责任链主流程类

public class CheckChain
{
    public static int doCheck(HttpServletRequest request, HttpServletResponse response, String activityId, String accountId, Iterator<CheckHandler> iterator, CheckInfoDto checkInfoDto)
    {
        if (iterator != null && iterator.hasNext())
        {
            CheckHandler handler = iterator.next();
            return handler.check(request, response, activityId, accountId, iterator, checkInfoDto);
        }
        return ActivityReturnCodeEnum.SUC.getValue();
    }
}

2.责任链具体的检验实现类列表

上面“验证用户是新用户还是老用户”的实现类,是其中一个校验类的具体实现。

可以把现在已用到的校验类都实现,下图就是系统中所有的校验实现类,即责任链上的处理节点:

checkHandlers

方便使用,将这些处理类的bean名字放在一个枚举中:

public enum CheckHandlerBeanNameEnum
{
    ACTIVITY_STATUS_CHECK("activityStatusCheckHandler"),

    USER_LOGIN_CHECK("userLoginCheckHandler"),

    RUBBISH_ACCOUNTID_CHECK("rubbishAccountIdCheckHandler"),

    USER_PARTIN_ACTIVITY_SERIES_CHECK("userPartInActivitySeriesCheckHandler"),

    OLD_USER_CHECK("oldUserCheckHandler"),

    SMS_VALIDATE_CHECK("smsValidateCheckHandler"),

    .........//此处省略若干

    private String beanName;

    CheckHandlerBeanNameEnum(String beanName)
    {
        this.beanName = beanName;
    }

    public CheckHandler getCheckHanler()
    {
        if (!SpringContextHolder.isSpringContextLoaded())
            return null;
        return SpringContextHolder.getBean(beanName);
    }
}

3.可组装的责任链

public enum CouponSendActivitySeriesEnum
{

    /**
     * “0元购”活动
     */
    FREE_BUY("freeParticipate", "freeBuyActHandler", "/mobile/activity/0yuanbuy-20160118/index", "",
            CheckHandlerBeanNameEnum.ACTIVITY_STATUS_CHECK, CheckHandlerBeanNameEnum.USER_LOGIN_CHECK,
            CheckHandlerBeanNameEnum.RUBBISH_ACCOUNTID_CHECK,
            CheckHandlerBeanNameEnum.USER_PARTIN_ACTIVITY_SERIES_CHECK, CheckHandlerBeanNameEnum.OLD_USER_CHECK,
            CheckHandlerBeanNameEnum.SMS_VALIDATE_CHECK, CheckHandlerBeanNameEnum.MOBILE_PARTIN_CHECK),

    /**
     * 老用户送红包
     */
    OLD_USER_SEND_COUPON_ACT(ActivityConstant.OLD_USER_SEND_COUPON,
            "oldUserSendCouponActHandler", "/mobile/activity/olduser-sms-coupon-20160310/index", "",
            CheckHandlerBeanNameEnum.ACTIVITY_STATUS_CHECK, CheckHandlerBeanNameEnum.USER_LOGIN_CHECK,
            CheckHandlerBeanNameEnum.RUBBISH_ACCOUNTID_CHECK, CheckHandlerBeanNameEnum.ACCOUNT_LIMIT_JUDGE_CHECK,
            CheckHandlerBeanNameEnum.USER_PARTIN_ACTIVITYID_CHECK, CheckHandlerBeanNameEnum.IS_XXX_USER_CHECK,
            CheckHandlerBeanNameEnum.OLD_USER_CHECK),

    .....//此处省略若干

    private String activitySeries;//活动系列
    private String ftlPath;//wap页面返回的ftl路径
    private String ftlPCPath;//PC页面返回的ftl路径
    private String handlerBeanName;//不同的活动不同的handler,对应的bean name
    private CheckHandlerBeanNameEnum[] checkhandlerEnums;//验证枚举数组

    CouponSendActivitySeriesEnum(String activitySeries, String handlerBeanName, String ftlPath, String ftlPCPath,
            CheckHandlerBeanNameEnum... checkhandlerEnums)
    {
        this.activitySeries = activitySeries;
        this.handlerBeanName = handlerBeanName;
        this.ftlPath = ftlPath;
        this.ftlPCPath = ftlPCPath;
        this.checkhandlerEnums = checkhandlerEnums;
    }

    .....//后面代码省略
}

这个枚举类实现了责任链的可组装,就是该类型活动的一个配置模板,枚举类中checkhandlerEnums变量就是用来存储责任链上的每一个节点。

对于不同的活动系列,根据activitySeries得到对应的配置。责任链会依次校验每一个条件,若某一个节点不满足,则责任链结束,直接返回错误状态码,若一直到整个责任链结束,则校验成功,返回成功。

4.总结

责任链的活动模板引擎虽然不能涵盖全部的运营活动的开发,但是对于同一类型的活动,能大大减少代码的开发量和提升代码的重用率。

曾经有过一个活动,过完需求,不用五分钟就完成了开发,仅仅需要按照配置模板添加一个配置项即可。

参考链接

http://alaric.iteye.com/blog/1926447