前端也要学系列:设计模式之策略模式

做前端开发已经好几年了,对设计模式一直没有深入学习总结过。随着架构相关的工作越来越多,越来越能感觉到设计模式成为了我前进道路上的一个阻碍。所以从今天开始深入学习和总结经典的设计模式以及面向对象的几大原则。

今天第一天,首先来讲策略模式。什么是策略模式?GoF四兄弟的经典《设计模式》中,对策略模式的定义如下:

定义一系列的算法,把它们一个个封装起来,并且使它们可互相替换。

上边这句话,从字面来看很简单。但是如何在开发过程中去应用,仅凭一个定义依然是一头雾水。以笔者曾经做过的商户进销存系统为例:

某超市准备举行促销活动,市场人员经过调查分析制定了一些促销策略:

  • 购物满100减10
  • 购物满200减30
  • 购物满300减50
  • 。。。
  • 收银软件的界面是这样的(简单示意):我们应该如何计算实际消费金额?最初的实现是这样的:
    //方便起见,我们把各个促销策略定义为枚举值:0,1,2...
    var getActualTotal = function(onSaleType,originTotal){
        if(onSaleType===0){
            return originTotal-Math.floor(originTotal/100)*10
        }
        if(onSaleType===1){
            return originTotal-Math.floor(originTotal/200)*30
        }
        if(onSaleType===0){
            return originTotal-Math.floor(originTotal/300)*50
        }
    }
    
    getActualTotal(1,2680); //2208
    上面这段代码很简单,而且缺点也很明显。随着我的满减策略逐渐增多,getActualTotal函数会越变越大,而且充满了if判断,稍一疏忽就容易弄错。OK,有人说我很懒,虽然这样不够优雅但并不影响我的使用,毕竟满减策略再多也多不到哪去。
    我只能说,需求永远不是程序员定的。。这时,市场人员说我们新版程序添加了会员功能,我们需要支持以下的促销策略:

    会员促销策略:

  • 会员充300返60,且首单打9折
  • 会员充500返100,且首单打8折
  • 会员充1000返300,且首单打7折
  • ...
  • 这个时候,如果你还在原先的getActualTotal函数中继续添加if判断,我想如果你的领导review你这段代码,可能会怀疑自己当初怎么把你招进来。。OK,我们终于下定决心要重构促销策略的代码,我们可以这么做:
    var vipPolicy_0=function(originTotal){
        return originTotal-Math.floor(originTotal/100)*10
    }
    var vipPolicy_1=function(originTotal){
        return originTotal-Math.floor(originTotal/200)*30
    }
    ...
    //会员充1000返300
    var vipPolicy_10=function(account,originTotal){
        if(account===0){
            account+=1300;
            return originTotal*0.9
        }else{
            account+=1300;
            return originTotal;
        }
        return originTotal-Math.floor(originTotal/200)*30
    }
    ...
    var vipPolicy_n=function(){
        ...
    }
    
    var getActualTotal=function(onSaleType,originTotal,account){
        switch(onSaleType){
            case 0:
                return vipPolicy_0(originTotal);
            case 1:
                return vipPolicy_0(originTotal);
            ...
            case n:
                return ...
            default:
                return originTotal;
        }
    }
    好了,现在我们每种策略都有自己独立的空间了,看起来井井有条。但是还有两个问题没有解决:
  • 随着促销策略的增加,getActualTotal的代码量依然会越来越大
  • 系统缺乏弹性,如果需要增加一种策略,那么除了添加一个策略函数,还需要修改switch...case..语句
  • 让我们再来回顾一下策略模式的定义:

    定义一系列的算法,把它们一个个封装起来,并且使它们可互相替换

    在我们的例子中,每种促销策略的实现方式是不一样的,但我们最终的目的都是为了求得实际金额。策略模式可以把我们对促销策略的算法一个个封装起来,并且使它们可互相替换而不影响我们对实际金额的求值,这正好是我们所需要的。下面我们用策略模式来重构上面的代码:
    var policies={
        "Type_0":function(originTotal){
            return originTotal-Math.floor(originTotal/100)*10 
        },
        "Type_1":function(originTotal){
            return originTotal-Math.floor(originTotal/200)*30 
        },
        ...
        "Type_n":function(originTotal){
            ... 
        }
    }
    
    var getActualTotal=function(onSaleType,originTotal,account){
        return policies["Type_"+onSaleType](originTotal,account)
    }
    //执行
    getActualTotal(0,2680.00);//2208
    分析上面的代码我们发现,不管促销策略如何增加,getActualTotal函数完全不需要再变化了。我们要做的,就是增加新策略的函数而已。通过策略模式的代码,我们消除了让人反胃的大片条件分支语句,getActualTotal本身并没有计算能力,而是将计算全权委托给了策略函数。由此我们可以总结出策略模式实现的要点:
  • 将变化的算法封装成独立的策略函数,并负责具体的计算
  • 委托函数,该函数接受客户请求,并将请求委托给某一个具体的策略函数
  • 用一张UML图表示如下:
    怎么样?现在看到上面这张图是不是有了了然于胸的感觉?那就赶紧去试一试策略模式吧!参考书籍:
  • 《设计模式:可复用面向对象软件的基础》
  • 《大话设计模式》
  • 《Javascript设计模式与开发实践》
  • 相关内容推荐