JavaScript 設(shè)計(jì)模式之橋接模式

2018-07-28 17:57 更新

介紹

橋接模式(Bridge)將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化。

正文

橋接模式最常用在事件監(jiān)控上,先看一段代碼:

addEvent(element, 'click', getBeerById);
function getBeerById(e) {
var id = this.id;
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// Callback response.
console.log('Requested Beer: ' + resp.responseText);
});
}

上述代碼,有個(gè)問(wèn)題就是getBeerById必須要有瀏覽器的上下文才能使用,因?yàn)槠鋬?nèi)部使用了this.id這個(gè)屬性,如果沒(méi)用上下文,那就歇菜了。所以說(shuō)一般稍微有經(jīng)驗(yàn)的程序員都會(huì)將程序改造成如下形式:

function getBeerById(id, callback) {
// 通過(guò)ID發(fā)送請(qǐng)求,然后返回?cái)?shù)據(jù)
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// callback response
callback(resp.responseText);
});
}

實(shí)用多了,對(duì)吧?首先ID可以隨意傳入,而且還提供了一個(gè)callback函數(shù)用于自定義處理函數(shù)。但是這個(gè)和橋接有什么關(guān)系呢?這就是下段代碼所要體現(xiàn)的了:

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

這里的getBeerByIdBridge就是我們定義的橋,用于將抽象的click事件和getBeerById連接起來(lái),同時(shí)將事件源的ID,以及自定義的call函數(shù)(console.log輸出)作為參數(shù)傳入到getBeerById函數(shù)里。

這個(gè)例子看起來(lái)有些簡(jiǎn)單,我們?cè)賮?lái)一個(gè)復(fù)雜點(diǎn)的實(shí)戰(zhàn)例子。

實(shí)戰(zhàn)XHR連接隊(duì)列

我們要構(gòu)建一個(gè)隊(duì)列,隊(duì)列里存放了很多ajax請(qǐng)求,使用隊(duì)列(queue)主要是因?yàn)橐_保先加入的請(qǐng)求先被處理。任何時(shí)候,我們可以暫停請(qǐng)求、刪除請(qǐng)求、重試請(qǐng)求以及支持對(duì)各個(gè)請(qǐng)求的訂閱事件。

基礎(chǔ)核心函數(shù)

在正式開(kāi)始之前,我們先定義一下核心的幾個(gè)封裝函數(shù),首先第一個(gè)是異步請(qǐng)求的函數(shù)封裝:

var asyncRequest = (function () {
    function handleReadyState(o, callback) {
        var poll = window.setInterval(
                    function () {
                        if (o && o.readyState == 4) {
                            window.clearInterval(poll);
                            if (callback) {
                                callback(o);
                            }
                        }
                    },
                    50
                    );
    }

    var getXHR = function () {
        var http;
        try {
            http = new XMLHttpRequest;
            getXHR = function () {
                return new XMLHttpRequest;
            };
        }

        catch (e) {
            var msxml = [
                        'MSXML2.XMLHTTP.3.0',
                        'MSXML2.XMLHTTP',
                        'Microsoft.XMLHTTP'
                        ];

            for (var i = 0, len = msxml.length; i < len; ++i) {
                try {
                    http = new ActiveXObject(msxml[i]);
                    getXHR = function () {
                        return new ActiveXObject(msxml[i]);
                    };
                    break;
                }
                catch (e) { }
            }
        }
        return http;
    };

    return function (method, uri, callback, postData) {
        var http = getXHR();
        http.open(method, uri, true);
        handleReadyState(http, callback);
        http.send(postData || null);
        return http;
    };
})();

上述封裝的自執(zhí)行函數(shù)是一個(gè)通用的Ajax請(qǐng)求函數(shù),相信屬性Ajax的人都能看懂了。

接下來(lái)我們定義一個(gè)通用的添加方法(函數(shù))的方法:

Function.prototype.method = function (name, fn) {
    this.prototype[name] = fn;
    return this;
};

最后再添加關(guān)于數(shù)組的2個(gè)方法,一個(gè)用于遍歷,一個(gè)用于篩選:

if (!Array.prototype.forEach) {
    Array.method('forEach', function (fn, thisObj) {
        var scope = thisObj || window;
        for (var i = 0, len = this.length; i < len; ++i) {
            fn.call(scope, this[i], i, this);
        }
    });
}

if (!Array.prototype.filter) {
    Array.method('filter', function (fn, thisObj) {
        var scope = thisObj || window;
        var a = [];
        for (var i = 0, len = this.length; i < len; ++i) {
            if (!fn.call(scope, this[i], i, this)) {
                continue;
            }
            a.push(this[i]);
        }
        return a;
    });
}

因?yàn)橛械男滦蜑g覽器已經(jīng)支持了這兩種功能(或者有些類庫(kù)已經(jīng)支持了),所以要先判斷,如果已經(jīng)支持的話,就不再處理了。

觀察者系統(tǒng)

觀察者在隊(duì)列里的事件過(guò)程中扮演著重要的角色,可以隊(duì)列處理時(shí)(成功、失敗、掛起)訂閱事件:

window.DED = window.DED || {};
DED.util = DED.util || {};
DED.util.Observer = function () {
    this.fns = [];
}

DED.util.Observer.prototype = {
    subscribe: function (fn) {
        this.fns.push(fn);
    },

    unsubscribe: function (fn) {
        this.fns = this.fns.filter(
            function (el) {
                if (el !== fn) {
                    return el;
                }
            }
            );
            },
    fire: function (o) {
        this.fns.forEach(
            function (el) {
                el(o);
            }
            );
    }
};

隊(duì)列主要實(shí)現(xiàn)代碼

首先訂閱了隊(duì)列的主要屬性和事件委托:

DED.Queue = function () {
    // 包含請(qǐng)求的隊(duì)列.
 this.queue = [];
    // 使用Observable對(duì)象在3個(gè)不同的狀態(tài)上,以便可以隨時(shí)訂閱事件
 this.onComplete = new DED.util.Observer;
    this.onFailure = new DED.util.Observer;
    this.onFlush = new DED.util.Observer;

    // 核心屬性,可以在外部調(diào)用的時(shí)候進(jìn)行設(shè)置
 this.retryCount = 3;
    this.currentRetry = 0;
    this.paused = false;
    this.timeout = 5000;
    this.conn = {};
    this.timer = {};
};

然后通過(guò)DED.Queue.method的鏈?zhǔn)秸{(diào)用,則隊(duì)列上添加了很多可用的方法:

DED.Queue.
    method('flush', function () {
        // flush方法
 if (!this.queue.length > 0) {
            return;
        }

        if (this.paused) {
            this.paused = false;
            return;
        }

        var that = this;
        this.currentRetry++;
        var abort = function () {
            that.conn.abort();
            if (that.currentRetry == that.retryCount) {
                that.onFailure.fire();
                that.currentRetry = 0;
            } else {
                that.flush();
            }
        };

        this.timer = window.setTimeout(abort, this.timeout);
        var callback = function (o) {
            window.clearTimeout(that.timer);
            that.currentRetry = 0;
            that.queue.shift();
            that.onFlush.fire(o.responseText);
            if (that.queue.length == 0) {
                that.onComplete.fire();
                return;
            }

            // recursive call to flush
 that.flush();

        };

        this.conn = asyncRequest(
            this.queue[0]['method'],
            this.queue[0]['uri'],
            callback,
            this.queue[0]['params']
            );
    }).
    method('setRetryCount', function (count) {
        this.retryCount = count;
    }).
    method('setTimeout', function (time) {
        this.timeout = time;
    }).
    method('add', function (o) {
        this.queue.push(o);
    }).
    method('pause', function () {
        this.paused = true;
    }).
    method('dequeue', function () {
        this.queue.pop();
    }).
    method('clear', function () {
        this.queue = [];
    });

代碼看起來(lái)很多,折疊以后就可以發(fā)現(xiàn),其實(shí)就是在隊(duì)列上定義了flush, setRetryCount, setTimeout, add, pause, dequeue, 和clear方法。

簡(jiǎn)單調(diào)用

var q = new DED.Queue;
// 設(shè)置重試次數(shù)高一點(diǎn),以便應(yīng)付慢的連接
q.setRetryCount(5);
// 設(shè)置timeout時(shí)間
q.setTimeout(1000);
// 添加2個(gè)請(qǐng)求.
q.add({
    method: 'GET',
    uri: '/path/to/file.php?ajax=true'
});

q.add({
    method: 'GET',
    uri: '/path/to/file.php?ajax=true&woe=me'
});

// flush隊(duì)列
q.flush();
// 暫停隊(duì)列,剩余的保存
q.pause();
// 清空.
q.clear();
// 添加2個(gè)請(qǐng)求.
q.add({
    method: 'GET',
    uri: '/path/to/file.php?ajax=true'
});

q.add({
    method: 'GET',
    uri: '/path/to/file.php?ajax=true&woe=me'
});

// 從隊(duì)列里刪除最后一個(gè)請(qǐng)求.
q.dequeue();
// 再次Flush
q.flush();

橋接呢?

上面的調(diào)用代碼里并沒(méi)有橋接,那橋呢?看一下下面的完整示例,就可以發(fā)現(xiàn)處處都有橋哦:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Ajax Connection Queue</title>
    <script src="utils.js"></script>
    <script src="queue.js"></script>
    <script type="text/javascript">
        addEvent(window, 'load', function () {
            // 實(shí)現(xiàn).
var q = new DED.Queue;
            q.setRetryCount(5);
            q.setTimeout(3000);
            var items = $('items');
            var results = $('results');
            var queue = $('queue-items');
            // 在客戶端保存跟蹤自己的請(qǐng)求
var requests = [];
            // 每個(gè)請(qǐng)求flush以后,訂閱特殊的處理步驟
            q.onFlush.subscribe(function (data) {
                results.innerHTML = data;
                requests.shift();
                queue.innerHTML = requests.toString();
            });
            // 訂閱時(shí)間處理步驟
            q.onFailure.subscribe(function () {
                results.innerHTML += ' <span style="color:red;">Connection Error!</span>';
            });
            // 訂閱全部成功的處理步驟x
            q.onComplete.subscribe(function () {
                results.innerHTML += ' <span style="color:green;">Completed!</span>';
            });
            var actionDispatcher = function (element) {
                switch (element) {
                    case 'flush':
                        q.flush();
                        break;
                    case 'dequeue':
                        q.dequeue();
                        requests.pop();
                        queue.innerHTML = requests.toString();
                        break;
                    case 'pause':
                        q.pause();
                        break;
                    case 'clear':
                        q.clear();
                        requests = [];
                        queue.innerHTML = '';
                        break;
                }
            };
            var addRequest = function (request) {
                var data = request.split('-')[1];
                q.add({
                    method: 'GET',
                    uri: 'bridge-connection-queue.php?ajax=true&s=' + data,
                    params: null
                });
                requests.push(data);
                queue.innerHTML = requests.toString();
            };
            addEvent(items, 'click', function (e) {
                var e = e || window.event;
                var src = e.target || e.srcElement;
                try {
                    e.preventDefault();
                }
                catch (ex) {
                    e.returnValue = false;
                }
                actionDispatcher(src.id);
            });
            var adders = $('adders');
            addEvent(adders, 'click', function (e) {
                var e = e || window.event;
                var src = e.target || e.srcElement;
                try {
                    e.preventDefault();
                }
                catch (ex) {
                    e.returnValue = false;
                }
                addRequest(src.id);
            });
        });
    </script>
    <style type="text/css" media="screen">
        body
        {
            font: 100% georgia,times,serif;
        }
        h1, h2
        {
            font-weight: normal;
        }
        #queue-items
        {
            height: 1.5em;
        }
        #add-stuff
        {
            padding: .5em;
            background: #ddd;
            border: 1px solid #bbb;
        }
        #results-area
        {
            padding: .5em;
            border: 1px solid #bbb;
        }
    </style>
</head>
<body id="example">
    <div id="doc">
        <h1>
            異步聯(lián)接請(qǐng)求</h1>
        <div id="queue-items">
        </div>
        <div id="add-stuff">
            <h2>向隊(duì)列里添加新請(qǐng)求</h2>
            <ul id="adders">
                <li><a href="#" id="action-01">添加 "01" 到隊(duì)列</a></li>
                <li><a href="#" id="action-02">添加 "02" 到隊(duì)列</a></li>
                <li><a href="#" id="action-03">添加 "03" 到隊(duì)列</a></li>
            </ul>
        </div>
        <h2>隊(duì)列控制</h2>
        <ul id='items'>
            <li><a href="#" id="flush">Flush</a></li>
            <li><a href="#" id="dequeue">出列Dequeue</a></li>
            <li><a href="#" id="pause">暫停Pause</a></li>
            <li><a href="#" id="clear">清空Clear</a></li>
        </ul>
        <div id="results-area">
            <h2>
                結(jié)果:
            </h2>
            <div id="results">
            </div>
        </div>
    </div>
</body>
</html>

在這個(gè)示例里,你可以做flush隊(duì)列,暫停隊(duì)列,刪除隊(duì)列里的請(qǐng)求,清空隊(duì)列等各種動(dòng)作,同時(shí)相信大家也體會(huì)到了橋接的威力了。

總結(jié)

橋接模式的優(yōu)點(diǎn)也很明顯,我們只列舉主要幾個(gè)優(yōu)點(diǎn):

  1. 分離接口和實(shí)現(xiàn)部分,一個(gè)實(shí)現(xiàn)未必不變地綁定在一個(gè)接口上,抽象類(函數(shù))的實(shí)現(xiàn)可以在運(yùn)行時(shí)刻進(jìn)行配置,一個(gè)對(duì)象甚至可以在運(yùn)行時(shí)刻改變它的實(shí)現(xiàn),同將抽象和實(shí)現(xiàn)也進(jìn)行了充分的解耦,也有利于分層,從而產(chǎn)生更好的結(jié)構(gòu)化系統(tǒng)。
  2. 提高可擴(kuò)充性
  3. 實(shí)現(xiàn)細(xì)節(jié)對(duì)客戶透明,可以對(duì)客戶隱藏實(shí)現(xiàn)細(xì)節(jié)。

同時(shí)橋接模式也有自己的缺點(diǎn):

大量的類將導(dǎo)致開(kāi)發(fā)成本的增加,同時(shí)在性能方面可能也會(huì)有所減少。


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)