深入理解JavaScript系列(44):设计模式之桥接模式详解


Posted in Javascript onMarch 04, 2015

介绍

桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。

正文

桥接模式最常用在事件监控上,先看一段代码:

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);

});

}

上述代码,有个问题就是getBeerById必须要有浏览器的上下文才能使用,因为其内部使用了this.id这个属性,如果没用上下文,那就歇菜了。所以说一般稍微有经验的程序员都会将程序改造成如下形式:
function getBeerById(id, callback) {

// 通过ID发送请求,然后返回数据

asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {

// callback response

callback(resp.responseText);

});

}

实用多了,对吧?首先ID可以随意传入,而且还提供了一个callback函数用于自定义处理函数。但是这个和桥接有什么关系呢?这就是下段代码所要体现的了:
addEvent(element, 'click', getBeerByIdBridge);
function getBeerByIdBridge (e) {


getBeerById(this.id, function(beer) {



console.log('Requested Beer: '+beer);

});

}

这里的getBeerByIdBridge就是我们定义的桥,用于将抽象的click事件和getBeerById连接起来,同时将事件源的ID,以及自定义的call函数(console.log输出)作为参数传入到getBeerById函数里。

这个例子看起来有些简单,我们再来一个复杂点的实战例子。

实战XHR连接队列

我们要构建一个队列,队列里存放了很多ajax请求,使用队列(queue)主要是因为要确保先加入的请求先被处理。任何时候,我们可以暂停请求、删除请求、重试请求以及支持对各个请求的订阅事件。

基础核心函数
在正式开始之前,我们先定义一下核心的几个封装函数,首先第一个是异步请求的函数封装:

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;

    };

})();

上述封装的自执行函数是一个通用的Ajax请求函数,相信属性Ajax的人都能看懂了。

接下来我们定义一个通用的添加方法(函数)的方法:

Function.prototype.method = function (name, fn) {

    this.prototype[name] = fn;

    return this;

};

最后再添加关于数组的2个方法,一个用于遍历,一个用于筛选:
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;

    });

}

因为有的新型浏览器已经支持了这两种功能(或者有些类库已经支持了),所以要先判断,如果已经支持的话,就不再处理了。

观察者系统
观察者在队列里的事件过程中扮演着重要的角色,可以队列处理时(成功、失败、挂起)订阅事件:

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);

            }

            );

    }

};

队列主要实现代码
首先订阅了队列的主要属性和事件委托:

DED.Queue = function () {

    // 包含请求的队列.

 this.queue = [];

    // 使用Observable对象在3个不同的状态上,以便可以随时订阅事件

 this.onComplete = new DED.util.Observer;

    this.onFailure = new DED.util.Observer;

    this.onFlush = new DED.util.Observer;
    // 核心属性,可以在外部调用的时候进行设置

 this.retryCount = 3;

    this.currentRetry = 0;

    this.paused = false;

    this.timeout = 5000;

    this.conn = {};

    this.timer = {};

};

然后通过DED.Queue.method的链式调用,则队列上添加了很多可用的方法:

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 = [];

    });

代码看起来很多,折叠以后就可以发现,其实就是在队列上定义了flush, setRetryCount, setTimeout, add, pause, dequeue, 和clear方法。

简单调用

var q = new DED.Queue;

// 设置重试次数高一点,以便应付慢的连接

q.setRetryCount(5);

// 设置timeout时间

q.setTimeout(1000);

// 添加2个请求.

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队列

q.flush();

// 暂停队列,剩余的保存

q.pause();

// 清空.

q.clear();

// 添加2个请求.

q.add({

    method: 'GET',

    uri: '/path/to/file.php?ajax=true'

});
q.add({

    method: 'GET',

    uri: '/path/to/file.php?ajax=true&woe=me'

});
// 从队列里删除最后一个请求.

q.dequeue();

// 再次Flush

q.flush();

桥接呢?

上面的调用代码里并没有桥接,那桥呢?看一下下面的完整示例,就可以发现处处都有桥哦:

<!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 () {

            // 实现.

var q = new DED.Queue;

            q.setRetryCount(5);

            q.setTimeout(3000);

            var items = $('items');

            var results = $('results');

            var queue = $('queue-items');

            // 在客户端保存跟踪自己的请求

var requests = [];

            // 每个请求flush以后,订阅特殊的处理步骤

            q.onFlush.subscribe(function (data) {

                results.innerHTML = data;

                requests.shift();

                queue.innerHTML = requests.toString();

            });

            // 订阅时间处理步骤

            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>

            异步联接请求</h1>

        <div id="queue-items">

        </div>

        <div id="add-stuff">

            <h2>向队列里添加新请求</h2>

            <ul id="adders">

                <li><a href="#" id="action-01">添加 "01" 到队列</a></li>

                <li><a href="#" id="action-02">添加 "02" 到队列</a></li>

                <li><a href="#" id="action-03">添加 "03" 到队列</a></li>

            </ul>

        </div>

        <h2>队列控制</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>

                结果:

            </h2>

            <div id="results">

            </div>

        </div>

    </div>

</body>

</html>

在这个示例里,你可以做flush队列,暂停队列,删除队列里的请求,清空队列等各种动作,同时相信大家也体会到了桥接的威力了。

总结

桥接模式的优点也很明显,我们只列举主要几个优点:

1.分离接口和实现部分,一个实现未必不变地绑定在一个接口上,抽象类(函数)的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现,同将抽象和实现也进行了充分的解耦,也有利于分层,从而产生更好的结构化系统。
2.提高可扩充性
3.实现细节对客户透明,可以对客户隐藏实现细节。

同时桥接模式也有自己的缺点:

大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。

Javascript 相关文章推荐
javascript中call,apply,bind的用法对比分析
Feb 12 Javascript
jquery中live()方法和bind()方法区别分析
Jun 23 Javascript
微信小程序开发之从相册获取图片 使用相机拍照 本地图片上传
Apr 18 Javascript
jQuery Easyui Treegrid实现显示checkbox功能
Aug 08 jQuery
详谈DOM简介及节点、属性、查找节点的方法
Nov 16 Javascript
JS实现获取word文档内容并输出显示到html页面示例
Jun 23 Javascript
利用原生的JavaScript实现简单拼图游戏
Nov 18 Javascript
浅谈Vue.js中如何实现自定义下拉菜单指令
Jan 06 Javascript
Vue-CLI 3.X 部署项目至生产服务器的方法
Mar 22 Javascript
webpack 如何解析代码模块路径的实现
Sep 04 Javascript
Jquery高级应用Deferred对象原理及使用实例
May 28 jQuery
微信小程序学习总结(三)条件、模板、文件引用实例分析
Jun 04 Javascript
JS实现FLASH幻灯片图片切换效果的方法
Mar 04 #Javascript
javascript下拉框选项单击事件的例子分享
Mar 04 #Javascript
js实现仿QQ秀换装效果的方法
Mar 04 #Javascript
深入理解JavaScript系列(43):设计模式之状态模式详解
Mar 04 #Javascript
深入理解JavaScript系列(42):设计模式之原型模式详解
Mar 04 #Javascript
javascript 动态创建表格的2种方法总结
Mar 04 #Javascript
深入理解JavaScript系列(41):设计模式之模板方法详解
Mar 04 #Javascript
You might like
ThinkPHP关于session的操作方法汇总
2014/07/18 PHP
给WordPress的编辑后台添加提示框的代码实例分享
2015/12/25 PHP
PHP的mysqli_sqlstate()函数讲解
2019/01/23 PHP
PHP基于phpqrcode类生成二维码的方法示例详解
2020/08/07 PHP
使用js获取QueryString的方法小结
2010/02/28 Javascript
Document对象内容集合(比较全)
2010/09/06 Javascript
基于mootools插件实现遮罩层新手引导
2012/05/24 Javascript
jquery仿京东导航/仿淘宝商城左侧分类导航下拉菜单效果
2013/04/24 Javascript
使用闭包对setTimeout进行简单封装避免出错
2013/07/10 Javascript
JS动态修改图片的URL(src)的方法
2015/04/01 Javascript
javascript实现的网站访问量统计代码
2015/12/20 Javascript
Bootstrap编写一个在当前网页弹出可关闭的对话框 非弹窗
2016/06/30 Javascript
AngularJS基础 ng-model-options 指令简单示例
2016/08/02 Javascript
浅谈JS中的bind方法与函数柯里化
2016/08/10 Javascript
JS制作图形验证码实现代码
2020/10/19 Javascript
Angular实现的日程表功能【可添加及隐藏显示内容】
2017/12/27 Javascript
easyUI使用分页过滤器对数据进行分页操作实例分析
2020/06/01 Javascript
基于postman获取动态数据过程详解
2020/09/08 Javascript
如何基于viewport vm适配移动端页面
2020/11/13 Javascript
Python实现Windows和Linux之间互相传输文件(文件夹)的方法
2017/05/08 Python
R vs. Python 数据分析中谁与争锋?
2017/10/18 Python
python针对excel的操作技巧
2018/03/13 Python
python实现在IDLE中输入多行的方法
2018/04/19 Python
Win10系统下安装labelme及json文件批量转化方法
2019/07/30 Python
Python用input输入列表的实例代码
2020/02/07 Python
Windows下Anaconda安装、换源与更新的方法
2020/04/17 Python
Python绘图之柱形图绘制详解
2020/07/28 Python
俄罗斯韩国化妆品网上商店:Cosmasi.ru
2019/10/31 全球购物
写演讲稿所需要注意的4个条件
2014/01/09 职场文书
刚毕业大学生自荐信范文
2014/02/20 职场文书
2014年“世界无车日”活动方案
2014/09/21 职场文书
员工辞职信范文
2015/03/02 职场文书
2014年个人年终总结
2015/03/09 职场文书
入党函调证明材料
2015/06/19 职场文书
PHP获取学生成绩的方法
2021/11/17 PHP
SpringBoot使用ip2region获取地理位置信息的方法
2022/06/21 Java/Android