JavaScript設計模式之策略模式(學習筆記)

在網上搜索「爲何MVC不是一種設計模式呢?」其中有解答:MVC實際上是三個經典設計模式的演變:觀察者模式(Observer)、策略模式(Strategy)、組合模式(Composite)。因此我今天選擇學習策略模式。javascript

策略模式:定義了一系列家族算法,並對每一種算法單獨封裝起來,讓算法之間能夠相互替換,獨立於使用算法的客戶。css

一般我並不會記得「牛頓第必定律」的具體內容,因此我也難保證我會對這個定義記得多久……用FE常常見到的東西來舉個例子說明一下:html

$("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的源碼嗎?java

在《大話設計模式》一書中,做者舉例的是一個商場的收銀系統,在實際操做中,商場可能由於「雙11買一送一」、「滿500立減50」、「中秋節全場11折」等活動而對最終的收費產生變化。若是哪一天商場忽然倒閉,全場兩元,這時候咱們僅須要給軟件系統增長一個全部商品價格變兩元的插件算法(類)便可。node

我先來模擬一下策略模式的基本代碼形態:jquery

<!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部分:mvc

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

相關文章
相關標籤/搜索