装饰器模式

装饰器模式

装饰器模式是一种旨在提升代码复用率的结构性模式。有点类似于混入模式,它被认为是一种可以替代子类的可行方案。

一般来说,装饰器提供一种动态的为系统中的类添加行为的能力。装饰器本身没必要有类的基本功能,否则它自己就可以作为父类了。

他们通常可以被用来修改现存的系统,为一些对象加入一些额外的功能(特性),而不用大量修改底层代码。开发人员使用他们的一个常见原因可能包含特征——需要大量不同类型的对象。想象一下要定义几百种不同的对象构造函数,比如一个js游戏。

对象构造函数可能表示不同类型的角色类型,每个角色都有不同的能力。魔戒这款游戏可能需要霍比特人,精灵,兽人,精灵,山岭巨人,石巨人等等,这些很容易有成百上千个。如果我们再考虑能力,想象得为每种能力的结合创建一个子类,例如带指环的霍比特人,带剑的霍比特人,带指环和剑的霍比特人等等。当我们考虑伴随着能力类型数量的增长,这是非常不实用且不可想象的。

装饰器模式没有过分的关注如何对象如何被创建,而是如何扩展他们的功能。相比较原型继承,我们用一个单一的基本对象,并逐步第为它增加提供额外能力的对象的方式工作。这个想法相比较子类,通过增加一些属性或者方法到基本对象上,可以更加精简。

为js对像添加新属性是一个非常直接的过程,考虑着一点,一个非常简单的装饰器可能实现如下:

案例1:具有新功能的装饰构造器

// A vehicle constructor
function Vehicle( vehicleType ){
 
    // some sane defaults
    this.vehicleType = vehicleType || "car";
    this.model = "default";
    this.license = "00000-000";
 
}
 
// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );
 
// Outputs:
// vehicle: car, model:default, license: 00000-000
 
// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );
 
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
    this.model = modelName;
};
 
truck.setColor = function( color ){
    this.color = color;
};
 
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
 
console.log( truck );
 
// Outputs:
// vehicle:truck, model:CAT, color: blue
 
// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );
 
// Outputs:
// vehicle: car, model:default, license: 00000-000

这种最简单的实现是有多种用途的,但是并没有真正的展示装饰器提供的全部力量。对于这一点,我们将首先完成一个优秀的案例。

案例2:用多个装饰器装饰一个对象

// The constructor to decorate
function MacBook() {
 
  this.cost = function () { return 997; };
  this.screenSize = function () { return 11.6; };
 
}
 
// Decorator 1
function memory( macbook ) {
 
  var v = macbook.cost();
  macbook.cost = function() {
    return v + 75;
  };
 
}
 
// Decorator 2
function engraving( macbook ){
 
  var v = macbook.cost();
  macbook.cost = function(){
    return v + 200;
  };
 
}
 
// Decorator 3
function insurance( macbook ){
 
  var v = macbook.cost();
  macbook.cost = function(){
     return v + 250;
  };
 
}
 
var mb = new MacBook();
memory( mb );
engraving( mb );
insurance( mb );
 
// Outputs: 1522
console.log( mb.cost() );
 
// Outputs: 11.6
console.log( mb.screenSize() );

In the above example, our Decorators are overriding the MacBook() super-class objects .cost() function to return the current price of the Macbook plus the cost of the upgrade being specified.

在上面的例子中,我们就要重写了macbook()父类对象的cost()函数以返回的MacBook升级特定部分后加总的目前价格。

装饰器只修改原对象的部分。

伪-经典 装饰器

这种装饰器模式的特别变种提供参考用途,如果发现它太过复杂,我建议选择一个之前提到过的简单实现。

接口

PJDP 描述装饰器作为一种模式被用来,将对象透明的包装在其他具有相同接口中的对象中。接口是一种一个对象的方法应该做什么的定义方式。然而,他并不指明如何实现这些方法。

他们可以指明方法所使用的参数,但这被认为是可选的。

那么,我们为什么要在js中使用接口呢?我们的像是,接口自身就是种记录,并且可以提高复用性。理论上说,接口也使得代码更稳定,通过确保改变他们也必须改变实现他们的对象。

以下是一种通过js鸭式编程(一种方法,帮助决定一个对象是否是一个构造函数/基于构造函数生成对象的实例,的实现)方式实现的接口的例子。

// Create interfaces using a pre-defined Interface
// constructor that accepts an interface name and
// skeleton methods to expose.
 
// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );
 
var properties = {
  name: "Remember to buy the milk",
  date: "05/06/2016",
  actions:{
    summary: function (){
      return "Remember to buy the milk, we are almost out!";
   },
    placeOrder: function (){
      return "Ordering milk from your local grocery store";
    }
  }
};
 
// Now create a constructor implementing the above properties
// and methods
 
function Todo( config ){
 
  // State the methods we expect to be supported
  // as well as the Interface instance being checked
  // against
 
  Interface.ensureImplements( config.actions, reminder );
 
  this.name = config.name;
  this.methods = config.actions;
 
}
 
// Create a new instance of our Todo constructor
 
var todoItem = new Todo( properties );
 
// Finally test to make sure these function correctly
 
console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );
 
// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store

上面的代码中,Interface.ensureImplements确保严格的功能检查,关于Interface实现可以看这里:[URL:https://gist.github.com/1057989]

使用接口最大的问题是,js并没提供内置的接口支持,当我们试图模拟其他语言的特性的有可能是不合适的。清理的接口可能不会造成太大的性能开销,我们接下来将看一下使用相同概念的抽象装饰器模式。

抽象装饰器

为了展示这个版本的的装饰模式,我们准备再次想象有个父类模型Macbook,和一个允许我们付费升级我们Macbook的商店。

升级可以包括,4GB Ram到8GB Ram,雕刻或者其他。如果我们的模型使用独立的子类组合各种可能的升级选项,那么将看起来像这样:

var Macbook = function(){
        //...
};
 
var  MacbookWith4GBRam = function(){},
     MacbookWith8GBRam = function(){},
     MacbookWith4GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndParallels = function(){},
     MacbookWith4GBRamAndParallels = function(){},
     MacbookWith8GBRamAndParallelsAndCase = function(){},
     MacbookWith4GBRamAndParallelsAndCase = function(){},
     MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
     MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};

这是一个不切实际的解决方案,作为一个新的子类将需要每一个可能的组合。因为我们喜欢将事情简单化而不是维护一堆的子类,让我们看看装饰器模式如何更好的帮我们解决这个问题。

相比要求我们早期看到的全部组合,我们只需要创建五个装饰器类。在这些加强类上调用的方法将被传递给我们的Macbook类。

在下一个例子中,装饰器透明的包装他们的组件,并且有趣的是可以使用相同的接口互换他们。

这是我们将定义的Macbook接口:

var Macbook = new Interface( "Macbook",
  ["addEngraving",
  "addParallels",
  "add4GBRam",
  "add8GBRam",
  "addCase"]);
 
// A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
    // implements Macbook
};
 
MacbookPro.prototype = {
    addEngraving: function(){
    },
    addParallels: function(){
    },
    add4GBRam: function(){
    },
    add8GBRam:function(){
    },
    addCase: function(){
    },
    getPrice: function(){
      // Base price
      return 900.00;
    }
};

为了使以后我们更容易的添加更多的选项,一个抽象抽象的装饰器被定义,并且需要默认实现Macbook类所定义的接口,剩余的选项讲需要子类。抽象装饰器确保我们可以装饰一个基本类独立于为了不同的组合而存在许多装饰器,不需要为每一可能的组合派生出一个类。

// Macbook decorator abstract decorator class
 
var MacbookDecorator = function( macbook ){
 
    Interface.ensureImplements( macbook, Macbook );
    this.macbook = macbook;
 
};
 
MacbookDecorator.prototype = {
    addEngraving: function(){
        return this.macbook.addEngraving();
    },
    addParallels: function(){
        return this.macbook.addParallels();
    },
    add4GBRam: function(){
        return this.macbook.add4GBRam();
    },
    add8GBRam:function(){
        return this.macbook.add8GBRam();
    },
    addCase: function(){
        return this.macbook.addCase();
    },
    getPrice: function(){
        return this.macbook.getPrice();
    }
};

上面的示例中Macbook装饰器接受一个对象作为我们的基本组件,它使用我们之前定义的Macbook接口并且每个方法只需要调用组件上相同的方法。我们现在可以创建我们的选项类,用来装饰Macbook。


// First, define a way to extend an object a
// with the properties in object b. We'll use
// this shortly!
function extend( a, b ){
    for( var key in b )
        if( b.hasOwnProperty(key) )
            a[key] = b[key];
    return a;
}
 
var CaseDecorator = function( macbook ){
   this.macbook = macbook;
};
 
// Let's now extend (decorate) the CaseDecorator
// with a MacbookDecorator
extend( CaseDecorator, MacbookDecorator );
 
CaseDecorator.prototype.addCase = function(){
    return this.macbook.addCase() + "Adding case to macbook";
};
 
CaseDecorator.prototype.getPrice = function(){
    return this.macbook.getPrice() + 45.00;
};

我们在这里重写了需要被装饰的addCase()和getPrice()方法,并且我们实现它是通过首先在原始的Macbook对象中调用它。并简单的加一个字符串或者一个数字值。

到目前为止有大量的信息被展现在本章节。让我们试着用一个简单的案例总结在一起,这样有望于突出我们所学。

// Instantiation of the macbook
var myMacbookPro = new MacbookPro();
 
// Outputs: 900.00
console.log( myMacbookPro.getPrice() );
 
// Decorate the macbook
var decoratedMacbookPro = new CaseDecorator( myMacbookPro );
 
// This will return 945.00
console.log( decoratedMacbookPro.getPrice() );

作为装饰器可以动态的修改对象,这对于修改现存系统是一个非常棒的模式。有时,他只是简单的额创建一个相关的对象,以对抗为每个对象类型维护一堆子类。这使得需要维护一大堆子类对象的应用变得更直接。

本例的功能版本可以在JSBin上找到。

jQuery中的装饰器

正如我们上面所提到的其他模式一样,这里也有一个装饰模式的例子可以通过jQuery实现。jQuery.extend()允许我们在程序运行中扩展(或者合并)两个或者更多个对象到一个对象中。

在这种情况下,一个目标对象可以从原对象/父类,被装饰新的功能而不用打破或者重写现有的方法。

在下面的案例中,我们定义了三个对象:defaults,options 和settings。我们的目标是用在options,settings中的额外功能去装饰默认的对象。我们必须:

a)保留“default”的状态在一种未接触的状态下,即我们不会在之后的时间点失去访问它属性和方法的权限。
b)获得options中用来装饰属性和功能的能力。

var decoratorApp = decoratorApp || {};
 
// define the objects we're going to use
decoratorApp = {
 
    defaults: {
        validate: false,
        limit: 5,
        name: "foo",
        welcome: function () {
            console.log( "welcome!" );
        }
    },
 
    options: {
        validate: true,
        name: "bar",
        helloWorld: function () {
            console.log( "hello world" );
        }
    },
 
    settings: {},
 
    printObj: function ( obj ) {
        var arr = [],
            next;
        $.each( obj, function ( key, val ) {
            next = key + ": ";
            next += $.isPlainObject(val) ? printObj( val ) : val;
            arr.push( next );
        } );
 
        return "{ " + arr.join(", ") + " }";
    }
 
};
 
// merge defaults and options, without modifying defaults explicitly
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);
 
// what we have done here is decorated defaults in a way that provides
// access to the properties and functionality it has to offer (as well as
// that of the decorator "options"). defaults itself is left unchanged
 
$("#log")
    .append( decoratorApp.printObj(decoratorApp.settings) +
          + decoratorApp.printObj(decoratorApp.options) +
          + decoratorApp.printObj(decoratorApp.defaults));
 
// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log( "hello world" ); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }
 

优点和缺点

开发人员喜欢使用这种模式,因为它可以被透明的使用。并且也十分自由,正如我们看到的那样,对象可以被包裹或者装饰而含有新的行为,继续被使用并且不用担心原来对象被修改。在更管饭的上下文中,这种模式也可以避免我们需要依赖一大子类而获得同样的效用。

当然实现这种模式的时候我们也应该意识到一些缺点。如果管理不善,它可以显著的复杂话我们的应用架构,因为它给我们的命名空间进入了许多小但是相似的对象。除了变得难以管理,其他不熟悉这种模式的开发者可能很难了解为什么这种模式被使用。

充足的交流和模式的研究可以对第二个问题有帮助,然而,我们应该控制我们的应用程序中装饰器的广泛成都,并且统计他们的全部数量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 定义 装饰器模式又名包装(Wrapper)模式。装饰器模式以对客户端透明的方式拓展对象的功能,是继承关系的一种替代...
    步积阅读 35,095评论 0 38
  • 装饰器模式可以在不修改代码的情况下灵活的为一对象添加行为和职责。当你要修改一个被其它类包含的类的行为时,它可以代替...
    泥孩儿0107阅读 280评论 0 0
  • 前言 距离上一篇,间隔时间有点长哈(尴尬 ==!)经历过漫长的实习期,试用期,第一份工作终于慢慢走上正轨,中间发生...
    暗影飞客阅读 1,171评论 0 0
  • 概述 装饰器模式一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某...
    ymjkMaster阅读 781评论 0 0
  • 1 策略模式:定义一系列算法的方法,所有的算法功能相同但是实现不同。 示例类图: 如上类图所示:鸭子有两个可能...
    Richard_80ec阅读 3,281评论 0 2