cover_image

巧用设计模式,打造通用规则模块

XJZ 三七互娱技术团队
2020年06月05日 10:00




巧用设计模式,

打造通用规则模块












    在活动业务需求中我们常有一些需求规则如:登录游戏x天、活动期间累计充值x元、角色等级达到x级等等,而这些规则会被不同的功能模块调用如:领取礼包功能、抽奖功能、虚拟积分功能等。组合后就可以实现如下活动规则:登录游戏1天且累计充值1元可领取礼包A、角色等级达到10级可抽奖1次等等。


01

图片

  何为通用规则模块?

    传统的实现方式如下,每个模块都有自己的规则组合,如(图1)所示:

图片

图1

    但缺点也比较明显,会有大量重复的规则代码,功能模块与规则逻辑之间有着较强的耦合。因此我们可以做一个通用的规则模块,供不同的功能组件调用,既可减少重复逻辑代码,方便维护,亦可一次开发,多模块共用,方便拓展。通用规则模块如(图2)所示:

图片

图2

    下面我们来逐步实现通用规则模块的开发,本文虽以PHP代码为例说明,但设计思想是相通的。

02

图片

  if-else工程师?

    有人说在编程领域,没有什么是if-else不能解决的,如果有,那就多来几个if-else。在项目初期,往往以快速实现功能为目标,不要跟我说什么架构什么设计模式,拿起键盘就是敲。如(图3)所示,有3项规则需要实现,代码如下:

图片

class Client {    /**     * 检查规则     * @param  [string] $rule_type 规则类型     * @return [bool]     */    public function rule_check($rule_type) {        if ($rule_type == 'FREE') {            //处理免费规则逻辑        } elseif ($rule_type == 'PAY') {            //处理充值规则逻辑        } elseif ($rule_type == 'LOGIN') {            //处理登录规则逻辑        } else {            //默认处理        }        return true;    }}
    如果只有简单的两三项逻辑,确实可以用以上的if-else轻松解决。而当业务场景逐渐丰富,规则也慢慢增多,如(图4)所示:从原有的3项规则变成5项。

图片

图4

    此时往往会将if-else改为switch,如:
class Client {    const RULE_TYPE_FREE = 'FREE';    const RULE_TYPE_PAY = 'PAY';    const RULE_TYPE_LOGIN = 'LOGIN';    const RULE_TYPE_LEVEL = 'LEVEL';    const RULE_TYPE_VIP = 'VIP';    //检查规则    public function rule_check($rule_type) {        switch ($rule_type) {            case self::RULE_TYPE_FREE:                //处理免费规则逻辑                break;            case self::RULE_TYPE_PAY:                //处理充值规则逻辑                break;            case self::RULE_TYPE_LOGIN:                //处理登录规则逻辑                break;            case self::RULE_TYPE_LEVEL:                //处理等级规则逻辑                break;            case self::RULE_TYPE_VIP:                //处理vip规则逻辑                break;            default:                //默认处理                break;        }        return true;    }}
    以上代码应付三五个规则判断似乎还行,但当业务场景更多,规则数量迅猛增长时,如(图5)所示:规则数量超过了10个。

图片

图5

    而此时的代码如下:
class Client {    const RULE_TYPE_FREE = 'FREE';  //免费机会    const RULE_TYPE_PAY = 'PAY';    const RULE_TYPE_LOGIN = 'LOGIN';    const RULE_TYPE_LEVEL = 'LEVEL';    const RULE_TYPE_VIP = 'VIP';    const RULE_TYPE_SINGLE_PAY = 'SINGLE_PAY'; //单笔充值    const RULE_TYPE_GAME_VIP = 'GAME_VIP'; //游戏VIP等级    const RULE_TYPE_LOGIN_APP = 'LOGIN_APP'; //端登录    const RULE_TYPE_USER_NEW = 'USER_NEW'; //新用户    const RULE_TYPE_ONLINE_DURATION = 'ONLINE_DURATION'; //在线时长    const RULE_TYPE_PHONE_BIND = 'PHONE_BIND'; //绑定手机    //检查规则    public function rule_check($rule_type) {        switch ($rule_type) {            case self::RULE_TYPE_FREE:                //处理免费规则逻辑                break;            case self::RULE_TYPE_PAY:                //处理充值规则逻辑                break;            case self::RULE_TYPE_LOGIN:                //处理登录规则逻辑                break;            case self::RULE_TYPE_LEVEL:                //处理等级规则逻辑                break;            case self::RULE_TYPE_VIP:                //处理vip规则逻辑                break;            case self::RULE_TYPE_SINGLE_PAY:                //处理单笔充值规则逻辑                break;            case self::RULE_TYPE_GAME_VIP:                //处理游戏VIP等级规则逻辑                break;            case self::RULE_TYPE_LOGIN_APP:                //处理端登录规则逻辑                break;            case self::RULE_TYPE_USER_NEW:                //处理新用户规则逻辑                break;            case self::RULE_TYPE_ONLINE_DURATION:                //处理在线时长规则逻辑                break;            case self::RULE_TYPE_PHONE_BIND:                //处理绑定手机规则逻辑                break;            default:                //默认处理                break;        }        return true;    }}
    处理规则逻辑判断的代码相当冗长且集中,存在以下问题:
1、缺少封装:所有逻辑都在客户端代码里,若某一规则逻辑需要被多个地方调用,则需要复制粘贴,出现大量重复代码。
2、不便于拓展:每次新增规则或者优化规则逻辑都需要修改提交客户端代码,稍有不慎可能会影响其他规则,甚至是其他方法的正常运行。
3、不便于维护:逻辑过于集中且繁杂,不利于多人协同开发,不利于codereview,也不便于单元测试。
仅凭大量if-else代码显然比较乏力,为解决以上问题,我们需要运用设计模式。

03

图片

  设计模式之策略模式(Strategy Pattern)

    策略模式正是大量if-else代码的救星。if-else本是一种判断上下文环境逻辑所采用的策略,但策略数过多时逻辑代码则相当冗长,而且拓展新策略还得不断增加if-else。而策略模式的思想则是把每种策略封装到不同的类,再通过一个引用了策略对象的环境类来统一操作和使用具体的策略,实现如(图6)所示:

图片

图6

//抽象策略类,定义规则校验方法abstract class RuleStrategy {    abstract public function ruleCheck();}
//具体算法类,实现具体的规则逻辑class RulePay extends RuleStrategy { public function ruleCheck() { //处理充值规则逻辑 }}
class RuleLogin extends RuleStrategy { public function ruleCheck() { //处理登录规则逻辑 }}
class RuleLevel extends RuleStrategy { public function ruleCheck() { //处理等级规则逻辑 }}
//环境类,对策略实现的封装,在客户端中使用class RuleContext { public $strategy; public function __construct(RuleStrategy $rs) { $this->strategy = $rs; } public function doCheck() { return $this->strategy->ruleCheck(); }}
class Client { //检查规则 public function rule_check($rule_type) { switch ($rule_type) { case self::RULE_TYPE_PAY: $strategy = new RulePay(); break; case self::RULE_TYPE_LOGIN: $strategy = new RuleLogin(); break; case self::RULE_TYPE_LEVEL: $strategy = new RuleLevel(); break; //... default: //默认处理 break; }
//获取具体的规则策略 $rule_context = new RuleContext($strategy); //执行具体的规则逻辑 $rule_context->doCheck(); return true; }}
    采用了策略模式后,不同规则的逻辑被封装到不同的策略类中,在客户端调用时可按需实例化对应的策略类,利用环境类统一调用处理逻辑的方法,使得代码更加优雅并解决了以下问题:
1、封装性更好:每个规则逻辑被封装成类,可以单独实现自己对外开放的功能,方便多处地方调用,减少重复代码。
2、更易维护:规则与规则之间相互独立,在更新规则逻辑时,不会因疏忽而影响其他规则的正常运行,且便于对每种规则做单元测试。
不过在客户端代码中仍然有一大段的switch判断。当需要新增一个规则时,除了要添加新的类文件外,还需要修改客户端代码增加一个switch的case项,略显麻烦。
    为了解决不便于拓展规则的问题,我们利用反射对代码进行改造。

04

图片

  反射+策略模式

1、按照策略模式把各规则逻辑封装到不同类中
//抽象策略类,定义规则校验方法abstract class RuleStrategy {    abstract public function ruleCheck();} //具体算法类,实现具体的规则逻辑class RulePay extends RuleStrategy {    public function ruleCheck() {        //处理充值规则逻辑    }} class RuleLogin extends RuleStrategy {    public function ruleCheck() {        //处理登录规则逻辑    }} class RuleLevel extends RuleStrategy {    public function ruleCheck() {        //处理等级规则逻辑    }}
2、原本引用规则策略对象的环境类,改成利用反射的方式实例化策略对象
class RuleContext {    public $rule;    /**     * 实例化规则校验对象     * @param  [string] $name 具体的规则类型     * @param  [array]  $args 实例化时的参数     * @return [RuleStrategy] 具体的策略类对象     */    public function rule($name, $args=[]) {        //参数的标识 用于区分不同的实例化对象        $uniq_args_tag = md5(serialize($args));        $name = strtolower($name);        $name_arr = explode('_', $name);        $full_name = '';        foreach ($name_arr as $str) {            $full_name .= ucfirst($str);        }        //根据约定的命名规则构造类名        $name = "Rule{$full_name}";
if (!empty($this->rule[$name][$uniq_args_tag])) { return $this->rule[$name][$uniq_args_tag]; }
//利用反射实现对象实例化 if(empty($args)) { $this->rule[$name][$uniq_args_tag] = new $name(); } else { $ref = new ReflectionClass($name); $this->rule[$name][$uniq_args_tag] = $ref->newInstanceArgs($args); }
//获取到具体的策略类对象 return $this->rule[$name][$uniq_args_tag]; }}
3、调用时,仅需要3行代码:
class Client {    //检查规则    public function rule_check($rule_type) {        $rule_context = new RuleContext();        //获取具体的规则策略对象        $rule_strategy = $rule_context->rule($rule_type);        //执行具体的规则逻辑        $rule_strategy->ruleCheck();        return true;    }}
    当我们需要新增规则时,只需要增加一个规则策略类即可,无需再对客户端代码进行修改,如下代码所示。
//新增规则class RuleOnlineDuration extends RuleStrategy {    public function ruleCheck() {        //处理在线时长规则逻辑    }}
    至此我们已经完成了一个通用规则模块的设计和优化,它可以供我们的多个活动组件调用,从而实现一次编码多次复用的效果。


    最后为大家推荐两本设计模式的书,可以更系统清晰地认识设计模式,体会其魅力所在:

1、《大话设计模式》- 程杰

2、《深入PHP:面向对象、模式与实践(第3版)》 - Matt Zandstra[译者: 陈浩等]


图片
图片

编辑 / chuanrui


图片

图片



继续滑动看下一个
三七互娱技术团队
向上滑动看下一个