297°

JavaScript设计模式之策略模式

在网上搜索“为什么MVC不是一种设计模式呢?”其中有解答:MVC其实是三个经典设计模式的演变:观察者模式(Observer)、策略模式(Strategy)、组合模式(Composite)。所以我今天选择学习策略模式。

策略模式:定义了一系列家族算法,并对每一种算法单独封装起来,让算法之间可以相互替换,独立于使用算法的客户。

通常我并不会记得“牛顿第一定律”的具体内容,所以我也难保证我会对这个定义记得多久……用FE经常见到的东西来举个例子说明一下:

$("div").animation({left: '50px'},1000,'easein');

$("div").animation({left: '50px'},1000,'linear');

$("div").animation({left: '50px'},1000,'swing');

//看最后三个关于动画效果的参数

//Jquery文档总提到easing(第三个参数):要使用的擦除效果的名称(需要插件支持).默认jQuery提供"linear" 和 "swing".

我们在对元素设置动画的缓动效果,实际就是策略模式的一种实现。这样的缓动算法跟我们使用Jquery的人来说没有直接关系,假如我的项目中某个动画需要一种新的算法效果,那么我们再去开发一个插件就好了。反之,如果Jquery没有提供这样一种插件机制,那针对需求变化难不成要去改动Jquery的源码吗?

在《大话设计模式》一书中,作者举例的是一个商场的收银系统,在实际操作中,商場可能因为“双11买一送一”、“满500立减50”、“中秋节全场11折”等活动而对最终的收费产生变化。如果哪一天商场突然倒闭,全场两元,这时候我们仅需要给软件系统增加一个所有商品价格变两元的插件算法(类)即可。

我先来模拟一下策略模式的基本代码形态:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script type="text/javascript">
        function ConcreteStrategyA(){
            this.AlgorithmInterface = function(){
                console.log("算法A");
            }
        }

        function ConcreteStrategyB(){
            this.AlgorithmInterface = function(){
                console.log("算法B");
            }
        }

        function ConcreteStrategyC(){
            this.AlgorithmInterface = function(){
                console.log("算法C");
            }
        }

        //Context,用一个createStrategy来配置,维护一个对Strategy对象的引用

        function Context(strategy){
            this.strategy = strategy;
            this.ContextInterface = function(){
                strategy.AlgorithmInterface();
            }

        }

        //应用
        var context1 = new Context(new ConcreteStrategyA());
        context1.ContextInterface();

        var context2 = new Context(new ConcreteStrategyB());
        context2.ContextInterface();

        var context3 = new Context(new ConcreteStrategyC());
        context3.ContextInterface();
    </script>
</body>
</html>

通常来说,具体的某一种算法必须保证实现了某一些接口或者继承某个抽象类,才不会发生类型错误,在javascript中去实现接口、抽象类、继承等特性要费一些周章,所以我这个例子是不严谨的,仅从最简单的实现方式着手。

具体实现一个商场收银系统:包括一个单独js文件,和一个具体的实现html文件

//因为要用到数值验证,所以...这里用的是jquery2.1里面的isNum
function isNum(obj){
    return obj - parseFloat(obj)>=0;
}
//算法A,没有活动,正常收费
function ConcreteStrategyA(){
    this.AlgorithmInterface = function(money){
        return money;
    }
}
//算法B,满300减100
function ConcreteStrategyB(MoneyCondition,MoneyReturn){
    this.MoneyCondition = MoneyCondition,
    this.MoneyReturn    = MoneyReturn;

    this.AlgorithmInterface = function(money){
        var result=money;
        if(money>=MoneyCondition){
            result = money - Math.floor(money/MoneyCondition)*MoneyReturn;
        }
        return result;
    }
}
//算法C,打折
function ConcreteStrategyC(moneyRebate){
    this.moneyRebate = moneyRebate;
    this.AlgorithmInterface = function(money){
        return money*this.moneyRebate;
    }
}

//Context,用一个createStrategy来配置,维护一个对Strategy对象的引用
//这里将算法相关的从客户端剥离出来,简单工厂模式
function Context(type){
    this.strategy = null;
    switch(type){
        case "a":
            this.strategy = new ConcreteStrategyA();
            break;
        case "b":
            this.strategy = new ConcreteStrategyB("300","100");
            break;
        case "c":
            this.strategy = new ConcreteStrategyC("0.8");
            break;
    }

    this.ContextInterface = function(money){
        if(!isNum(money)){
            money = 0;
        }
        return this.strategy.AlgorithmInterface(money);
    }

}

HTML部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style type="text/css">
        .block {
            padding:5px 0;
            border-bottom:1px solid #ccc;
        }
        .menu {margin:10px auto;text-align: center;}
    </style>
</head>
<body>
    <div class="block">
        <section class="product"><label>单价(RMB):<input type="text" class="tPrice" /></label><label>数量:<input type="text" class="tNum" /></label><label>计算方式:<select class="tAlg"><option value="a">正常收费</option><option value="b">满300减100</option><option value="c">打8折</option></select></label><label>合计:<input type="text" class="tMoney" /></label></section>
    </div>
    <div class="menu">
        <input type="button" id="addBtn" value="增加一个" />
    </div>
    <div>
        <label>总价:<input type="text" id="total" readonly /></label>
    </div>
    <script type="text/javascript" src="strategy.js"></script>
    <script type="text/javascript">
        var tPrice = document.getElementsByClassName("tPrice"),
            tNum   = document.getElementsByClassName("tNum"),
            tAlg   = document.getElementsByClassName("tAlg"),
            tMoney = document.getElementsByClassName("tMoney"),
            total  = document.querySelector("#total");

        var addBtn = document.querySelector("#addBtn");
        addBtn.addEventListener("click",function(){
            var html = '<section class="product"><label>单价(RMB):<input type="text" class="tPrice" /></label><label>数量:<input type="text" class="tNum" /></label><label>计算方式:<select class="tAlg"><option value="a">正常收费</option><option value="b">满300减100</option><option value="c">打8折</option></select></label><label>合计:<input type="text" class="tMoney" /></label></section>';
            var div = document.createElement("div");
            div.className="block";
            div.innerHTML = html;
            this.parentNode.parentNode.insertBefore(div,this.parentNode);
        })

        
        function calculate(e){

            //根据事件对象判断事件源,获取同类元素中的位置
            var num = 0,className = e.target.className;
            switch(className){
                case "tPrice":
                    for(var i=tPrice.length-1;i>=0;i--){
                        if(tPrice[i]==e.target){
                            num = i;
                        }
                    }
                    break;
                case "tNum":
                    for(var i=tNum.length-1;i>=0;i--){
                        if(tNum[i]==e.target){
                            num = i;
                        }
                    }
                    break;
                case "tAlg":
                    for(var i=tAlg.length-1;i>=0;i--){
                        if(tAlg[i]==e.target){
                            num = i;
                        }
                    }
                    break;
                default:
                    return;
            }


            var context = new Context(tAlg[num].value);
            var money   = 0;
            var totalValue = 0;

            money = context.ContextInterface(tPrice[num].value*tNum[num].value);

            tMoney[num].value = money;

            for(var index=0,len=tMoney.length;index<len;index++){
                totalValue += tMoney[index].value*1;
            }
            total.value = totalValue;
        }

        //绑定DOM事件
        // tPrice[0].addEventListener('keyup',calculate,false);
        // tNum[0].addEventListener('keyup',calculate,false);
        // tAlg[0].addEventListener('change',calculate,false);

        document.addEventListener('keyup',calculate,false);
        document.addEventListener('change',calculate,false);
    </script>
</body>
</html>

最开始我对商品单价、数量、计算方式仅提供一个可操作的地方,这也是《大话设计模式》一书中产品的基本形态,考虑到更良好交互性,我增加了一个按钮,可以增加更多行。这带来的一点小问题就是:起初我只需要为几个元素绑定事件即可,现在要对可能产生的更多元素绑定事件,所以我就选择了“事件代理”,获得发生事件的元素位置,改变同一行中的相应元素的值,对于总价,则总是遍历所有的单行总价相加。

本文由【不负好时光】发布于开源中国,原文链接:https://my.oschina.net/shuaihong/blog/3007140

全部评论: 0

    我有话说: