常用的Javascript設(shè)計(jì)模式

2018-08-11 14:41 更新

1.策略模式(Strategy)


策略模式定義了算法家族,分別封裝起來(lái),讓他們之間可以互相替換,此模式讓算法的變化不會(huì)影響到使用算法的客戶(hù)。


在理解策略模式之前,我們先來(lái)一個(gè)例子,一般情況下,如果我們要做數(shù)據(jù)合法性驗(yàn)證,很多時(shí)候都是按照swith語(yǔ)句來(lái)判斷,但是這就帶來(lái)幾個(gè)問(wèn)題,首先如果增加需求的話(huà),我們還要再次修改這段代碼以增加邏輯,而且在進(jìn)行單元測(cè)試的時(shí)候也會(huì)越來(lái)越復(fù)雜,代碼如下:


代碼如下:

        validator = {
            validate: function (value, type) {
                switch (type) {
                    case 'isNonEmpty ':
                        {
                            return true; // NonEmpty 驗(yàn)證結(jié)果
                        }
                    case 'isNumber ':
                        {
                            return true; // Number 驗(yàn)證結(jié)果
                            break;
                        }
                    case 'isAlphaNum ':
                        {
                            return true; // AlphaNum 驗(yàn)證結(jié)果
                        }
                    default:
                        {
                            return true;
                        }
                }
            }
        };
        //  測(cè)試
        alert(validator.validate("123", "isNonEmpty"));

那如何來(lái)避免上述代碼中的問(wèn)題呢,根據(jù)策略模式,我們可以將相同的工作代碼單獨(dú)封裝成不同的類(lèi),然后通過(guò)統(tǒng)一的策略處理類(lèi)來(lái)處理,OK,我們先來(lái)定義策略處理類(lèi),代碼如下:

代碼如下:

var validator = {

    // 所有可以的驗(yàn)證規(guī)則處理類(lèi)存放的地方,后面會(huì)單獨(dú)定義
    types: {},

    // 驗(yàn)證類(lèi)型所對(duì)應(yīng)的錯(cuò)誤消息
    messages: [],

    // 當(dāng)然需要使用的驗(yàn)證類(lèi)型
    config: {},

    // 暴露的公開(kāi)驗(yàn)證方法
    // 傳入的參數(shù)是 key => value對(duì)
    validate: function (data) {

        var i, msg, type, checker, result_ok;

        // 清空所有的錯(cuò)誤信息
        this.messages = [];

        for (i in data) {
            if (data.hasOwnProperty(i)) {

                type = this.config[i];  // 根據(jù)key查詢(xún)是否有存在的驗(yàn)證規(guī)則
                checker = this.types[type]; // 獲取驗(yàn)證規(guī)則的驗(yàn)證類(lèi)

                if (!type) {
                    continue; // 如果驗(yàn)證規(guī)則不存在,則不處理
                }
                if (!checker) { // 如果驗(yàn)證規(guī)則類(lèi)不存在,拋出異常
                    throw {
                        name: "ValidationError",
                        message: "No handler to validate type " + type
                    };
                }

                result_ok = checker.validate(data[i]); // 使用查到到的單個(gè)驗(yàn)證類(lèi)進(jìn)行驗(yàn)證
                if (!result_ok) {
                    msg = "Invalid value for *" + i + "*, " + checker.instructions;
                    this.messages.push(msg);
                }
            }
        }
        return this.hasErrors();
    },

    // helper
    hasErrors: function () {
        return this.messages.length !== 0;
    }
};


然后剩下的工作,就是定義types里存放的各種驗(yàn)證類(lèi)了,我們這里只舉幾個(gè)例子:

代碼如下:

// 驗(yàn)證給定的值是否不為空
validator.types.isNonEmpty = {
    validate: function (value) {
        return value !== "";
    },
    instructions: "傳入的值不能為空"
};

// 驗(yàn)證給定的值是否是數(shù)字
validator.types.isNumber = {
    validate: function (value) {
        return !isNaN(value);
    },
    instructions: "傳入的值只能是合法的數(shù)字,例如:1, 3.14 or 2010"
};

// 驗(yàn)證給定的值是否只是字母或數(shù)字
validator.types.isAlphaNum = {
    validate: function (value) {
        return !/[^a-z0-9]/i.test(value);
    },
    instructions: "傳入的值只能保護(hù)字母和數(shù)字,不能包含特殊字符"
};


使用的時(shí)候,我們首先要定義需要驗(yàn)證的數(shù)據(jù)集合,然后還需要定義每種數(shù)據(jù)需要驗(yàn)證的規(guī)則類(lèi)型,代碼如下:

代碼如下:

var data = {
    first_name: "Tom",
    last_name: "Xu",
    age: "unknown",
    username: "TomXu"
};

validator.config = {
    first_name: 'isNonEmpty',
    age: 'isNumber',
    username: 'isAlphaNum'
};


最后,獲取驗(yàn)證結(jié)果的代碼就簡(jiǎn)單了:

代碼如下:

validator.validate(data);

if (validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}


總結(jié)

策略模式定義了一系列算法,從概念上來(lái)說(shuō),所有的這些算法都是做相同的事情,只是實(shí)現(xiàn)不同,他可以以相同的方式調(diào)用所有的方法,減少了各種算法類(lèi)與使用算法類(lèi)之間的耦合。

從另外一個(gè)層面上來(lái)說(shuō),單獨(dú)定義算法類(lèi),也方便了單元測(cè)試,因?yàn)榭梢酝ㄟ^(guò)自己的算法進(jìn)行單獨(dú)測(cè)試。

實(shí)踐中,不僅可以封裝算法,也可以用來(lái)封裝幾乎任何類(lèi)型的規(guī)則,是要在分析過(guò)程中需要在不同時(shí)間應(yīng)用不同的業(yè)務(wù)規(guī)則,就可以考慮是要策略模式來(lái)處理各種變化。



2.裝飾者模式(Decorator)


裝飾者提供比繼承更有彈性的替代方案。 裝飾者用用于包裝同接口的對(duì)象,不僅允許你向方法添加行為,而且還可以將方法設(shè)置成原始對(duì)象調(diào)用(例如裝飾者的構(gòu)造函數(shù))。
裝飾者用于通過(guò)重載方法的形式添加新功能,該模式可以在被裝飾者前面或者后面加上自己的行為以達(dá)到特定的目的。


那么裝飾者模式有什么好處呢?前面說(shuō)了,裝飾者是一種實(shí)現(xiàn)繼承的替代方案。當(dāng)腳本運(yùn)行時(shí),在子類(lèi)中增加行為會(huì)影響原有類(lèi)所有的實(shí)例,而裝飾者卻不然。取而代之的是它能給不同對(duì)象各自添加新行為。如下代碼所示:

代碼如下:


//需要裝飾的類(lèi)(函數(shù))
function Macbook() {
    this.cost = function () {
        return 1000;
    };
}
function Memory(macbook) {
    this.cost = function () {
        return macbook.cost() + 75;
    };
}
function BlurayDrive(macbook) {
    this.cost = function () {
        return macbook.cost() + 300;
    };
}


function Insurance(macbook) {
    this.cost = function () {
        return macbook.cost() + 250;
    };
}


// 用法
var myMacbook = new Insurance(new BlurayDrive(new Memory(new Macbook())));
console.log(myMacbook.cost());
下面是另一個(gè)實(shí)例,當(dāng)我們?cè)谘b飾者對(duì)象上調(diào)用performTask時(shí),它不僅具有一些裝飾者的行為,同時(shí)也調(diào)用了下層對(duì)象的performTask函數(shù)。

代碼如下:


function ConcreteClass() {
    this.performTask = function () {
        this.preTask();
        console.log('doing something');
        this.postTask();
    };
}
function AbstractDecorator(decorated) {
    this.performTask = function () {
        decorated.performTask();
    };
}
function ConcreteDecoratorClass(decorated) {
    this.base = AbstractDecorator;
    this.base(decorated);
    decorated.preTask = function () {
        console.log('pre-calling..');
    };
    decorated.postTask = function () {
        console.log('post-calling..');
    };
}
var concrete = new ConcreteClass();
var decorator1 = new ConcreteDecoratorClass(concrete);
var decorator2 = new ConcreteDecoratorClass(decorator1);
decorator2.performTask();
再來(lái)一個(gè)徹底的例子:

代碼如下:


var tree = {};
tree.decorate = function () {
    console.log('Make sure the tree won\'t fall');
};
tree.getDecorator = function (deco) {
    tree[deco].prototype = this;
    return new tree[deco];
};
tree.RedBalls = function () {
    this.decorate = function () {
        this.RedBalls.prototype.decorate(); // 第7步:先執(zhí)行原型(這時(shí)候是Angel了)的decorate方法
        console.log('Put on some red balls'); // 第8步 再輸出 red
        // 將這2步作為RedBalls的decorate方法
    }
};
tree.BlueBalls = function () {
    this.decorate = function () {
        this.BlueBalls.prototype.decorate(); // 第1步:先執(zhí)行原型的decorate方法,也就是tree.decorate()
        console.log('Add blue balls'); // 第2步 再輸出blue
        // 將這2步作為BlueBalls的decorate方法
    }
};
tree.Angel = function () {
    this.decorate = function () {
        this.Angel.prototype.decorate(); // 第4步:先執(zhí)行原型(這時(shí)候是BlueBalls了)的decorate方法
        console.log('An angel on the top'); // 第5步 再輸出angel
        // 將這2步作為Angel的decorate方法
    }
};
tree = tree.getDecorator('BlueBalls'); // 第3步:將BlueBalls對(duì)象賦給tree,這時(shí)候父原型里的getDecorator依然可用
tree = tree.getDecorator('Angel'); // 第6步:將Angel對(duì)象賦給tree,這時(shí)候父原型的父原型里的getDecorator依然可用
tree = tree.getDecorator('RedBalls'); // 第9步:將RedBalls對(duì)象賦給tree
tree.decorate(); // 第10步:執(zhí)行RedBalls對(duì)象的decorate方法


總結(jié)
裝飾者模式是為已有功能動(dòng)態(tài)地添加更多功能的一種方式,把每個(gè)要裝飾的功能放在單獨(dú)的函數(shù)里,然后用該函數(shù)包裝所要裝飾的已有函數(shù)對(duì)象,因此,當(dāng)需要執(zhí)行特殊行為的時(shí)候,調(diào)用代碼就可以根據(jù)需要有選擇地、按順序地使用裝飾功能來(lái)包裝對(duì)象。優(yōu)點(diǎn)是把類(lèi)(函數(shù))的核心職責(zé)和裝飾功能區(qū)分開(kāi)了。



3.代理模式(Proxy)


代理,顧名思義就是幫助別人做事,GoF對(duì)代理模式的定義如下:

代理模式(Proxy),為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪(fǎng)問(wèn)。

代理模式使得代理對(duì)象控制具體對(duì)象的引用。代理幾乎可以是任何對(duì)象:文件,資源,內(nèi)存中的對(duì)象,或者是一些難以復(fù)制的東西。


我們來(lái)舉一個(gè)簡(jiǎn)單的例子,假如dudu要送酸奶小妹玫瑰花,卻不知道她的聯(lián)系方式或者不好意思,想委托大叔去送這些玫瑰,那大叔就是個(gè)代理(其實(shí)挺好的,可以扣幾朵給媳婦),那我們?nèi)绾蝸?lái)做呢?


代碼如下:

// 先聲明美女對(duì)象
var girl = function (name) {
    this.name = name;
};

// 這是dudu
var dudu = function (girl) {
    this.girl = girl;
    this.sendGift = function (gift) {
        alert("Hi " + girl.name + ", dudu送你一個(gè)禮物:" + gift);
    }
};

// 大叔是代理
var proxyTom = function (girl) {
    this.girl = girl;
    this.sendGift = function (gift) {
        (new dudu(girl)).sendGift(gift); // 替dudu送花咯
    }
};

調(diào)用方式就非常簡(jiǎn)單了:


代碼如下:

var proxy = new proxyTom(new girl("酸奶小妹"));
proxy.sendGift("999朵玫瑰");

實(shí)戰(zhàn)一把

通過(guò)上面的代碼,相信大家對(duì)代理模式已經(jīng)非常清楚了,我們來(lái)實(shí)戰(zhàn)下:我們有一個(gè)簡(jiǎn)單的播放列表,需要在點(diǎn)擊單個(gè)連接(或者全選)的時(shí)候在該連接下方顯示視頻曲介紹以及play按鈕,點(diǎn)擊play按鈕的時(shí)候播放視頻,列表結(jié)構(gòu)如下:


代碼如下:

<h1>Dave Matthews vids</h1>
<p><span id="toggle-all">全選/反選</span></p>
<ol id="vids">
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water</a></li>
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is</a></li>
  <li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a>
</li>
</ol>

我們先來(lái)分析如下,首先我們不僅要監(jiān)控a連接的點(diǎn)擊事件,還要監(jiān)控“全選/反選”的點(diǎn)擊事件,然后請(qǐng)求服務(wù)器查詢(xún)視頻信息,組裝HTML信息顯示在li元素的最后位置上,效果如下:

JavaScript

然后再監(jiān)控play連接的點(diǎn)擊事件,點(diǎn)擊以后開(kāi)始播放,效果如下:
w3c

好了,開(kāi)始,沒(méi)有jQuery,我們自定義一個(gè)選擇器:


代碼如下:

var $ = function (id) {
    return document.getElementById(id);
};

由于Yahoo的json服務(wù)提供了callback參數(shù),所以我們傳入我們自定義的callback以便來(lái)接受數(shù)據(jù),具體查詢(xún)字符串拼裝代碼如下:

代碼如下:

var http = {
    makeRequest: function (ids, callback) {
        var url = 'http://query.yahooapis.com/v1/public/yql?q=',
            sql = 'select * from music.video.id where ids IN ("%ID%")',
            format = "format=json",
            handler = "callback=" + callback,
            script = document.createElement('script');

            sql = sql.replace('%ID%', ids.join('","'));
            sql = encodeURIComponent(sql);

            url += sql + '&' + format + '&' + handler;
            script.src = url;

        document.body.appendChild(script);
    }
};

代理對(duì)象如下:


代碼如下:

var proxy = {
    ids: [],
    delay: 50,
    timeout: null,
    callback: null,
    context: null,
    // 設(shè)置請(qǐng)求的id和callback以便在播放的時(shí)候觸發(fā)回調(diào)
    makeRequest: function (id, callback, context) {

        // 添加到隊(duì)列dd to the queue
        this.ids.push(id);

        this.callback = callback;
        this.context = context;

        // 設(shè)置timeout
        if (!this.timeout) {
            this.timeout = setTimeout(function () {
                proxy.flush();
            }, this.delay);
        }
    },
    // 觸發(fā)請(qǐng)求,使用代理職責(zé)調(diào)用了http.makeRequest
    flush: function () {
        // proxy.handler為請(qǐng)求yahoo時(shí)的callback
        http.makeRequest(this.ids, 'proxy.handler'); 
        // 請(qǐng)求數(shù)據(jù)以后,緊接著執(zhí)行proxy.handler方法(里面有另一個(gè)設(shè)置的callback)
        
        // 清楚timeout和隊(duì)列
        this.timeout = null;
        this.ids = [];

    },
    handler: function (data) {
        var i, max;

        // 單個(gè)視頻的callback調(diào)用
        if (parseInt(data.query.count, 10) === 1) {
            proxy.callback.call(proxy.context, data.query.results.Video);
            return;
        }

        // 多個(gè)視頻的callback調(diào)用
        for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
            proxy.callback.call(proxy.context, data.query.results.Video[i]);
        }
    }
};

視頻處理模塊主要有3種子功能:獲取信息、展示信息、播放視頻:


代碼如下:

var videos = {
    // 初始化播放器代碼,開(kāi)始播放
    getPlayer: function (id) {
        return '' +
            '<object width="400" height="255" id="uvp_fop" allowFullScreen="true">' +
            '<param name="movie" value="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf"\/>' +
            '<param name="flashVars" value="id=v' + id + '&eID=1301797&lang=us&enableFullScreen=0&shareEnable=1"\/>' +
            '<param name="wmode" value="transparent"\/>' +
            '<embed ' +
            'height="255" ' +
            'width="400" ' +
            'id="uvp_fop" ' +
            'allowFullScreen="true" ' +
            'src="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf" ' +
            'type="application/x-shockwave-flash" ' +
            'flashvars="id=v' + id + '&eID=1301797&lang=us&ympsc=4195329&enableFullScreen=1&shareEnable=1"' +
            '\/>' +
            '<\/object>';
                },
    // 拼接信息顯示內(nèi)容,然后在append到li的底部里顯示
    updateList: function (data) {
        var id,
            html = '',
            info;

        if (data.query) {
            data = data.query.results.Video;
        }
        id = data.id;
        html += '<img src="' + data.Image[0].url + '" width="50" \/>';
        html += '<h2>' + data.title + '<\/h2>';
        html += '<p>' + data.copyrightYear + ', ' + data.label + '<\/p>';
        if (data.Album) {
            html += '<p>Album: ' + data.Album.Release.title + ', ' + data.Album.Release.releaseYear + '<br \/>';
        }
        html += '<p><a class="play" href="http://new.music.yahoo.com/videos/--' + id + '">? play<\/a><\/p>';
        info = document.createElement('div');
        info.id = "info" + id;
        info.innerHTML = html;
        $('v' + id).appendChild(info);
    },
    // 獲取信息并顯示
    getInfo: function (id) {
        var info = $('info' + id);

        if (!info) {
            proxy.makeRequest(id, videos.updateList, videos); //執(zhí)行代理職責(zé),并傳入videos.updateList回調(diào)函數(shù)
            return;
        }

        if (info.style.display === "none") {
            info.style.display = '';
        } else {
            info.style.display = 'none';
        }
    }
};

現(xiàn)在可以處理點(diǎn)擊事件的代碼了,由于有很多a連接,如果每個(gè)連接都綁定事件的話(huà),顯然性能會(huì)有問(wèn)題,所以我們將事件綁定在<ol>元素上,然后檢測(cè)點(diǎn)擊的是否是a連接,如果是說(shuō)明我們點(diǎn)擊的是視頻地址,然后就可以播放了:


代碼如下:

$('vids').onclick = function (e) {
    var src, id;

    e = e || window.event;
    src = e.target || e.srcElement;

    // 不是連接的話(huà)就不繼續(xù)處理了
    if (src.nodeName.toUpperCase() !== "A") {
        return;
    }
    //停止冒泡
    if (typeof e.preventDefault === "function") {
        e.preventDefault();
    }
    e.returnValue = false;

    id = src.href.split('--')[1];

    //如果點(diǎn)擊的是已經(jīng)生產(chǎn)的視頻信息區(qū)域的連接play,就開(kāi)始播放
    // 然后return不繼續(xù)了
    if (src.className === "play") {
        src.parentNode.innerHTML = videos.getPlayer(id);
        return;
    }
        
    src.parentNode.id = "v" + id;
    videos.getInfo(id); // 這個(gè)才是第一次點(diǎn)擊的時(shí)候顯示視頻信息的處理代碼
};

全選反選的代碼大同小異,我們就不解釋了:


代碼如下:

$('toggle-all').onclick = function (e) {

    var hrefs, i, max, id;

    hrefs = $('vids').getElementsByTagName('a');
    for (i = 0, max = hrefs.length; i < max; i += 1) {
        // 忽略play連接
        if (hrefs[i].className === "play") {
            continue;
        }
        // 忽略沒(méi)有選擇的項(xiàng)
        if (!hrefs[i].parentNode.firstChild.checked) {
            continue;
        }

        id = hrefs[i].href.split('--')[1];
        hrefs[i].parentNode.id = "v" + id;
        videos.getInfo(id);
    }
};


總結(jié)

代理模式一般適用于如下場(chǎng)合:

1.遠(yuǎn)程代理,也就是為了一個(gè)對(duì)象在不同的地址空間提供局部代表,這樣可以隱藏一個(gè)對(duì)象存在于不同地址空間的事實(shí),就像web service里的代理類(lèi)一樣。
2.虛擬代理,根據(jù)需要?jiǎng)?chuàng)建開(kāi)銷(xiāo)很大的對(duì)象,通過(guò)它來(lái)存放實(shí)例化需要很長(zhǎng)時(shí)間的真實(shí)對(duì)象,比如瀏覽器的渲染的時(shí)候先顯示問(wèn)題,而圖片可以慢慢顯示(就是通過(guò)虛擬代理代替了真實(shí)的圖片,此時(shí)虛擬代理保存了真實(shí)圖片的路徑和尺寸。
3.安全代理,用來(lái)控制真實(shí)對(duì)象訪(fǎng)問(wèn)時(shí)的權(quán)限,一般用于對(duì)象應(yīng)該有不同的訪(fǎng)問(wèn)權(quán)限。
4.智能指引,只當(dāng)調(diào)用真實(shí)的對(duì)象時(shí),代理處理另外一些事情。例如C#里的垃圾回收,使用對(duì)象的時(shí)候會(huì)有引用次數(shù),如果對(duì)象沒(méi)有引用了,GC就可以回收它了。



4.工廠(chǎng)模式(Factory)


工廠(chǎng)模式也是對(duì)象創(chuàng)建模式之一,它通常在類(lèi)或類(lèi)的靜態(tài)方法中去實(shí)現(xiàn)。構(gòu)造對(duì)象的一種方式是使用new操作符,但使用new時(shí)正是針對(duì)實(shí)現(xiàn)編程,會(huì)造成“耦合”問(wèn)題,與具體的類(lèi)關(guān)系緊密。導(dǎo)致代碼更脆弱,缺乏彈性,在復(fù)雜邏輯的項(xiàng)目中建議是面向接口編程。 

先看簡(jiǎn)單工廠(chǎng)模式 


代碼如下:

Person(name, age) { 
var obj = {} 
obj.name = name 
obj.age = age 
return obj 

var p1 = Person('jack', 25) 
var p2 = Person('lily', 22) 

與構(gòu)造函數(shù)方式寫(xiě)一個(gè)類(lèi)的區(qū)別在于沒(méi)有使用this,而是每次都構(gòu)造一個(gè)空對(duì)象,然后給其添加屬性。創(chuàng)建對(duì)象方式不是使用new,而是使用函數(shù)調(diào)用方式。這種方式基本上用來(lái)替代一個(gè)類(lèi)(具有相同屬性的對(duì)象),而復(fù)雜一些的工廠(chǎng)則可以造不同類(lèi)型的對(duì)象。 
下面以個(gè)水果工廠(chǎng)示例 

代碼如下:

function Banana() { 
this.price = '$1.5' 

function Apple() { 
this.price = '$1.2' 

function Orange() { 
this.price = '$2.2' 

// 靜態(tài)工廠(chǎng)類(lèi) 
function Fruit() {} 
Fruit.factory = function(type) { 
if (!window[type]) { 
return 

var fruit = new window[type] 
return fruit 

// 制造不同的水果 
var banana = Fruit.factory('Banana') 
var apple = Fruit.factory('Apple') 
var orange = Fruit.factory('Orange') 

有三個(gè)水果類(lèi)Banana、Apple、Orange,一個(gè)水果工廠(chǎng)類(lèi)Fruit,通過(guò)靜態(tài)方法factory每次可以造出不同的水果類(lèi)對(duì)象。 
工廠(chǎng)模式在JavaScript原生對(duì)象Object也有所體現(xiàn),比如 

代碼如下:

var obj = Object(), 
num = Object(1), 
str = Object('s'), 
boo = Object(false); 

Object就是一個(gè)工廠(chǎng),根據(jù)參數(shù)不同會(huì)構(gòu)造出不同的對(duì)象。obj是一個(gè)空對(duì)象,num是一個(gè)Number類(lèi)型的對(duì)象,str是一個(gè)String類(lèi)型的對(duì)象,boo是Boolean類(lèi)型的對(duì)象。 
jQuery.Callbacks也是一個(gè)工廠(chǎng),每次調(diào)用它都會(huì)返回一個(gè)具有add, remove, fire等方法的對(duì)象。還可以根據(jù)參數(shù)如“once”, “memory”等構(gòu)造出具有不同性質(zhì)的對(duì)象。 

所謂的工廠(chǎng)模式,是指可以返回一個(gè)對(duì)象的方法。 
利用這種模式,我們可以做什么呢?假設(shè)我不滿(mǎn)足現(xiàn)有的DOM對(duì)象里面所擁有的方法,我想要增加一個(gè)自定義的方法叫sayHello,我們可以這樣做: 

代碼如下:

function RemouldNodeObj(DomNode){ 
//先判斷一下傳遞進(jìn)來(lái)的參數(shù)是不是一個(gè)Dom節(jié)點(diǎn) 
if(typeof DomNode == "object" && DomNode.nodeType == 1){ 
DomNode.say = function(){ 
alert("Hello!!"); 

}else{ 
alert("你傳遞進(jìn)來(lái)的參數(shù)不正確!"); 



//這樣調(diào)用: 
window.onload = function(){ 
var oDiv = RemouldNodeObj(document.getElementById("test")); 
//通過(guò)這一步,oDiv就擁有了新的方法say 
oDiv.say(); 


有了上面的基礎(chǔ)后,我們來(lái)實(shí)現(xiàn)點(diǎn)復(fù)雜的功能,我們要實(shí)現(xiàn)只要通過(guò)js的調(diào)用就生成一個(gè)簡(jiǎn)單的form表單,看代碼: 

代碼如下:

<html> 
<head> 
<title>JavaScript之工廠(chǎng)模式</title> 
<script type="text/javascript"> 
function RemouldNodeObj(DOMnode){ 
//先判斷一下傳遞進(jìn)來(lái)的參數(shù)是不是一個(gè)Dom節(jié)點(diǎn) 
if(typeof DOMnode == "object" && DOMnode.nodeType == 1){ 
DOMnode.createForm = function(opt){ 
//下面是一大串的字符串加法,只是為了拼裝出form元素 
var oForm = ""; 
oForm += "<form action=\"" + opt.action + "\" "; 
oForm += "method=\"" + (opt.method || 'GET') + "\" id=\""; 
oForm += (opt.id || "") + "\""; 
oForm += "style=\"width:200px;height:30px;border:2px solid #223344\">"; 
oForm += "</form>"; 
//這里的this不要想得太復(fù)雜,誰(shuí)調(diào)用就指向誰(shuí),所以this指向 oDiv 
this.innerHTML = oForm; 

}else{ 
alert("參數(shù)不正確!"); 

return DOMnode; 


//這樣調(diào)用 
window.onload = function(){ 
var oDiv = RemouldNodeObj(document.getElementById("custom")); 
oDiv.createForm({ 
'action' : 'index.jsp', 
'method' : 'post', 
'id' : 'myForm' 
}); 

</script> 
</head> 

<body> 
<div id="custom">###</div> 
</body> 
</html> 

看到了沒(méi)?這樣的調(diào)用方式是不是很像jQuery?如果能夠解決跨瀏覽器問(wèn)題的話(huà),其實(shí)完全可以做出一個(gè)搜索欄插件來(lái)!


5.模板模式(Template)

一、定義
模板方法是基于繼承的設(shè)計(jì)模式,可以很好的提高系統(tǒng)的擴(kuò)展性。 java中的抽象父類(lèi)、子類(lèi) 
模板方法有兩部分結(jié)構(gòu)組成,第一部分是抽象父類(lèi),第二部分是具體的實(shí)現(xiàn)子類(lèi)。


二、示例
Coffee or Tea 
(1) 把水煮沸 
(2) 用沸水浸泡茶葉 
(3) 把茶水倒進(jìn)杯子 
(4) 加檸檬

/* 抽象父類(lèi):飲料 */
var Beverage = function(){};
// (1) 把水煮沸
Beverage.prototype.boilWater = function() {
  console.log("把水煮沸");
};
// (2) 沸水浸泡
Beverage.prototype.brew = function() {
  throw new Error("子類(lèi)必須重寫(xiě)brew方法");
};
// (3) 倒進(jìn)杯子
Beverage.prototype.pourInCup = function() {
  throw new Error("子類(lèi)必須重寫(xiě)pourInCup方法");
};
// (4) 加調(diào)料
Beverage.prototype.addCondiments = function() {
  throw new Error("子類(lèi)必須重寫(xiě)addCondiments方法");
};
 
/* 模板方法 */
Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
}
 
/* 實(shí)現(xiàn)子類(lèi) Coffee*/
var Coffee = function(){};
Coffee.prototype = new Beverage();
// 重寫(xiě)非公有方法
Coffee.prototype.brew = function() {
  console.log("用沸水沖泡咖啡");
};
Coffee.prototype.pourInCup = function() {
  console.log("把咖啡倒進(jìn)杯子");
};
Coffee.prototype.addCondiments = function() {
  console.log("加牛奶");
};
var coffee = new Coffee();
coffee.init();
通過(guò)模板方法模式,在父類(lèi)中封裝了子類(lèi)的算法框架。這些算法框架在正常狀態(tài)下是適用大多數(shù)子類(lèi)的,但也會(huì)出現(xiàn)“個(gè)性”子類(lèi)。 
如上述流程,加調(diào)料是可選的。 
鉤子方法可以解決這個(gè)問(wèn)題,放置鉤子是隔離變化的一種常見(jiàn)手段。

/* 添加鉤子方法 */
Beverage.prototype.customerWantsCondiments = function() {
  return true;
};
Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  if(this.customerWantsCondiments()) {
    this.addCondiments();
  }
}
 
/* 實(shí)現(xiàn)子類(lèi) Tea*/
var Tea = function(){};
Tea.prototype = new Beverage();
// 重寫(xiě)非公有方法
Tea.prototype.brew = function() {
  console.log("用沸水沖泡茶");
};
Tea.prototype.pourInCup = function() {
  console.log("把茶倒進(jìn)杯子");
};
Tea.prototype.addCondiments = function() {
  console.log("加牛奶");
};
Tea.prototype.customerWantsCondiments = function() {
  return window.confirm("需要添加調(diào)料嗎?");
};
var tea = new Tea();
tea.init();
JavaScript沒(méi)有提供真正的類(lèi)式繼承,繼承是通過(guò)對(duì)象與對(duì)象之間的委托來(lái)實(shí)現(xiàn)的。


三、“好萊塢原則”:別調(diào)用我們,我們會(huì)調(diào)用你
典型使用場(chǎng)景: 
(1)模板方法模式:使用該設(shè)計(jì)模式意味著子類(lèi)放棄了對(duì)自己的控制權(quán),而是改為父類(lèi)通知子類(lèi)。作為子類(lèi),只負(fù)責(zé)提供一些設(shè)計(jì)上的細(xì)節(jié)。 
(2)觀(guān)察者模式:發(fā)布者把消息推送給訂閱者。 
(3)回調(diào)函數(shù):ajax異步請(qǐng)求,把需要執(zhí)行的操作封裝在回調(diào)函數(shù)里,當(dāng)數(shù)據(jù)返回后,這個(gè)回調(diào)函數(shù)才被執(zhí)行。



6.外觀(guān)模式(Facade)


外觀(guān)模式(Facade)為子系統(tǒng)中的一組接口提供了一個(gè)一致的界面,此模塊定義了一個(gè)高層接口,這個(gè)接口值得這一子系統(tǒng)更加容易使用。


外觀(guān)模式不僅簡(jiǎn)化類(lèi)中的接口,而且對(duì)接口與調(diào)用者也進(jìn)行了解耦。外觀(guān)模式經(jīng)常被認(rèn)為開(kāi)發(fā)者必備,它可以將一些復(fù)雜操作封裝起來(lái),并創(chuàng)建一個(gè)簡(jiǎn)單的接口用于調(diào)用。

外觀(guān)模式經(jīng)常被用于JavaScript類(lèi)庫(kù)里,通過(guò)它封裝一些接口用于兼容多瀏覽器,外觀(guān)模式可以讓我們間接調(diào)用子系統(tǒng),從而避免因直接訪(fǎng)問(wèn)子系統(tǒng)而產(chǎn)生不必要的錯(cuò)誤。

外觀(guān)模式的優(yōu)勢(shì)是易于使用,而且本身也比較輕量級(jí)。但也有缺點(diǎn) 外觀(guān)模式被開(kāi)發(fā)者連續(xù)使用時(shí)會(huì)產(chǎn)生一定的性能問(wèn)題,因?yàn)樵诿看握{(diào)用時(shí)都要檢測(cè)功能的可用性。

下面是一段未優(yōu)化過(guò)的代碼,我們使用了外觀(guān)模式通過(guò)檢測(cè)瀏覽器特性的方式來(lái)創(chuàng)建一個(gè)跨瀏覽器的使用方法。


代碼如下:

var addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false);
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn);
    } else {
        el['on' + ev] = fn;
    }
}; 

再來(lái)一個(gè)簡(jiǎn)單的例子,說(shuō)白了就是用一個(gè)接口封裝其它的接口:

代碼如下:

var mobileEvent = {
    // ...
    stop: function (e) {
        e.preventDefault();
        e.stopPropagation();
    }
    // ...
};

總結(jié)

那么何時(shí)使用外觀(guān)模式呢?一般來(lái)說(shuō)分三個(gè)階段:

首先,在設(shè)計(jì)初期,應(yīng)該要有意識(shí)地將不同的兩個(gè)層分離,比如經(jīng)典的三層結(jié)構(gòu),在數(shù)據(jù)訪(fǎng)問(wèn)層和業(yè)務(wù)邏輯層、業(yè)務(wù)邏輯層和表示層之間建立外觀(guān)Facade。

其次,在開(kāi)發(fā)階段,子系統(tǒng)往往因?yàn)椴粩嗟闹貥?gòu)演化而變得越來(lái)越復(fù)雜,增加外觀(guān)Facade可以提供一個(gè)簡(jiǎn)單的接口,減少他們之間的依賴(lài)。

第三,在維護(hù)一個(gè)遺留的大型系統(tǒng)時(shí),可能這個(gè)系統(tǒng)已經(jīng)很難維護(hù)了,這時(shí)候使用外觀(guān)Facade也是非常合適的,為系系統(tǒng)開(kāi)發(fā)一個(gè)外觀(guān)Facade類(lèi),為設(shè)計(jì)粗糙和高度復(fù)雜的遺留代碼提供比較清晰的接口,讓新系統(tǒng)和Facade對(duì)象交互,F(xiàn)acade與遺留代碼交互所有的復(fù)雜工作。



7.建造者模式(Builder)


在軟件系統(tǒng)中,有時(shí)候面臨著“一個(gè)復(fù)雜對(duì)象”的創(chuàng)建工作,其通常由各個(gè)部分的子對(duì)象用一定的算法構(gòu)成;由于需求的變化,這個(gè)復(fù)雜對(duì)象的各個(gè)部分經(jīng)常面臨著劇烈的變化,但是將它們組合在一起的算法確相對(duì)穩(wěn)定。如何應(yīng)對(duì)這種變化?如何提供一種“封裝機(jī)制”來(lái)隔離出“復(fù)雜對(duì)象的各個(gè)部分”的變化,從而保持系統(tǒng)中的“穩(wěn)定構(gòu)建算法”不隨著需求改變而改變?這就是要說(shuō)的建造者模式。

建造者模式可以將一個(gè)復(fù)雜對(duì)象的構(gòu)建與其表示相分離,使得同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表示。也就是說(shuō)如果我們用了建造者模式,那么用戶(hù)就需要指定需要建造的類(lèi)型就可以得到它們,而具體建造的過(guò)程和細(xì)節(jié)就不需要知道了。


這個(gè)模式相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,先上代碼,然后再解釋


代碼如下:

function getBeerById(id, callback) {
    // 使用ID來(lái)請(qǐng)求數(shù)據(jù),然后返回?cái)?shù)據(jù).
    asyncRequest('GET', 'beer.uri?id=' + id, function (resp) {
        // callback調(diào)用 response
        callback(resp.responseText);
    });
}

var el = document.querySelector('#test');
el.addEventListener('click', getBeerByIdBridge, false);

function getBeerByIdBridge(e) {
    getBeerById(this.id, function (beer) {
        console.log('Requested Beer: ' + beer);
    });
}

根據(jù)建造者的定義,表相即是回調(diào),也就是說(shuō)獲取數(shù)據(jù)以后如何顯示和處理取決于回調(diào)函數(shù),相應(yīng)地回調(diào)函數(shù)在處理數(shù)據(jù)的時(shí)候不需要關(guān)注是如何獲取數(shù)據(jù)的,同樣的例子也可以在jquery的ajax方法里看到,有很多回調(diào)函數(shù)(比如success, error回調(diào)等),主要目的就是職責(zé)分離。

同樣再來(lái)一個(gè)jQuery的例子:


代碼如下:

$('<div class= "foo"> bar </div>');

我們只需要傳入要生成的HTML字符,而不需要關(guān)系具體的HTML對(duì)象是如何生產(chǎn)的。


總結(jié)

建造者模式主要用于“分步驟構(gòu)建一個(gè)復(fù)雜的對(duì)象”,在這其中“分步驟”是一個(gè)穩(wěn)定的算法,而復(fù)雜對(duì)象的各個(gè)部分則經(jīng)常變化,其優(yōu)點(diǎn)是:建造者模式的“加工工藝”是暴露的,這樣使得建造者模式更加靈活,并且建造者模式解耦了組裝過(guò)程和創(chuàng)建具體部件,使得我們不用去關(guān)心每個(gè)部件是如何組裝的。



8.觀(guān)察者模式(Observer)



觀(guān)察者模式有時(shí)也稱(chēng)為發(fā)布--訂閱模式,在觀(guān)察者模式中,有一個(gè)觀(guān)察者可以管理所有的目標(biāo),等到有狀態(tài)發(fā)生改變的時(shí)候發(fā)出通知。(其實(shí)sql server中的發(fā)布訂閱也是這個(gè)道理)

假如以前村里的廣播是一個(gè)觀(guān)察者,那么每個(gè)村民就是被觀(guān)察對(duì)象,如果村子里有通知,政策發(fā)生改變的時(shí)候,就需要通過(guò)廣播把這個(gè)消息發(fā)布出去,而不用直接一家家的跑去發(fā)通知。

代碼如下:

<!DOCTYPE html> 
<html lang="en"> 
<head> 
<meta charset="UTF-8"> 
<title>觀(guān)察者模式</title> 
</head> 
<body> 
<script> 
var observer = {//觀(guān)察者 
villagers: [],//村名 
addVillager: function (callback) {//增加村名 
this.villagers[this.villagers.length] = callback; 
}, 
removeVillager: function (callback) {//移除村名 
for (var i = 0; i < this.villagers.length; i++) { 
if (this.villagers[i] === callback) { 
delete this.villagers[i]; 


}, 
publish: function (info) {//發(fā)布信息 
for (var i = 0; i < this.villagers.length; i++) { 
if (typeof this.villagers[i] === 'function') { 
this.villagers[i](info); 


}, 
make: function (o) {//這里將村子建一個(gè)這種廣播方式 
for (var i in this) { 
o[i] = this[i]; 


}; 
var village1 = {}; 
observer.make(village1);//將村子1建立這種觀(guān)察者模式 
var villager11 = { 
read: function (what) { 
console.log('我是第一個(gè)村子的第一個(gè)村名:' + what); 

}; 
var villager12 = { 
read: function (what) { 
console.log('我是第一個(gè)村子的第二個(gè)村名:'+what); 

}; 
village1.addVillager(villager11.read); 
village1.addVillager(villager12.read); 
village1.publish('大家來(lái)開(kāi)會(huì)呀?。?!'); 
village1.removeVillager(villager11.read); 
village1.publish('大家來(lái)開(kāi)會(huì)呀?。?!'); 
/* var village2 = { 
myAddVillager:function(callback){ 
this.addVillager(callback); 
}, 
myRemoveVillager:function(callback){ 
this.removeVillager(callback); 
}, 
myPublish:function(info){ 
this.publish(info); 

}; 
observer.make(village2);//將村子1建立這種觀(guān)察者模式 
var villager21 = { 
read: function (what) { 
console.log('我是第二個(gè)村子的第一個(gè)村名:' + what); 

}; 
var villager22 = { 
read: function (what) { 
console.log('我是第二個(gè)村子的第二個(gè)村名:'+what); 

}; 
village2.myAddVillager(villager21.read); 
village2.myAddVillager(villager22.read); 
village2.myPublish('大家來(lái)領(lǐng)豬肉了?。?!');*/
</script> 
</body> 
</html>


寫(xiě)到這里觀(guān)察者模式實(shí)現(xiàn)了,但是可能會(huì)有多個(gè)村子需要這種模式,那我們這里將observer改造成構(gòu)造函數(shù)的方式

<!DOCTYPE html> 
<html lang="en"> 
<head> 
<meta charset="UTF-8"> 
<title>觀(guān)察者模式</title> 
</head> 
<body> 
<script> 
function Observer(){//觀(guān)察者,這里采用構(gòu)造函數(shù),可以對(duì)不同村落進(jìn)行使用 
if(!(this instanceof Observer)){ 
return new Observer(); 

this.villagers = []; 
}; 
Observer.prototype = { 
// villagers: [],//村名 
addVillager: function (callback) {//增加村名 
this.villagers[this.villagers.length] = callback; 
}, 
removeVillager: function (callback) {//移除村名 
for (var i = 0; i < this.villagers.length; i++) { 
if (this.villagers[i] === callback) { 
delete this.villagers[i]; 


}, 
publish: function (info) {//發(fā)布信息 
for (var i = 0; i < this.villagers.length; i++) { 
if (typeof this.villagers[i] === 'function') { 
this.villagers[i](info); 


}, 
make: function (o) {//這里將村子建一個(gè)這種廣播方式 
for (var i in this) { 
o[i] = this[i]; 



var village1 = {}; 
var observer1 = new Observer(); 
observer1.make(village1);//將村子1建立這種觀(guān)察者模式 
var villager11 = { 
read: function (what) { 
console.log('我是第一個(gè)村子的第一個(gè)村名:' + what); 

}; 
var villager12 = { 
read: function (what) { 
console.log('我是第一個(gè)村子的第二個(gè)村名:'+what); 

}; 
village1.addVillager(villager11.read); 
village1.addVillager(villager12.read); 
village1.publish('大家來(lái)開(kāi)會(huì)呀!??!'); 
village1.removeVillager(villager11.read); 
village1.publish('大家來(lái)開(kāi)會(huì)呀?。?!'); 
var village2 = { 
myAddVillager:function(callback){ 
this.addVillager(callback); 
}, 
myRemoveVillager:function(callback){ 
this.removeVillager(callback); 
}, 
myPublish:function(info){ 
this.publish(info); 

}; 
var observer2 = new Observer(); 
observer2.make(village2);//將村子1建立這種觀(guān)察者模式 
var villager21 = { 
read: function (what) { 
console.log('我是第二個(gè)村子的第一個(gè)村名:' + what); 

}; 
var villager22 = { 
read: function (what) { 
console.log('我是第二個(gè)村子的第二個(gè)村名:'+what); 

}; 
village2.myAddVillager(villager21.read); 
village2.myAddVillager(villager22.read); 
village2.myPublish('大家來(lái)領(lǐng)豬肉了?。?!'); 
</script> 
</body> 
</html>



9.抽象工廠(chǎng)模式(Abstract Factory)


抽象工廠(chǎng)模式說(shuō)明

1. 工廠(chǎng)方法模式的問(wèn)題: 在工廠(chǎng)方法模式里,創(chuàng)建類(lèi)都需要通過(guò) 工廠(chǎng)類(lèi),如果要擴(kuò)展程序,就必須修改工廠(chǎng)類(lèi),這違背了閉包原則,對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉;對(duì)于設(shè)計(jì)有一定的問(wèn)題。
2. 如何解決:就要用到抽象工廠(chǎng)模式,就是對(duì)功能類(lèi)單獨(dú)創(chuàng)建工廠(chǎng)類(lèi),這樣就不必修改之前的代碼,又?jǐn)U展了功能。
3. 工廠(chǎng)模式其實(shí)就是對(duì) 實(shí)現(xiàn)同一接口的 實(shí)現(xiàn)類(lèi) 的 統(tǒng)一 工廠(chǎng)方式創(chuàng)建調(diào)用,但 javascript 沒(méi)有接口這號(hào)東西,所以就去掉這一層 實(shí)現(xiàn),但位功能類(lèi)的成員及方法都應(yīng)當(dāng)一樣;


抽象工廠(chǎng)源碼例子

1. 郵件發(fā)送類(lèi):


代碼如下:

function MailSender() {
    this.to = '';
    this.title = '';
    this.content = '';
}

MailSender.prototype.send = function() {
    //send body
}

2. 短信發(fā)送類(lèi):


代碼如下:

function SmsSender() {
    this.to = '';
    this.title = '';
    this.content = '';
}

SmsSender.prototype.send = function() {
    //send body
}

3. 這里本來(lái)是創(chuàng)建工廠(chǎng)接口類(lèi),這里就去掉了; 直接創(chuàng)建各功能類(lèi)工廠(chǎng);

1>. 郵件工廠(chǎng)類(lèi):


代碼如下:

function MailFactory() {
    
}
MailFactory.prototype.produce = function() {
    return new MailSender();
}


2>. 短信工廠(chǎng)類(lèi):


代碼如下:

function SmsFactory() {
    
}
SmsFactory.prototype.produce = function() {
    return new SmsSender();
}


4. 使用方法:


代碼如下:

var factory = new MailFactory();
var sender = factory.produce();
sender.to = 'toname#mail.com';
sender.title = '抽象工廠(chǎng)模式';
sender.content = '發(fā)送內(nèi)容';
sender.send();

其他說(shuō)明

在面向?qū)ο笳Z(yǔ)言如 java,.net C# 使用的工廠(chǎng)模式,都用到接口,接口是對(duì)外向各種用戶(hù)暴露的可用方法,說(shuō)明這個(gè)功能應(yīng)用有些什么的方法應(yīng)用,用戶(hù)應(yīng)該怎么用這個(gè)接口。對(duì)象以類(lèi)的形式表現(xiàn)出來(lái),代表現(xiàn)實(shí)世界中的某種抽象,也許場(chǎng)景會(huì)有很多類(lèi)似的應(yīng)用,比如上面的 郵件發(fā)送,短信發(fā)送,再比如商場(chǎng)中的各種促銷(xiāo)手段,以及動(dòng)物世界中的各種飛禽走獸等..


如果我們不以接口形式提供用戶(hù)使用,勢(shì)必提供暴露真實(shí)的功能類(lèi)對(duì)象給用戶(hù),用戶(hù)可以隨意對(duì)類(lèi)對(duì)象進(jìn)行修改跟擴(kuò)展,這是不允許的。

工廠(chǎng)方法模式 跟 抽象工廠(chǎng)模式可以很好的解決這樣的問(wèn)題,用戶(hù)只能使用接口調(diào)用工廠(chǎng)類(lèi),來(lái)進(jìn)行規(guī)定的操作;抽象工廠(chǎng)模式更進(jìn)一步使用擴(kuò)展功能變得容易,功能類(lèi)跟工廠(chǎng)類(lèi)都在實(shí)現(xiàn)相應(yīng)的接口上實(shí)現(xiàn)各自類(lèi)級(jí)別的擴(kuò)展,不會(huì)涉及修改到其他的類(lèi)或方法;



10.適配器模式(Adapter)


說(shuō)明: 適配器模式,一般是為要使用的接口,不符本應(yīng)用或本系統(tǒng)使用,而需引入的中間適配層類(lèi)或?qū)ο蟮那闆r;


場(chǎng)景: 就好比我們買(mǎi)了臺(tái)手機(jī),買(mǎi)回來(lái)后發(fā)現(xiàn),充電線(xiàn)插頭是三插頭,但家里,只有兩插頭的口的插座,怎么辦?為了方便,也有為能在任何地方都能充上電,就得去買(mǎi)個(gè)通用充電適配器; 這樣手機(jī)才能在自己家里充上電;不然只能放著,或跑到有這個(gè)插頭的地方充電;


實(shí)際開(kāi)發(fā)環(huán)境下,由于舊的系統(tǒng),或第三方應(yīng)用提供的接口,與我們定義的接口不匹配,在以面向接口編程的環(huán)境下,就無(wú)法使用這樣舊的,或第三方的接口,這時(shí)我們就使用適配類(lèi)繼承待適匹配的類(lèi),并讓適配類(lèi)實(shí)現(xiàn)接口的方式來(lái)引入舊的系統(tǒng)或第三方應(yīng)用的接口;


這樣使用接口編程時(shí),就可以使用這個(gè)適匹配類(lèi),來(lái)間接調(diào)用舊的系統(tǒng)或第三方應(yīng)用的接口。


在 Javascript 要實(shí)現(xiàn)類(lèi)似動(dòng)態(tài)面向?qū)ο笳Z(yǔ)言的適配器模式的代碼,可以使用到 prototype 的繼承實(shí)例來(lái)實(shí)現(xiàn);因?yàn)槭腔诮涌诩s束的,但是Javascript沒(méi)有接口這號(hào)東西,我們?nèi)サ艚涌谶@一層,直接實(shí)現(xiàn)接口實(shí)現(xiàn)類(lèi) Target ,模擬類(lèi)似的源碼出來(lái);


源碼實(shí)例

1. 待適配的類(lèi)及接口方法:


代碼如下:


function Adaptee() {
    this.name = 'Adaptee';
}
Adaptee.prototype.getName = function() {
    return this.name;
}

2. 普通實(shí)現(xiàn)類(lèi) [由于 Javascript 中沒(méi)有接口,所以就直接提供實(shí)現(xiàn)類(lèi)]


代碼如下:

function Target() {
    this.name = 'Target';
}

Target.prototype.queryName= function() {
    return this.name;
}

3. 適配類(lèi):


代碼如下:

function Adapte() {
    this.name = '';
}

Adapte.prototype = new Adaptee();

Adapte.prototype.queryName = function() {
    this.getName();
}

4.使用方法:


代碼如下:

var local = new Target();
local.queryName(); //調(diào)用普通實(shí)現(xiàn)類(lèi)

var adapte = new Adapte();
adapte.queryName(); //調(diào)用舊的系統(tǒng)或第三方應(yīng)用接口;


其他說(shuō)明

上面第四步,var local 以及 var adapte 類(lèi)似像 Java,C# 這樣的面向?qū)ο笳Z(yǔ)言中接口引用指定,如:


代碼如下:

interface Target {
    public String queryName();
}
//接口引用指向
Target local = new RealTarget(); //即上面 Javascript 的 Target 實(shí)現(xiàn)類(lèi)
local.queryName();

//適配器
Target adapte = new Adapte();
adapte.queryName();

可見(jiàn)適配器類(lèi)是連接接口與目標(biāo)類(lèi)接口的中間層;就是用來(lái)解決,需要的目標(biāo)已經(jīng)存在了,但我們無(wú)法直接使用,不能跟我們的代碼定義協(xié)同使用,就得使用適器模式,適配器模式也叫轉(zhuǎn)換模式,包裝模式;



11.單例模式(Singleton)


單例模式的定義:保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪(fǎng)問(wèn)它的全局訪(fǎng)問(wèn)點(diǎn)。


單例模式是一種常用的模式,有一些對(duì)象我們往往只需要一個(gè),比如線(xiàn)程池、全局緩存、瀏覽器的window對(duì)象。在js開(kāi)發(fā)中,單例模式的用途同樣非常廣泛。試想一下,當(dāng)我們單擊登錄按鈕的時(shí)候,頁(yè)面中會(huì)出現(xiàn)一個(gè)登錄框,而這個(gè)浮窗是唯一的,無(wú)論單擊多少次登錄按鈕,這個(gè)浮窗只會(huì)被創(chuàng)建一次。因此這個(gè)登錄浮窗就適合用單例模式。


1、單例模式的使用場(chǎng)景
在使用一種模式之前,我們最好要知道,這種模式的使用場(chǎng)景。用了這么久的單例模式,竟全然不知!用它具體有哪些好處呢?
1).可以用它來(lái)劃分命名空間(這個(gè)就是就是經(jīng)常用的了)
2).利用分支技術(shù)來(lái)封裝瀏覽器之間的差異(這個(gè)還真沒(méi)用過(guò),挺新鮮)
3).借助單例模式,可以把代碼組織的更為一致,方便閱讀與維護(hù)(這個(gè)也用過(guò)了)


2、最基本的單例模式
最簡(jiǎn)單的單例其實(shí)就是一個(gè)對(duì)象字面量。它把一批有一定關(guān)聯(lián)的方法和屬性組織在一起。

var Singleton = {
  attr1: true , 
  attr2: 10 ,
  method1 : function(){
    alert('我是方法1');
  },
  method2 : function(){
    alert('我是方法2');
  }
};
這個(gè)對(duì)象可以被修改。你可以添加屬性和方法。你也可以用delete運(yùn)算符刪除現(xiàn)有成員。這實(shí)際上違背了面向?qū)ο笤O(shè)計(jì)的一條原則:類(lèi)可以被擴(kuò)展,但不應(yīng)該被修改。如果某些變量需要保護(hù),那么可以將其定義在閉包中。
對(duì)象字面量只是創(chuàng)建單例的方法之一。也并非所有的對(duì)象字面量都是單例,那些只是用來(lái)模仿關(guān)聯(lián)數(shù)組或容納數(shù)據(jù)的對(duì)象字面量顯然不是單例。

3、借用閉包創(chuàng)建單例
閉包主要的目地 保護(hù)數(shù)據(jù)

// 命名空間
var BHX = {} ;
BHX.Singleton = (function(){
  // 添加自己的私有成員
  var a1 = true ;
  var a2 = 10 ;
  var f1 = function(){
    alert('f1');
  }
  var f2 = function(){
    alert('f2');
  }        
  // 把塊級(jí)作用域里的執(zhí)行結(jié)果賦值給我的單例對(duì)象
  return {
      attr1: a1 , 
      attr2: a2 ,
      method1 : function(){
        return f1();
      },
      method2 : function(){
        return f2();
      }            
  } ;
})();
 
alert(BHX.Singleton.attr1);
BHX.Singleton.method1();
這種單例模式又稱(chēng)模塊模式,指的是它可以把一批相關(guān)的方法和屬性組織為模塊并起到劃分命名空間的作用。

4、單例模式用于劃分命名空間
1)、防止全局聲明的修改

/*using a namespace*/
 
var BHX = {};
BHX.Singleton = {
  attr1: true , 
  attr2: 10 ,
  method1 : function(){
    alert('我是方法1');
  },
  method2 : function(){
    alert('我是方法2');
  }        
};
BHX.Singleton.attr1;
var attr1 = false;
這樣以來(lái),即使我們?cè)谕饷媛暶髁讼嗤淖兞?,也能在一定程度上防止attr1的被修改。

2)、防止其它來(lái)源代碼的修改
現(xiàn)在網(wǎng)頁(yè)上的JavaScript代碼往往不止用一個(gè)來(lái)源,什么庫(kù)代碼、廣告代碼和徽章代碼。為了避免與自己代碼的沖突,可以定義一個(gè)包含自己所有代碼的對(duì)象。

var XGP = {};
XGP.Common = {
  //A singleton with common methods used by all objects and modules
}
XGP.ErrorCodes = {
  //An object literal used to store data
}
XGP.PageHandler = {
  //A singleton with page specific methods and attributes.
}

3)、用作專(zhuān)用代碼封裝
在擁有許多網(wǎng)頁(yè)的網(wǎng)站中,有些代碼是所有網(wǎng)頁(yè)都要用到的,他們通常被存放在獨(dú)立的文件中;而有些代碼則是某個(gè)網(wǎng)頁(yè)專(zhuān)用的,不會(huì)被用到其他地方。最好把這兩種代碼分別包裝在自己的單例對(duì)象中。
我們經(jīng)常要用Javascript為表單添加功能。出于平穩(wěn)退化方面的考慮,通常先創(chuàng)建一個(gè)不依賴(lài)于Javascript的、使用普通提交機(jī)制完成任務(wù)的純HTML網(wǎng)頁(yè)。

XGP.RegPage = {
  FORM_ID: 'reg-form',
  OUTPUT_ID: 'reg-result',
 
  handleSubmit: function(e){
    e.preventDefault(); //stop the normal form submission
 
    var data = {};
    var inputs = XGP.RegPage.formEl.getElementByTagName('input');
 
    for(var i=0, len=inputs.length; i<len; i++){
      data[inputs[i].name] = inputs[i].value;
    }
 
    XGP.RegPage.sendRegistration(data);
  },
  sendRegistration: function(data){
    //make an xhr request and call displayResult() when response is recieved
    ...
  },
  displayResult: function(response){
    XGP.RegPage.outputEl.innerHTML = response;
  },
  init: function(){
    XGP.RegPage.formEl =$(XGP.RegPage.Form_ID);
    XGP.RegPage.outputEl = $(XGP.RegPage.OUTPUT_ID);
    //hijack the form submission
    addEvent(XGP.RegPage.formEl, 'submit', XGP.RegPage.handleSubmit);
  }
}
//invoke initialization method after the page load
addLoadEvent(XGP.RegPage.init);

5、惰性單例
前面所講的單例模式又一個(gè)共同點(diǎn):?jiǎn)卫龑?duì)象都是在腳本加載時(shí)被創(chuàng)建出來(lái)。對(duì)于資源密集的或配置開(kāi)銷(xiāo)甚大的單例,更合理的做法是將其實(shí)例化推遲到需要使用他的時(shí)候。
這種技術(shù)就是惰性加載(lazy loading)。
實(shí)現(xiàn)步驟如下:
1).將所有代碼移到constructor方法中
2).全權(quán)控制調(diào)用時(shí)機(jī)(正是getInstance所要做的)

XGP.lazyLoading = (function(){
  var uniqInstance;
 
  function constructor(){
    var attr = false;
    function method(){
 
    }
 
    return {
      attrp: true,
      methodp: function(){
 
      }
    }
  }
 
  return {
    getInstance: function(){
      if(!uniqInstance){
        uniqInstance = constructor();
      }
      return uniqInstance;
    }
  }
})();

6、分支技術(shù)
分支是一種用來(lái)把瀏覽器間的差異封裝在運(yùn)行期間進(jìn)行設(shè)置的動(dòng)態(tài)方法中的技術(shù)。

// 分支單例 (判斷程序的分支 <瀏覽器差異的檢測(cè)>)
var Ext = {} ;
var def = false ;
Ext.More = (function(){
  var objA = {    // 火狐瀏覽器 內(nèi)部的一些配置
      attr1:'FF屬性1'
      // 屬性1 
      // 屬性2 
      // 方法1 
      // 方法2
  } ;
  var objB = {    // IE瀏覽器 內(nèi)部的一些配置
      attr1:'IE屬性1'
      // 屬性1 
      // 屬性2 
      // 方法1 
      // 方法2             
  } ;
  return (def) ?objA:objB;
})();
alert(Ext.More.attr1);
比如說(shuō),如果網(wǎng)站中要頻繁使用xhr,每次調(diào)用都要再次運(yùn)行瀏覽器嗅探代碼,這樣會(huì)嚴(yán)重缺乏效率。更有效的做法是在腳本加載時(shí)一次性地確定針對(duì)瀏覽器的代碼。這正是分支技術(shù)所做的事情。當(dāng)然,分支技術(shù)并不總是更高效的選擇,在兩個(gè)或者多個(gè)分支中只有一個(gè)分支被用到了,其他分支就占用了內(nèi)存。
在考慮是否使用分支技術(shù)的時(shí)候,必須在縮短時(shí)間和占用更多內(nèi)存這一利一弊之間權(quán)衡一下。
下面利用分支技術(shù)實(shí)現(xiàn)XHR:

var XHR = (function(){
  var standard = {
    createXhrObj: function(){
      return new XMLHttpRequest();
    }
  };
  var activeXNew = {
    createXhrObj: function(){
      return new ActiveXObject('Msxml2.XMLHTTP');
    }
  };
  var activeXOld = {
    createXhrObj: function(){
      return new ActiveXObject('Microsoft.XMLHTTP');
    }
  };
 
  var testObj;
  try{
    testObj = standard.createXhrObj();
    return testObj;
  }catch(e){
    try{
      testObj = activeXNew.createXhrObj();
      return testObj;
    }catch(e){
      try{
        testObj = activeXOld.createXhrObj();
        return testObj;
      }catch(e){
        throw new Error('No XHR object found in this environment.');
      }
    }
  }
})();


7、單例模式的弊端
了解了這么多關(guān)于單例的知識(shí),我們?cè)賮?lái)看看它的弊端。
由于單例模式提供的是一種單點(diǎn)訪(fǎng)問(wèn),所以它有可能導(dǎo)致模塊間的強(qiáng)耦合。因此也就不利于單元測(cè)試了。
綜上,單例還是留給定義命名空間和實(shí)現(xiàn)分支型方法這些用途。
通過(guò)七點(diǎn)不同方面對(duì)單例模式的介紹,大家是不是對(duì)單例模式有了更深入的了解,希望這篇文章可以幫到大家。



12.命令模式(Command)


命令模式(Command)的定義是:用于將一個(gè)請(qǐng)求封裝成一個(gè)對(duì)象,從而使你可用不同的請(qǐng)求對(duì)客戶(hù)進(jìn)行參數(shù)化;對(duì)請(qǐng)求排隊(duì)或者記錄請(qǐng)求日志,以及執(zhí)行可撤銷(xiāo)的操作。也就是說(shuō)改模式旨在將函數(shù)的調(diào)用、請(qǐng)求和操作封裝成一個(gè)單一的對(duì)象,然后對(duì)這個(gè)對(duì)象進(jìn)行一系列的處理。此外,可以通過(guò)調(diào)用實(shí)現(xiàn)具體函數(shù)的對(duì)象來(lái)解耦命令對(duì)象與接收對(duì)象。


我們來(lái)通過(guò)車(chē)輛購(gòu)買(mǎi)程序來(lái)展示這個(gè)模式,首先定義車(chē)輛購(gòu)買(mǎi)的具體操作類(lèi):


代碼如下:

$(function () {

    var CarManager = {

        // 請(qǐng)求信息
        requestInfo: function (model, id) {
            return 'The information for ' + model +
        ' with ID ' + id + ' is foobar';
        },

        // 購(gòu)買(mǎi)汽車(chē)
        buyVehicle: function (model, id) {
            return 'You have successfully purchased Item '
        + id + ', a ' + model;
        },

        // 組織view
        arrangeViewing: function (model, id) {
            return 'You have successfully booked a viewing of '
        + model + ' ( ' + id + ' ) ';
        }
    };
})();

來(lái)看一下上述代碼,通過(guò)調(diào)用函數(shù)來(lái)簡(jiǎn)單執(zhí)行manager的命令,然而在一些情況下,我們并不想直接調(diào)用對(duì)象內(nèi)部的方法。這樣會(huì)增加對(duì)象與對(duì)象間的依賴(lài)?,F(xiàn)在我們來(lái)擴(kuò)展一下這個(gè)CarManager 使其能夠接受任何來(lái)自包括model和car ID 的CarManager對(duì)象的處理請(qǐng)求。根據(jù)命令模式的定義,我們希望實(shí)現(xiàn)如下這種功能的調(diào)用:


代碼如下:

CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });

根據(jù)這樣的需求,我們可以這樣啦實(shí)現(xiàn)CarManager.execute方法:

代碼如下:

CarManager.execute = function (command) {
    return CarManager[command.request](command.model, command.carID);
};

改造以后,調(diào)用就簡(jiǎn)單多了,如下調(diào)用都可以實(shí)現(xiàn)(當(dāng)然有些異常細(xì)節(jié)還是需要再完善一下的):

代碼如下:

CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });

總結(jié)

命令模式比較容易設(shè)計(jì)一個(gè)命令隊(duì)列,在需求的情況下比較容易將命令計(jì)入日志,并且允許接受請(qǐng)求的一方?jīng)Q定是否需要調(diào)用,而且可以實(shí)現(xiàn)對(duì)請(qǐng)求的撤銷(xiāo)和重設(shè),而且由于新增的具體類(lèi)不影響其他的類(lèi),所以很容易實(shí)現(xiàn)。

但敏捷開(kāi)發(fā)原則告訴我們,不要為代碼添加基于猜測(cè)的、實(shí)際不需要的功能,如果不清楚一個(gè)系統(tǒng)是否需要命令模式,一般就不要著急去實(shí)現(xiàn)它,事實(shí)上,在需求的時(shí)通過(guò)重構(gòu)實(shí)現(xiàn)這個(gè)模式并不困難,只有在真正需求如撤銷(xiāo)、恢復(fù)操作等功能時(shí),把原來(lái)的代碼重構(gòu)為命令模式才有意義。



以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)