装饰者模式_JavaScript设计模式11

一.最简单的装饰者实现

JS在动态扩展方面有着天生的优势,很容易就能实现装饰者:

// 初始类型
function Dog() {
    console.log('I am a dog');
}
// 装饰类型
function CanRun(dog) {
    dog.run = function() {
        console.log('I can run');
    }

    return dog;
}
function CanWalk(dog) {
    dog.walk = function() {
        console.log('I can walk');
    }

    return dog;
}
function CanBark(dog) {
    dog.bark = function() {
        console.log('I can bark');
    }

    return dog;
}
// ...

// test
var dog = new Dog();
CanWalk(CanBark(CanRun(dog)));  // 通过“包裹”扩展dog的功能
dog.run();
dog.bark();
dog.walk();

简单是足够简单了,但存在一些问题:

  1. 好像不需要用装饰者模式吧,直接把功能全放进Dog里不就好了吗?

    从上面的例子看确实是这样的,但如果Dog是我们无法直接修改的第三方组件呢,这时通过装饰者模式来扩展功能就很合适了。从这个角度看,装饰者模式和外观模式很相像,唯一的差别是目的不同,前者是为了扩展新功能,后者追求现有接口易用

  2. 哪些功能应该作为装饰类型存在?

    基础的、必要的功能应该是Dog的组成部分,可选的、额外的、不常用的功能应该有装饰类型提供

  3. 装饰者不小心重写了原有的属性怎么办?

    确实存在属性被重写的风险,因为我们没有做任何类型上的约束,各个装饰者之间也是相对独立的,还有可能覆盖掉其他装饰者添上的属性,我们需要更可靠的(后文介绍的)装饰者实现来避免这些风险

二.伪经典装饰者

JS没有提供Interface支持,我们无法通过接口来约束类型,提高其可靠性,但我们可以自己实现Interface来约束类型,简单的Interface可能是这样的:

function Interface(strName, arrMethodNames) {
    this.name = strName;
    this.strMethods = arrMethodNames;
}
Interface.ensureImplements = function(obj, interface) {
    for(var i = 0; i < interface.strMethods.length; i++) {
        if (!(interface.strMethods[i] in obj)) {
            throw new TypeError('Interface.ensureImplements: no ' + interface.strMethods[i] + '\'s here');
        }
    }
}

利用自定义的Interface来实现类型约束,装饰者模式可以变成这样:

// 作用相当于装饰对象
var spec = {
    attr: 'value',
    actions: {
        fun1: function() {
            console.log('fun1');
        },
        fun2: function() {
            console.log('fun2');
        }
    }
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// 构造函数
function MyObject(spec) {
    // 接口检查
    Interface.ensureImplements(spec.actions, myInterface);

    this.attr = spec.attr;
    this.methods = spec.actions;
}

// test
var obj = new MyObject(spec);
obj.methods.fun1();
obj.methods.fun2();

虽然利用接口实现了类型约束,但结构不够清晰,不便于管理,最易于管理的当然是层级结构,也就是下面抽象装饰者中的继承机制

三.抽象装饰者

把可选功能先定义在抽象装饰者类中,但不提供实现,由具体装饰者子类提供实现,并利用接口实现类型约束,示例代码如下:

// 定义接口
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// 定义基类
function Coffee() {
    console.log('make a cup of coffee');
}
Coffee.prototype = {
    addMilk: function() {},
    addSalt: function() {},
    addSugar: function() {},

    getPrice: function() {
        // 原味价格
        return 30;
    }
}
// 定义抽象装饰者类
function CoffeeDecorator(coffee) {
    Interface.ensureImplements(coffee, iCoffee);
    this.coffee = coffee;
}
CoffeeDecorator.prototype = {
    addMilk: function() {
        return this.coffee.addMilk();
    },
    addSalt: function() {
        return this.coffee.addSalt();
    },
    addSugar: function() {
        return this.coffee.addSugar();
    },

    getPrice: function() {
        return this.coffee.getPrice();
    }
}
// 装饰者子类
function MilkDecorator(coffee) {
    // 调用父类构造函数
    this.superType(coffee);
}
// 定义继承
function extend(subType, superType) {
    var F = function() {};
    F.prototype = superType.prototype;
    subType.prototype = new F();    // 继承原型属性

    subType.prototype.superType = superType;
    console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // 继承
// 重写父类方法(扩展)
MilkDecorator.prototype.addMilk = function() {
    console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
    return this.coffee.getPrice() + 8;
}
// ...定义其它装饰者子类

// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30

coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38

这种实现方式的优点是结构清晰,缺点是复杂度增加了,手动模拟语言本身没有提供的特性可能有潜在的风险,我们模拟了接口和继承机制,可能埋下其它隐患

四.jQuery提供的装饰者机制

嗯,又是$.extend()Mixin模式_JavaScript设计模式10中说$.extend()提供了Mixin模式的实现,这里又说提供了装饰者模式的实现,并不冲突,因为这两种模式的目的都是扩展现有组件的功能,严格地说,jQueryextend更像Mixin模式(合并几个组件产生新组件,如果说这种机制算装饰者模式,也勉强说得过去)

关于jQueryextend的更多信息请查看:jQuery.extend 函数详解

五.装饰者模式的优缺点

优点

  1. 分离了基础功能和可选功能(装饰功能)

  2. 可以动态扩展对象的功能,而不会意外修改基本对象

缺点

  1. 引入了大量小类型,可能会引起命名空间混乱

  2. 管理不当会使应用的结构更复杂,尤其是基于继承机制的实现,深层继承会极大地降低可读性

参考资料

  • 《JavaScript设计模式》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code