您的位置:首页 > Web前端 > JavaScript

JavaScript设计模式(二)适配器、装饰者和工厂模式

2016-07-25 00:28 591 查看


作者 Joseph
Zimmerman

http://www.joezimjs.com


创建日期

15 May 2012

您已经来到 JavaScript 设计模式系列的第 2 部分。 现在离第 1 部分已经有一段时间了,所以您可能希望复习一下单例、组合与外观模式。
这一次,您将学习 Adapter(适配器)、Decorator(装饰者)和 Factory(工厂)模式。


适配器模式 

适配器模式使您可以根据您的需求转换(或调整)一个接口。 创建含有您所需接口的另一个对象,并将它连接到您想改变接口的对象,从而完成这种转换。



图 1. 适配器模式的结构 


为什么需要采用适配器模式?

在开发应用程序时,您往往会需要更换其中某一部分,例如,您用于保存日志或类似性质的内容的一个库。 当您用一个新库来替换它时,新库不太可能有完全相同的接口。 从这里开始,您有两种选择:

检查所有代码,并更改指向旧库的一切代码。

创建一个适配器,使新库可以使用与旧库相同的接口。

显然,在一些情况下,假如您的应用程序很小,或者对旧库的引用很少,更合适的做法是检查完整的代码,并更改它以匹配新库,而不是添加一个新的抽象层,使代码更复杂。 但是,在大多数情况下,创建一个适配器更为实用且节省时间。


适配器示例

何不采用上述假设的日志场景,并将我们的代码用作示例呢? 您在原来的代码中可能使用了内置到大多数浏览器中的控制台来记录日志,但有几个问题: 它并没有内置到所有浏览器(尤其是较旧的浏览器),您也不能看到其他用户使用您的应用程序时所发生的日志,所以您无法看到任何在您自己测试它时没有捕捉到的问题。 所以您决定,您想要一个日志程序,它可以使用 AJAX 将这些日志发送给服务器进行处理。 您的新 
AjaxLogger
库的 API 看起来如下所示:

AjaxLogger.sendLog(arguments);
AjaxLogger.sendInfo(arguments);
AjaxLogger.sendDebug(arguments);
etc...

显然,这个库的作者并没有意识到,您会试图以它来取代控制台,所以他/她觉得有必要在每个方法名称的开头加上 "send"。 现在您会问,“为什么我不能编辑库并更改方法的名称呢?” 有几个充分的理由不这样做。 如果您需要更新该库,您所做的更改会被覆盖,所以您需要回去再次更改它们。 此外,如果您从内容交付网络下载您的库,则您就不能够编辑它们。 因此,让我们建立一个对象,使这个新库适应与控制台相同的接口。

var AjaxLoggerAdapter = {
log: function() {
AjaxLogger.sendLog(arguments);
},
info: function() {
AjaxLogger.sendInfo(arguments);
},
debug: function() {
AjaxLogger.sendDebug(arguments);
},
...
};


如何使用它?

我敢打赌,使用控制台的任何人都直接通过其引用来调用它,那么您如何让每一个对 
console.xxx
 的调用都指向新的适配器,而不是到控制台呢? 如果您曾使用一个抽象(比如工厂)来检索控制台,那么您可以只在该抽象层进行更改,但如前所述: 每个人都只是直接指向
console
。 JavaScript 是一种动态语言,这使我们能够在运行时更改代码,那么为什么不直接用新的 
AjaxLoggerAdapter
 覆盖控制台呢?
window.console = AjaxLoggerAdapter;


那很简单,不是吗? 但是,要小心! 如果您在由其他人使用的代码中这样做,控制台就不会再像用户期望的那样运作。 另外,不要被这个示例的简单性愚弄。 在许多情况下,您都不会有轻松进行彼此映射的方法(比如将 
sendLog
 映射到 
log
)。 您可能必须真正实施一点自己的逻辑,使接口转换为与新库兼容的接口。


装饰者模式 

装饰者模式与很多其他模式都有非常大的差异。 装饰者模式无需为每个功能组合创建一个子类,就可以解决在一个类上添加或更改功能的问题。 例如,有一个 car (汽车)类及其默认功能。 汽车有多种可选功能供您添加在车上(比如电源锁、电动车窗和空调)。 如果您尝试使用子类来做到这一点,您共需要 8 个类来覆盖所有组合:

Car

CarWithPowerLocks

CarWithPowerWindows

CarWithAc

CarWithPowerLocksAndPowerWindows

CarWithPowerLocksAndAc

CarWithPowerWindowsAndAc

CarWithPowerWindowsAndPowerLocksAndAc

这可能会非常迅速地超出可控范围,因为只是多添加一个选项,就会造成多增加 8 个子类。 使用装饰者模式可以解决这个问题,每次添加一个新的选项,您可以只创建一个类,而不是使类的数量增加一倍。


装饰者模式的结构

装饰者模式的工作方式是,使用与基本对象 (Car) 具有相同接口的装饰者对象包装基本对象。 关于装饰者如何处理方法,有几个选项:

它可以完全覆盖它所包装的对象的方法;

如果装饰者不影响方法的行为,它可以直接传递给被包装的对象;

在将调用传递给被包装的对象之前或之后,装饰者可以添加行为。

装饰者并不仅限于包装基本对象;它们还可以包装其他装饰者,因为它们都实施相同的接口。 一般结构类似于图 2 所示。



图 2. 装饰者模式的结构 


装饰者模式示例

让我们采用上面的 car 图示,并将它变成实际的代码。 首先构造基本
 Car
 类。

var Car = function() {
console.log('Assemble: build frame, add core parts');
};

// The decorators will also need to implement this interface
Car.prototype = {
start: function() {
console.log('The engine starts with roar!');
},
drive: function() {
console.log('Away we go!');
},
getPrice: function() {
return 11000.00;
}
};


为了使代码保持简单,我们只发送短语给控制台,以显示发生了什么事情。 显然,您可以在此添加更多功能,因为汽车是非常复杂的东西,但为了这个演示,我相信您不会介意保持简单。

现在,创建一个 
CarDecorator
 类。 这是一个抽象类,它的目的不是实例化它本身,而应该是被子类化,以创建真正的装饰者。
var CarDecorator = function(car) {
this.car = car;
};

// CarDecorator implements the same interface as Car
CarDecorator.prototype = {
start: function() {
this.car.start();
},
drive: function() {
this.car.drive();
},
getPrice: function() {
return this.car.getPrice();
}
};


这里要注意几个重点。 首先,
CarDecorator
 构造函数使用汽车,或者更确切地说,它需要一个实施与 
Car
 相同接口的对象,其中包括多个 
Car
 和 
CarDecorator
 子类。 另外,请注意,所有方法都只传递请求给被包装的对象。 这是我们所有装饰者都应针对没有被更改的方法使用的默认行为。

现在创建所有装饰者。 因为从 
CarDecorator
 继承,只需要重写发生变化的功能,其他 
CarDecorator
 正常工作。
var PowerLocksDecorator = function(car) {
// Call Parent Constructor
CarDecorator.call(this, car);
console.log('Assemble: add power locks');
};
PowerLocksDecorator.prototype = new CarDecorator();
PowerLocksDecorator.prototype.drive = function() {
// You can either do this
this.car.drive();
// or you can call the parent's drive function:
// CarDecorator.prototype.drive.call(this);
console.log('The doors automatically lock');
};
PowerLocksDecorator.prototype.getPrice = function() {
return this.car.getPrice() + 100;
};

var PowerWindowsDecorator = function(car) {
CarDecorator.call(this, car);
console.log('Assemble: add power windows');
};
PowerWindowsDecorator.prototype = new CarDecorator();
PowerWindowsDecorator.prototype.getPrice = function() {
return this.car.getPrice() + 200;
};

var AcDecorator = function(car) {
CarDecorator.call(this, car);
console.log('Assemble: add A/C unit');
};
AcDecorator.prototype = new CarDecorator();
AcDecorator.prototype.start = function() {
this.car.start();
console.log('The cool air starts blowing.');
};
AcDecorator.prototype.getPrice = function() {
return this.car.getPrice() + 600;
};


在本例中,每当我创建一个方法,对原来的代码增加功能,我只需要调用 
this.car.x()
,而不是调用父类的方法。 一般情况下,更明智的做法是调用父类的方法,但因为这很简单,我想直接使用 
car
 属性会更简单。 如果我需要更改内置在 
CarDecorator
 中的默认行为,以执行传递请求以外的任务,这可能会为我带来很大的麻烦,但是,考虑到这仅仅是一个示例,我不认为会发生这种事情。

那么,现在您已拥有所需要的所有元素,将它们投入使用吧。
var car = new Car();                    // log "Assemble: build frame, add core parts"

// give the car some power windows
car = new PowerWindowDecorator(car);    // log "Assemble: add power windows"

// now some power locks and A/C
car = new PowerLocksDecorator(car);     // log "Assemble: add power locks"
car = new AcDecorator(car);             // log "Assemble: add A/C unit"

// let's start this bad boy up and take a drive!
car.start(); // log 'The engine starts with roar!' and 'The cool air starts blowing.'
car.drive(); // log 'Away we go!' and 'The doors automatically lock'


创建具备您想要的功能的汽车,很简单。 只需创建汽车和您所需的装饰者,同时将汽车及其当前功能集添加到装饰者。 但是创建代码相当长,与只是实例化一个对象(如 
CarWithPowerLocksAndPowerWindowsAndAc
)相比,尤其如此。 不过别担心,我会在关于工厂模式的一节中告诉您如何用更好的方式来创建这些被装饰对象。 同时,如果您想阅读更多有关装饰者模式的材料,您可以在我的个人博客上阅读 JavaScript
Design Patterns: Decorator"。


工厂模式 

工厂模式的名称来自于其预期目的,即简化对象的创建。 简单工厂抽象出新关键字的所有这些用途,所以,如果一个类的名称改变或替换为另一个名称,您只需要在一个地方修改它。 另外,它为许多不同类型的对象或具有不同选项的单一类型对象的创建提供了一站式服务。 用这么几句话来解释标准的工厂模式,这稍有一点难度,所以我会在稍后解释它。


简单的 JavaScript 工厂

本例是一个简单工厂,使用我们在上面的装饰者示例中所遇到的难题: 创建具有功能的汽车的实际代码实在是太长了。 使用一个工厂,您可以将该代码消减为一个函数调用。 从一个只包含单个函数的对象字面量开始。 在 JavaScript 中,一个对象字面量/单例就是一个简单工厂的构建方式。
在经典的面向对象编程语言中,这将是一个静态类。

var CarFactory = {
makeCar: function(features) {
var car = new Car();

// If they specified some features then add them
if (features && features.length) {
var i = 0,
l = features.length;

// iterate over all the features and add them
for (; i < l; i++) {
var feature = features[i];

switch(feature) {
case 'powerwindows':
car = new PowerWindowsDecorator(car);
break;
case 'powerlocks':
car = new PowerLocksDecorator(car);
break;
case 'ac':
car = new ACDecorator(car);
break;
}
}
}

return car;
}

}

该工厂所拥有的一个函数是 
makeCar
,它要执行很多繁重的工作。 首先,它接受的一个参数是一个字符串数组,该数组映射到不同的装饰者类。
makeCar
 创建一个普通的 
Car
 对象,然后遍历所有功能并装饰汽车。
现在,我们不需要用至少 4 行代码来创建具有全部功能的汽车,我们只需要一行代码:

Var myCar = CarFactory.makeCar(['powerwindows', 'powerlocks', 'ac']);


您甚至可以调整 
makeCar
 函数,使它确保仅使用了一个任意类型的装饰者,并且确保它们以特定的顺序附加(就像它们真的是在工厂中生产一样)。 如果您想看到这种示例,以及其他一些使用工厂模式的良好方法,您可以在我的个人博客上阅读JavaScript
设计模式:工厂。


标准工厂模式

标准工厂模式与简单工厂模式相当不同,但当然,它仍然有创建对象的作用。 但我们不使用单例,只使用一个类的抽象方法。 例如,让我们假装有几个不同的汽车制造商,他们都有自己的店铺。 大部分情况下,所有店铺都利用同样的方法销售汽车,但他们制造不同的汽车。 因此,他们的方法都继承自相同的原型,但他们实施自己的制造流程。

让我们将该示例变成代码,以帮助理解我所谈论的内容。 首先创建一个汽车店,其目的只是被子类化,并且只有一个方法 
– manufactureCar –
 这是一个存根。 除非它被子类覆盖,否则它会抛出一个错误。
/* Abstract CarShop "class" */
var CarShop = function(){};
CarShop.prototype = {
sellCar: function (type, features) {
var car = this.manufactureCar(type, features);

getMoney(); // make-believe function

return car;
},
decorateCar: function (car, features) {
/*
Decorate the car with features using the same
technique laid out in the simple factory
*/
},
manufactureCar: function (type, features) {
throw new Error("manufactureCar must be implemented by a subclass");
}
};

请注意, 
sellCar
 调用 
manufactureCar
。这意味着 
manufacturecar
 需要被一个子类执行才能让你卖出一辆汽车。然后你可以创建几个汽车店,看看它们是如何执行它的

/* Subclass CarShop and create factory method */
var JoeCarShop = function() {};
JoeCarShop.prototype = new CarShop();
JoeCarShop.prototype.manufactureCar = function (type, features) {
var car;

// Create a different car depending on what type the user specified
switch(type) {
case 'sedan':
car = new JoeSedanCar();
break;
case 'hatchback':
car = new JoeHatchbackCar();
break;
case 'coupe':
default:
car = new JoeCoupeCar();
}

// Decorate the car with the specified features
return this.decorateCar(car, features);
};

/* Another CarShop and with factory method */
var ZimCarShop = function() {};
ZimCarShop.prototype = new CarShop();
ZimCarShop.prototype.manufactureCar = function (type, features) {
var car;

// Create a different car depending on what type the user specified
// These are all Zim brand
switch(type) {
case 'sedan':
car = new ZimSedanCar();
break;
case 'hatchback':
car = new ZimHatchbackCar();
break;
case 'coupe':
default:
car = new ZimCoupeCar();
}

// Decorate the car with the specified features
return this.decorateCar(car, features);
};


各个方法的工作方式基本上是完全相同的,但它们各自创建不同的汽车(为了简洁一点,我省去了汽车类的实施,因为它们不是必要的)。 重点是,
manufactureCar
 方法是工厂方法。 工厂方法是父类的抽象,由子类实施,它的工作是创建对象(每个工厂方法创建的对象都具有相同的接口)。


结束语

我们已经介绍了工厂模式背后的基础知识。 如果您希望再阅读多一点有关工厂模式的文章,可以访问我的个人博客上的 JavaScript
设计模式:工厂 (简单工厂) 和 JavaScript
设计模式:工厂,第 2 部分 (标准工厂)。


下一步阅读方向

我们总结了 JavaScript 设计模式系列的第 2 部分。 希望它能帮助您基本了解如何在 JavaScript 中实施适配器模式、装饰者模式和工厂模式。 JavaScript 设计模式的第三部分(最后一部分)帖子不久就会发布,但如果您不耐烦了,我的个人博客已经完成了有关 JavaScript 设计模式的系列,其中包括一些我不会在这个系列中介绍的模式: Joe
Zim 的 JavaScript 博客上的 JavaScript 设计模式。.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息