桥接模式

在上一篇博客中我讨论了单例模式以及如何在JavaScript中使用它。这次我们围绕桥接模式(the Bridge Pattern)展开讨论,为了强调它的重要性我们把它安排在JavaScript设计模式系列的第二篇。

我读过的所有关于桥接模式的解释几乎都是对GoF(the Gang of Four)的直接引用,所以我在想要不我也这么写吧?桥接模式应该是“把抽象概念和具体实现分离开来,让这两部分可以完全独立地变化”。桥接模式在像JavaScript这种事件驱动的应用程序中相当有用。而事实上它是一个完全未被充分利用的设计模式。

事件监听器示例

下面的例子里我会用一些jQuery,所以如果你不知道某个函数的功能或用法,你可以查看jQuery的文档。

下面你将看到的这段代码是使用一个名叫getXById的API方法,当然这个方法的实现很糟糕。我们用一个click事件来确认是哪个元素的id会被发送。getXById自己会获取click元素的id,然后用这个获取到的id发起Ajax向服务端请求X。

getXById = function() {
    var id = this.id;

    $.ajax({
        url:'/getx?id=' + id,
        success: function(response) {
            console.log(response);
        }
    });
}

$('someElement').bind('click', getXById);

这段代码如果仅仅只是用在某个特定页面的特定用途倒也没什么,然而它是(据说是)一个API的一部分,所以需要做个大修改。我们要把getXById从事件监听器和处理Ajax结果中剥离出来:

getXById = function(id, callback) {
    $.ajax({
        url:'/getx?id=' + id,
        success: callback
    }
}

getXBridge = function() {
    var id = this.id;
    getXById(this.id, function() {
        console.log(response);
    });
}

$('someElement').bind('click', getXBridge);

现在你可以在任何地方使用getXById并且你可以用返回的X做任何事情。

经典示例

我所说的“经典”有双重含义:这个例子是比较常见的面向对象编程语言的案例,并且它使用了类(译注:即classical里的class)。

JavaScript本身并没有类的概念,但是你可以模拟接口,并使用原型来模拟类。这个例子最早见于《Head First Design Pattern》,那是一个Java版本。实际上它是个无关紧的模式,因为在书的后面都没有提供它的代码实例,所以我只能使用几张示意图(此外我还重画了这些图,就是因为我太聪明了)。

我们的首发产品

首先我们从RemoteControl接口开始。ToshibaRemoteSonyRemote都实现了这个接口并且工作于各自的电视。通过这些代码你可以在任意遥控器上调用on()off()或者是setChannel(),尽管所有的电视都不一样,但是它们都能正常工作。当你想用桥接模式对控制器做些改进时会发生些什么呢:

现在这些电视继承了一个接口,其他遥控器继承了另一个接口(事实上遥控器只有一个类,因为它只需要一个实现),我们可以通过继承来创建变量,并且保持兼容性。是不是想看看代码是怎么写的?我会给你看一个新版本的桥接模式的代码 ,因为我觉得你没有必要看最原始那个版本。其实我认为你根本不需要看任何代码,但是我敢肯定有些人无论如何就是想看代码。谁让我们是程序员呢?那就给我们看代码!

var RemoteControl = function(tv) {
    this.tv = tv;

    this.on = function() {
        this.tv.on();
    };

    this.off = function() {
        this.tv.off();
    };

    this.setChannel = function(ch) {
        this.tv.tuneChannel(ch);
    };
};

/* 更新,更好的遥控器 */
var PowerRemote = function(tv) {
    this.tv = tv;
    this.currChannel = 0;

    this.setChannel = function(ch) {
        this.currChannel = ch;
        this.tv.tuneChannel(ch);
    };

    this.nextChannel = function() {
        this.setChannel(this.currChannel + 1);
    };

    this.prevChannel = function() {
        this.setChannel(this.currChannel - 1);
    };
};
PowerRemote.prototype = new RemoteControl();


/** TV 接口
    因为JavaScript里没有接口的概念,
    所以我只是使用一些注释来定义应该实现些什么

    function on
    function off
    function tuneChannel(channel)
*/

/* Sony TV */
var SonyTV = function() {
    this.on = function() {
        console.log('Sony TV is on');
    };

    this.off = function() {
        console.log('Sony TV is off');
    };

    this.tuneChannel = function(ch) {
        console.log('Sony TV tuned to channel ' + ch);
    };
}

/* Toshiba TV */
var ToshibaTV = function() {
    this.on = function() {
        console.log('Welcome to Toshiba entertainment');
    };

    this.off = function() {
        console.log('Goodbye Toshiba user');
    };

    this.tuneChannel = function(ch) {
        console.log('Channel ' + ch + ' is set on your Toshiba television');
    };
}

/* 让我们看看它是如何操作的 */
var sony = new SonyTV(),
    toshiba = new ToshibaTV(),
    std_remote = new RemoteControl(sony),
    pwr_remote = new PowerRemote(toshiba);

std_remote.on();            // prints "Sony TV is on"
std_remote.setChannel(55);  // prints "Sony TV tuned to channel 55"
std_remote.setChannel(20);  // prints "Sony TV tuned to channel 20"
std_remote.off();           // prints "Sony TV is off"

pwr_remote.on();            // prints "Welcome to Toshiba entertainment"
pwr_remote.setChannel(55);  // prints "Channel 55 is set on your Toshiba television"
pwr_remote.nextChannel();   // prints "Channel 56 is set on your Toshiba television"
pwr_remote.prevChannel();   // prints "Channel 55 is set on your Toshiba television"
pwr_remote.off();           // prints "Goodbye Toshiba user"

上面就是JavaScript里的桥接模式。