工厂模式
今天我们主要介绍工厂模式(the Factory Pattern)。工厂模式是我最喜欢的模式之一,尤其是我稍后会讲的“简单工厂”。工厂——在现实生活以及在程序世界里——都是用来创建object的(译注:作者这里一语双关,object有物品和对象两个意思,分别对应“现实生活”和“程序世界”)。通过省下所有的new
操作符,它可以让你的代码变得干净利落。
像往常一样我,我在文章底部列出了JavaScript设计模式系列的所有文章。我认为你花点时间去读下它们还是值得的。
简单工厂
有两种类型的工厂:简单工厂和标准工厂。我们会从简单工厂开始,因为…它确实更简单。今天,我们不打算搞个新例子出来,我会直接用装饰者模式那篇文章里的例子,把它修复的更棒。如果你还不理解装饰者模式,那你真应该先回过头去读一下上篇文章。
那么,工厂模式会做些什么事让装饰者模式那个例子变得更好呢?如果你还记得那个例子的最终实现代码,那么当你想要一个有3种功能的车,就得用new操作符创建4个对象。这么做单调乏味又烦人,所以我们打算只调用一个方法就能创建出一部拥有所有功能的车。
简单工厂就像一个单例(或者是大多数语言中的静态类,然而在JavaScript里,它们本质上是一样的),它有一个或多个方法来创建或者返回对象。在下面的代码中你会看到对象是如何创建和使用的。
var CarFactory = {
// 用只一个方法就能制造一辆可任意组合功能的汽车
makeCar: function (features) {
var car = new Car();
// 如果指定了功能就把功能加到car上
if (features && features.length) {
var i = 0,
l = features.length;
// 遍历所有的功能并添加到car上
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;
}
}
// 调用工厂方法,传一个字符串列表进去
// 这些字符串标识了你想让这辆车拥有的功能
var myCar = CarFactory.makeCar(['powerwindows', 'ac']);
// 如果你只想一辆普通的老款车,那就什么参数都不用传
var myCar = CarFactory.makeCar();
让工厂模式变得更好
虽然简单工厂能让解决问题变得简单,但是,上面对装饰者模式改进的代码还是有些问题。第一个问题是,无法保证一个功能只被添加了一次。也就是说你可能对同一辆车包装了多次PowerWindowDecorator
,而你却浑然不知。另一个问题是,如果这些功能的添加需要遵循特定的顺序的话,我们并没有一种强制的规则来约束。
我们可以用工厂模式来修复这两个问题。最让人称道的是,所有的逻辑并不是包含在Car或者是装饰者对象里的,而是只放在一个地方:工厂,从现实世界来看就应该是这样。难道你见过一辆车自己知道怎么添加功能么?或者你听说过这些功能自己知道要按什么样的顺序加到车上么?当然没有,这些都是工厂控制的。
var CarFactory = {
makeCar: function (features) {
var car = new Car(),
// 创建一个所有功能的列表,都设置成0,表示它还没有被添加
featureList = {
powerwindows: 0,
powerLocks: 0,
ac: 0
};
// 如果指定了功能就把功能加到car上
if (features && features.length) {
var i = 0,
l = features.length;
// 遍历所有的功能并添加到car上
for (; i < l; i++) {
// 在功能列表里标记哪个功能会被添加,
// 这样我们只获得每个功能中的一个
featureList[features[i]] = 1;
}
// 现在我们可以按一定的顺序添加功能了
if (featureList.powerwindows) {
car = new PowerWindowsDecorator(car);
}
if (featureList.powerlocks) {
car = new PowerLocksDecorator(car);
}
if (featureList.ac) {
car = new ACDecorator(car);
}
}
return car;
}
}
// 现在那些粗心和程序员可以这么调用
var myCar = CarFactory.makeCar(['ac', 'ac', 'powerlocks', 'powerwindows', 'ac']);
// 而且,它只会给你的车加一个ACDecorator,并且是以正确的顺序
现在你明白为什么简单工厂模式是我最喜欢的模式之一了吧?它省下创建对像时多余的字节。使用工厂创建一个对象时,除工厂自身之外不需要依赖任何接口,这太傻瓜式了。
另一种途径
工厂模式的强大不止限于对装饰者。基本上对共享一个接口的任何对象都可以用工厂来创建,它可以帮我们从代码里剥离独立对象。你肯定知道自己从工厂接收到的是什么类型的对象,所以你唯一需要依赖的只有工厂和一个接口,而无所谓有多少不同的对象实现了那个接口。
我给你们展示一个没有装饰者的工厂例子怎么样?下面这个工厂我们假设它是一个MVC(Model View Controller)框架的一部分。工厂获取一个特定类型的model对象,并把它传回到controller。
不同的controller使用不同的model,甚至有可能同一个controller里不同的方法使用不同的model。相比把具体的model“类”名硬编码到controller里,我们选择用工厂来获取model。使用这种方法时,如果我们想用一个新的model类(可能我们决定使用不同类型的数据库了),我们只需要在在那个工厂里做修改就行了。我不会去实现具体的细节,因为这是浪费时间。我们只展示它是怎么使用的,你自己负责想像代码的实现。下面展示的就是用工厂模式实现的一个控制器的代码。
// 这个controller使用两种不同的model:car和cars
var CarController = {
getCars: function () {
var model = ModelFactory.getModel('cars');
return model.get('all');
},
getCar: function (id) {
var model = ModelFactory.getModel('car');
return model.get(id);
},
createCar: function () {
var model = ModelFactory.getModel('car');
model.create();
return model.getId();
},
deleteCars: function (carIds) {
var model = ModelFactory.getModel('cars');
model.delete(carIds);
},
.
.
.
}
什么是真正的工厂模式
真正的工厂模式不同于简单工厂,因为它不是使用一个独立对象来创建汽车(car)的(见装饰者示例),它使用的是子类(subclass)。工厂模式的官方定义是:在子类中对一个类的成员对象进行实例化。
汽车店工厂示例
这个例子里我们继续汽车主题,我会延用Car
和我在装饰者模式中为它创建的装饰者的例子。但是我会增加一些车型,为的是帮助我们看到标准工厂是怎么工作的。别担心,我们其实也并没有做什么,这些车型只是Car
的子类。为了保持代码的简洁,况且这些子类也确实没影响到什么,所以你甚至看不到这些子类的具体实现。
我们从一个汽车店开始(就叫CarShop
吧)。我们会从汽车店里买到车,因为没人会直接去工厂买车(尽管在这个例子里我们的CarShop
用了工厂模式)。CarShop
并不是一个可以直接使用的对象,它本质上是一个抽象类。因为它只是实现了一些功能接口,但是它并没有被实例化,因为这些功能是留给子类去实现的。我们来看一下:
/* 抽象 CarShop “类” */
var CarShop = function(){};
CarShop.prototype = {
sellCar: function (type, features) {
var car = this.manufactureCar(type, features);
getMoney(); // 一个函数调用
return car;
},
decorateCar: function (car, features) {
/*
给车加装新功能使用和CarFactory里相同的技术,
请看我的上篇文章:http://www.codingserf.com/index.php/2015/05/javascript-design-patterns-factory-part-1/
*/
},
manufactureCar: function (type, features) {
throw new Error("manufactureCar必须由子类开实现");
}
};
看到decorateCar
方法了吧?它和上篇文章中的CarFactory.makeCar
是同一个方法,只不过这里是接受一个Car
对象的参数,而不是自己去初始化一个car。注意这里定义的manufactureCar
只是抛出一个错误,它是由子类来实现的方法,它其实就是一个工厂方法。现在我们就来建一个实现manufactureCar
的具体的汽车店吧。
/* 继承CarShop,同时创建工厂方法 */
var JoeCarShop = function() {};
JoeCarShop.prototype = new CarShop();
JoeCarShop.prototype.manufactureCar = function (type, features) {
var car;
// 根据用户的指定创建不同的汽车
switch(type) {
case 'sedan':
car = new JoeSedanCar();
break;
case 'hatchback':
car = new JoeHatchbackCar();
break;
case 'coupe':
default:
car = new JoeCoupeCar();
}
// 给汽车加装指定的功能
return this.decorateCar(car, features);
};
这家店只出售Joe牌汽车,所以工厂方法和销售其他类型汽车的店是不同的,比如和下面这个就不一样,下面这家店只卖Zim牌的车。
/* 另一个CarShop和工厂方法 */
var ZimCarShop = function() {};
ZimCarShop.prototype = new CarShop();
ZimCarShop.prototype.manufactureCar = function (type, features) {
var car;
// 根据用户的指定创建不同的汽车
// 这些全是Zim牌的
switch(type) {
case 'sedan':
car = new ZimSedanCar();
break;
case 'hatchback':
car = new ZimHatchbackCar();
break;
case 'coupe':
default:
car = new ZimCoupeCar();
}
// 给汽车加装指定的功能
return this.decorateCar(car, features);
};
你的汽车店开张了
下面你会看到怎么使用你刚刚创建的这些汽车店。从我个人来说我还是觉得简单工厂会更酷,你不妨尝试一下用简单工厂来创建你的汽车店。这么多工厂实现方式会让你看上去很专业!
// Joe店
var shop = new JoeCarShop();
var car = shop.sellCar("sedan", ["powerlocks"]);
// Zim店怎么办,同样
shop = new ZimCarShop();
car = shop.sellCar("sedan", ["powerlocks"]);
// 不同的店决定我会拿到不同品牌的车
// 就算我们给它相同的参数