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>

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

BTW,在获取元素的时候使用了getElementsByClassName而没有使用querySelectorAll,是因为后者获取的不是一个动态集合。

接着我尝试将昨天学习的观察者设计模式与策略模式混合起来,起初我是这样做的....

<!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">

        //发布者
        function Publisher(obj){
            this.observers = [];
            var number = 0;

            this.getState=function(){
                return number;
            }
            this.setState = function(num){
                number = num;
                this.notice();
            }
        }
        Publisher.prototype.addOb=function(observer){
            var flag = false;
            for (var i = this.observers.length - 1; i >= 0; i--) {
                if(this.observers[i]===observer){
                    flag=true;              
                }
            };
            if(!flag){
                this.observers.push(observer);
            }
            return this;
        }

        Publisher.prototype.removeOb=function(observer){
            var observers = this.observers;
            for (var i = 0; i < observers.length; i++) {
                if(observers[i]===observer){
                    observers.splice(i,1);
                }
            };
            return this;
        }
        Publisher.prototype.notice=function(){
            var observers = this.observers;
            for (var i = 0; i < observers.length; i++) {
                    observers[i].update(this.getState());
            };
        }

        //订阅者
        function Subscribe(obj){
            this.obj = obj;
            this.update = function(data){
                this.obj.value = data;
            };
        }

        //实际应用
        var tPrice = document.getElementsByClassName("tPrice"),
            tNum   = document.getElementsByClassName("tNum"),
            tAlg   = document.getElementsByClassName("tAlg");

        var pba = new Publisher(document);

        var oba = new Subscribe(document.getElementsByClassName("tMoney"));
        var obb = new Subscribe(document.querySelector("#total"));


        pba.addOb(oba).addOb(obb);

        oba.update = function(num){
            var context = new Context(tAlg[num].value);
            var money   = 0;

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

            this.obj[num].value = money;
        }
        obb.update = function(num){
            var totalValue = 0,
                tMoney = document.getElementsByClassName("tMoney");
            for(var index=0,len=tMoney.length;index<len;index++){
                totalValue += tMoney[index].value*1;
            }
            this.obj.value = totalValue;
        }

        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;
            }
            pba.setState(num);
        }

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

噢NO~~~~~~~

这尼玛有哪怕一点优雅的样子吗?反倒是徒添麻烦。。。不行,我既然学了这个,那么接下来就要学MVC了,MVC真的是长这样的吗???于是我又开始了度娘之旅。发现了这样一篇文章:JavaScript的MVC模式

这篇文章也是译文,好在我学过观察者模式了,耐着性子看吧~~~看着有点晕,这种观察者模式跟我之前学的不一样啊?

为了完全弄懂这篇文章的思路,我拿出笔纸开始画图,由于画工不好,字也写得差,我就不贴图了,弄一个对该文章整理思路后的总结:

 

我在之前学习观察者模式的时候,仅仅是对DOM元素进行了发布者与订阅者的区分,却不知道也没有思考过数据、视图与控制器这种结构中的发布者与订阅者区分,所以还是要多看看不同的案例。学习完这篇文章以后,我依葫芦画瓢对我这个“收银系统”也弄一下,但是我毕竟还没有学“组合模式”,所以我也不打算再写一个Controller,仅仅是Model和View之间加入观察者模式。最后的结果是这样的:

<!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">

        //实现了观察者的Event类
        function Event(pub){
            this._pub = pub;
            this._listener = [];
        }
        Event.prototype = {
            attach: function(listener){
                this._listener.push(listener);
            },
            notify: function(num){
                for(var i=0;i<this._listener.length;i++){
                    this._listener[i](this._pub,num);
                }
            }
        }

        //模型
        function Model(data){
            this._data =new Array();
            this._data.push(data);
            this.itemAdded = new Event(this);
            this.itemChanged = new Event(this);            
        }
        Model.prototype = {
            itemAdd : function(arr){
                this._data.push(arr);
                this.itemAdded.notify(this._data.length-1);
            },
            itemChange : function(arr,value){
                var a = arr[0], b=arr[1];
                this._data[a][b] = value;
                this.itemChanged.notify(a);
            }

        }
        //视图
        function View(model,ele){
            this._model = model;
            this._ele = ele;
            var that = this;

            //绑定模型侦听器
            this._model.itemAdded.attach(function(pub,num){
                 that.getTotal(pub,num);
            });
            this._model.itemChanged.attach(function(pub,num){
                 that.getTotal(pub,num);
            });

            //绑定DOM侦听器
            this._ele.eTarget.addEventListener('keyup',function(e){
                var target = e.target,
                    className = target.className;
                if(target.nodeName.toLowerCase()!=="input"){
                    return;
                }
                var elements = document.getElementsByClassName(className),
                    a,b;
                for(var i=elements.length-1;i>=0;i--){
                    if(elements[i]===target){
                        a = i;
                    }
                }
                switch(className){
                    case "tPrice":
                        b = 0;
                        break;
                    case "tNum":
                        b = 1;
                        break;
                    case "tMoney":
                        b = 3;
                        break;
                }
                if(!isNum(a)){
                    a = 0;
                }
                if(!isNum(b)){
                    b = 0;
                }
                that._model.itemChange([a,b],target.value);
            });
            this._ele.eTarget.addEventListener('change',function(e){
                var target = e.target,
                    className = target.className;
                if(target.nodeName.toLowerCase()!=="select"){
                    return;
                }
                var elements = document.getElementsByClassName(className),
                    a;
                for(var i=elements.length-1;i>=0;i--){
                    if(elements[i]===target){
                        a = i;
                    }
                }
                that._model.itemChange([a,2],target.value);
            });
            this._ele.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);

                that._model.itemAdd([0,0,"a",0]);
            });
        }
        View.prototype.getTotal= function(pub,num){
            var price = this._model._data[num][0],
                number = this._model._data[num][1],
                alg = this._model._data[num][2],
                money = this._model._data[num][3];

            var context = new Context(alg);
            money = context.ContextInterface(price*number);
            this._model._data[num][3]=money;

            var total = 0;
            for(var i=0;i<this._model._data.length;i++){
                total += this._model._data[i][3]*1;
            }
            this._ele.money[num].value = money;
            this._ele.total.value = total;
        }

        var mmm = new Model([0,0,"a",0]),

            vvv = new View(mmm,{
                eTarget: document,
                addBtn: document.getElementById("addBtn"),
                money: document.getElementsByClassName("tMoney"),
                total: document.getElementById("total")
            });

    </script>
</body>
</html>

在形成上面的最终结果途中,在对数据进行计算并且将结果传递给Model时,我用了会触发观察者模式更新内容的函数,从而导致在一次计算以后又更新又计算又更新的无限循环中,改为直接对Model中的数据进行操作就没事了。而在我参考的文章中,View层是没有直接对Model进行操作,仅有访问数据的权限,把相关的Model操作放进了Controller层。

以上就是我今天的策略模式学习之路(顺带学了点MVC的相关知识),请各位道友多多指正。o(∩_∩)o 

posted @ 2015-09-07 23:31  哥德  阅读(2055)  评论(0编辑  收藏  举报