《JavaScript设计模式》——9.12 Decorator(装饰者)模式

9.12 Decorator(装饰者)模式

Decorator是一种结构型设计模式,旨在促进代码复用。与Mixin相类似,它们可以被认为是另一个可行的对象子类化的替代方案。

通常,Decorator提供了将行为动态添加至系统的现有类的能力。其想法是,装饰本身对于类原有的基本功能来说并不是必要的;否则,它就可以被合并到超类本身了。

装饰者可以用于修改现有的系统,希望在系统中为对象添加额外的功能,而不需要大量修改使用它们的底层代码。开发人员使用它们的一个共同原因是,应用程序可能包含需要大量不同类型对象的功能。想象一下,如果必须为一个JavaScript游戏定义数百种不同的对象构造函数会怎么样(见图9-11)。

对象构造函数可以代表不同的玩家类型,每个类型都有不同的功能。魔戒游戏可能需要Hobbit、Elf、Orc、Wizard、Mountain Giant、Stone Giant等构造函数,甚至有可能数以百计。如果我们把功能作为因素计算,可以想象必须为每个能力类型组合创建子类—如:HobbitWithRing、HobbitWithSword、HobbitWithRingAndSword等等。当计算越来越多的不同能力时,这并不是很实用,当然也是不可控的。

Decorator模式并不严重依赖于创建对象的方式,而是关注扩展其额外功能。我们使用了一个单一的基本对象并逐步添加提供额外功能的decorator对象,而不是仅仅依赖于原型继承。这个想法是:向基本对象添加(装饰)属性或方法,而不是进行子类化,因此它较为精简。

在JavaScript中向对象添加新属性是一个非常简单的过程,所以带着这种想法,可以实现一个非常简单的decorator,如下所示(-7和示例9-8):

示例9-7 使用新功能装饰构造函数

// 车辆vehicle构造函数
function vehicle(vehicleType) {
     // 默认值
     this.vehicleType = vehicleType || "car";
     this.model = "default";
     this.license = "00000-000";
}
// 测试基本的vehicle实例
var testInstance = new vehicle("car");
console.log(testInstance);
// 输出:
// vehicle: car, model:default, license: 00000-000
// 创建一个vehicle实例进行装饰
var truck = new vehicle("truck");
// 给truck装饰新的功能
truck.setModel = function (modelName) {
     this.model = modelName;
};
truck.setColor = function (color) {
  this.color = color;
};
// 测试赋值操作是否正常工作
truck.setModel("CAT");
truck.setColor("blue");
console.log(truck);
// 输出:
// vehicle:truck, model:CAT, color: blue
// 下面的代码,展示vehicle依然是不被改变的
var secondInstance = new vehicle("car");
console.log(secondInstance);
// 输出:
// vehicle: car, model:default, license: 00000-000

这种类型的简单实现是可行的,但它并不能真正证明装饰者所提供的所有优势。为此,首先要查阅一下改编的咖啡示例,该示例来自Freeman、Sierra和Bates所著的一本名为《深入浅出设计模式》书籍,它围绕的是模拟购买苹果笔记本。

示例9-8 使用多个Decorator装饰对象

// 被装饰的对象构造函数
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);
// 输出: 1522
console.log(mb.cost());
// 输出: 11.6
console.log(mb.screenSize());

在这个示例中,Decorator重写MacBook()超类对象的.cost()函数来返回MacBook的当前价格加上特定的升级价格。

我们认为装饰作为并没有重写原始Macbook对象的构造函数方法(如screenSize()),为Macbook定义的其他属性也一样,依然保持不变并完好无损。

实际上在前面的示例中没有已定义的接口,从创建者移动到接收者时,我们转移了确保一个对象符合接口要求的职责。

9.12.1 伪经典Decorator(装饰者)
现在,我们要查看Dustin Diaz和Ross Harmes所著的《JavaScript设计模式》(PJDP)一书中提出的装饰者变体。

不像早些时候的一些示例,Diaz和Harmes更关注如何在其他编程语言(如Java或C++)中使用“接口”的概念实现装饰者,我们稍后将对其进行更详细地定义。

这个Decorator模式的特殊变体是用于引用目的。如果你发现它过于复杂,我建议选择前面介绍的较简单实现。
9.12.1.1 接口
PJDP将Decorator模式描述为一种用于在相同接口的其他对象内部透明地包装对象的模式。接口应该是对象定义方法的一种方式,但是,它实际上并不直接指定如何实现这些方法。

接口还可以定义接收哪些参数,但这些都是可选的。

那么,我们为什么要在JavaScript中使用接口呢?其想法是:它们可以自我记录,并能促进可复用性。理论上,通过确保实现类保持和接口相同的改变,接口可以使代码变得更加稳定。

下面是使用鸭子类型在JavaScript中实现接口的一个示例,这种方法帮助确定一个对象是否是基于其实现方法的构造函数/对象的实例。

// 用事先定义好的接口构造函数创建接口,该函数将接口名称和方法名称作为参数
// 在reminder示例中,summary()和placeOrder()描绘的功能,接口应该支持
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";
    }
  }
};
// 创建构造函数实现上述属性和方法
function Todo(config) {
// 为了支持这些功能,接口示例需要检查这些功能
Interface.ensureImplements(config.actions, reminder);
this.name = config.name;
this.methods = config.actions;
}
// 创建Todo构造函数的新实例
var todoItem = Todo(properties);
// 最后测试确保新增加的功能可用
console.log(todoItem.methods.summary());
console.log(todoItem.methods.placeOrder());
// 输出:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store

在这个示例中,Interface.ensureImplements提供了严格的功能检查。

接口的最大问题是,在JavaScript中没有为它们提供内置支持,试图模仿可能不太合适的另外一种语言特性是有风险的。可以在不花费大量性能成本的情况下使用享元接口,但我们将继续看一下使用相同概念的抽象装饰者。

9.12.1.2 抽象Decorator(抽象装饰者)
为了演示该版本Decorator模式的结构,假设我们有一个超类,再次模拟Macbook,以及模拟一个商店允许我们“装饰”苹果笔记本并收取增强功能的额外费用。

增强功能可以包括将内存升级到4GB或8GB、雕刻、Parallels或外壳。如果为每个增强选项组合使用单个子类来模拟它,看起来可能就是这样的:

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]);
// Macbook Pro可能需要如下这样来描述:
var MacbookPro = function () {
     // 实现Macbook
};
MacbookPro.prototype = {
     addEngraving: function () {
     },
     addParallels: function () {
     },
     add4GBRam: function () {
     },
     add8GBRam: function () {
     },
     addCase: function () {
     },
     getPrice: function () {
       // 基本价格
       return 900.00;
     }
};

为了便于我们添加后期需要的更多选项,我们定义了一个具有默认方法的抽象装饰者类来实现Macbook接口,其余的选项则划入子类。抽象装饰者确保我们可以装饰出一个独立的,而且多个装饰者在不同组合下都需要的基类(还记得前面的示例吗?),而不需要为每一个可能的组合都派生子类。

// Macbook装饰者抽象装饰者类
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 () {
          returnthis.macbook.add8GBRam();
     },
     addCase: function () {
          return this.macbook.addCase();
     },
     getPrice: function () {
          return this.macbook.getPrice();
     }
};

上面的示例演示的是:Macbook decorator接受一个对象作为组件。它使用了我们前面定义的Macbook接口,针对每个方法,在组件上会调用相同的方法。我们现在可以仅通过使用MacbookDecorator创建选项类;简单调用超类构造函数,必要时可以重写任何方法。

var CaseDecorator = function (macbook) {
    // 接下来调用超类的构造函数
    this.superclass.constructor(macbook);
};
// 扩展超类
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

var myMacbookPro = new MacbookPro();

// 输出: 900.00

console.log(myMacbookPro.getPrice());

// 装饰macbook

myMacbookPro = new CaseDecorator(myMacbookPro);

// 返回的将是945.00

console.log(myMacbookPro.getPrice());

由于装饰者可以动态地修改对象,因此它们是一种用于改变现有系统的完美模式。有时候,为对象创建装饰者比维护每个对象类型的单个子类要简单一些。可以让可能需要大量子类对象的应用程序的维护变得更加简单。
**
9.12.2 使用jQuery的装饰者**
与我们已经涉及的其他模式一样,也有一些使用jQuery实现的装饰者模式的示例。jQuery.extend()允许我们在运行时或者在随后一个点上动态地将两个或两个以上的对象(和它们的属性)一起扩展(或合并)为一个单一对象。

在这种情况下,一个目标对象可以用新功能来装饰,而不会在源/超类对象中破坏或重写现有的方法(虽然这是可以做到的)。

在接下来的示例中定义三个对象:defaults、options和settings。该任务的目的是为了装饰defaults对象,将options中的额外功能附加到defaults上。我们必须首先使defaults保持未接触状态,并且保持稍后可以访问其属性或函数的能力;然后,给defaults赋予使用装饰属性和函数的能力,这些装饰属性和函数是从options里获取的:

var decoratorApp = decoratorApp || {};
// 定义要使用的对象
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(", ") + " }";
 }
};

// 合并defaults和options,没有显式修改defaults

decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp. options);

// 这里所做的就是装饰可以访问defaults属性和功能的方式(options也一样),defaults本身未作改变

$("#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!"); } }
// options -- { validate: true, name: bar,
 helloWorld: function (){ console.log("hello!"); } }
// defaults -- { validate: false, limit: 5, name: foo,
 welcome: function (){ console.log("welcome!"); } }

9.12.3 优点和缺点
开发人员喜欢使用这种模式,因为它使用时可以是透明的,并且也是相当灵活的:正如我们所看到的,对象可以被新行为包装或“装饰”,然后可以继续被使用,而不必担心被修改的基本对象。在一个更广泛的上下文中,这种模式也使我们不必依靠大量的子类来获得同样的好处。

但是在实现该模式时,也有一些缺陷是我们应该要注意的。如果管理不当,它会极大地复杂化应用程序架构,因为它向我们的命名空间引入了很多小型但类似的对象。让人担心的是,除了对象变得难以管理,其他不熟悉这个模式的开发人员可能难以理解为什么使用它。

大量的评论或模式研究应该有助于解决后者的问题,但是,只要我们继续把握住在应用程序中使用装饰者的广度,在这两方面就应该可以做得很好。

时间: 2024-09-08 08:25:55

《JavaScript设计模式》——9.12 Decorator(装饰者)模式的相关文章

举例讲解Java设计模式编程中Decorator装饰者模式的运用_java

概念 装饰者模式动态地将责任附加到对象上.若要扩展功能,装饰者提供了比继承更有弹性的替代方案. 装饰者和被装饰对象有相同的超类型. 你可以用一个或多个装饰者包装一个对象. 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合 ,可以用装饰过的对象代替它. 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的. 对象可以在任何时候被装饰,所以可以在运行时动态地.不限量地用你喜欢的装饰者来装饰 对象. 在Java中,io包下的很多类就是典型的装饰

设计模式(八)装饰器模式Decorator(结构型)

1. 概述        若你从事过面向对象开发,实现给一个类或对象增加行为,使用继承机制,这是所有面向对象语言的一个基本特性.如果已经存在的一个类缺少某些方法,或者须要给方法添加更多的功能(魅力),你也许会仅仅继承这个类来产生一个新类-这建立在额外的代码上.       通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法.但是这种方法是静态的,用户不能控制增加行为的方式和时机.如果  你希望改变一个已经初始化的对象的行为,你怎么办?或者,你希望继承许多类的行为,改怎么办?前一个

ES7 decorator 装饰者模式

1.装饰模式 设计模式大家都有了解,网上有很多系列教程,比如 JS设计模式等等. 这里只分享 装饰者模式 以及在 如何使用 ES7 的 decorator 概念 1.1.装饰模式 v.s. 适配器模式 装饰模式和适配器模式都是"包装模式"(Wrapper Pattern),它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别. 适配器模式我们使用的场景比较多,比如连接不同数据库的情况,你需要包装现有的模块接口,从而使之适配数据库 -- 好比你手机使用转接口来适配插座那样:

传统设计模式(三)装饰者模式

说到这个模式的项目实例 虫子也满头疼的 所谓装饰者模式说白了动态将职责附加到对象上.如果你在项目某个场景中需要功能扩展根据基类衍生出非常多的子类,那么装饰者模式无疑是很好的.不过其实在实际的项目中,往往大家不直接衍生子类,而是通过组合的方式,根据逻辑讲各种扩展叠加来,对外公布的只是一个标签一个壳而已. 所以这个章节,虫子就虚构一个实例了.还拿电商来说.点券赠品系统. 背景: 1.所有点券.优惠券.赠品券.积分继承同一个基类 基类券 2.不用种类的券可以混合搭配 3.积分根据不同的场景可以配置不同

总结JavaScript设计模式编程中的享元模式使用_基础知识

享元模式不同于一般的设计模式,它主要用来优化程序的性能,它最适合解决大量类似的对象而产生的性能问题.享元模式通过分析应用程序的对象,将其解析为内在数据和外在数据,减少对象的数量,从而提高应用程序的性能. 基本知识 享元模式通过共享大量的细粒度的对象,减少对象的数量,从而减少对象的内存,提高应用程序的性能.其基本思想就是分解现有类似对象的组成,将其展开为可以共享的内在数据和不可共享的外在数据,我们称内在数据的对象为享元对象.通常还需要一个工厂类来维护内在数据. 在JS中,享元模式主要有下面几个角色

.NET设计模式(12):外观模式

概述 在软件开发系统中,客户程序经常会与复杂系统的内部子系统之间产生耦合,而导致客户程序随着子系统的变化而变化.那么如何简化客户程序与子系统之间的交互接口?如何将复杂系统的内部子系统与客户程序之间的依赖解耦?这就是要说的Faç ;ade 模式. 意图 为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用.[GOF <设计模式>] 示意图 门面模式没有一个一般化的类图描述,下面是一个示意性的对象图: 图1 Faç ;ade模式示意性对象图

(Head First 设计模式)学习笔记(3) --装饰者模式(StarBuzz咖啡店实例)

以下内容转载请注明来自"菩提树下的杨过(http://blog.sqlsky.com)" 应用概述: StarBuzz咖啡店有很多饮料,每种饮料都可以根据客户需要加一些调料,比如深培咖啡可以加摩卡(或双倍摩卡),而且某些饮料可以分为大中小杯,根据容量不同,售价不同,而且调料的价格根据饮料的容量不同而不同(比如大杯咖啡加糖要1元,中杯咖啡加糖要0.9元等) 又一设计原则: 对扩展开放,对修改关闭(本例中各种饮料都有共同的大中小杯特性--这是关闭的部分,另外各种具体子类饮料和调料的描述和价

Javascript设计模式理论与实战:桥接模式

桥接模式将抽象部分与实现部分分离开来,使两者都可以独立的变化,并且可以一起和谐地工作.抽象部分和实现部分都可以独立的变化而不会互相影响,降低了代码的耦合性,提高了代码的扩展性. 基本理论 桥接模式定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化. 桥接模式主要有4个角色组成: (1)抽象类 (2)扩充抽象类 (3)实现类接口 (4)具体实现类 根据javascript语言的特点,我们将其简化成2个角色: (1)扩充抽象类 (2)具体实现类 怎么去理解桥接模式呢?我们接下来举例说明 桥接

PHP设计模式装饰器模式实例

我们在使用面向对象的日常开发过程中,或许会碰见需要对某个方法或者某个对象,添加新的行为.然而常见的做法是,写一个子类继承需要改写的类,然后去重新实现类的方法. 但是装饰器模式(Decorator),可以动态地添加修改类的功能,在使用装饰器模式,仅需在运行时添加一个装饰器对象既可实现,相对与生成子类更加的灵活. 在我们需要改写一个类的时候通常的做法是采用继承的方式来重新方法,如下代码 /*  * 比如我们需要改写一串字符串的样式,采用继承的写法.  */ class Canvas {     fu

《JavaScript设计模式》——导读

前言 JavaScript设计模式 设计模式是解决软件设计中常见问题的可复用方案.探索任何编程语言时,设计模式都是一个令人兴奋和极具吸引力的话题. 原因之一是:设计模式是许多先前开发人员总结出的经验,我们可以借鉴这些经验进行编程,以确保能够以优化的方式组织代码,为我们解决棘手的问题提供参考. 设计模式还是我们用来描述解决方案的常用词汇.当我们想要向其他人表述一种以代码形式构建解决方案的方式时,描述设计模式比描述语法和语义要简单得多. 在本书中,我们将探讨JavaScript编程语言中经典的与现代