一个类或对象中往往会包含别的对象。在创建这种成员对象时,你可能习惯于使用常规方式,也即用new关键字和类构造函数。问题在于这回导致相关的两个类之间产生依赖性。
工厂模式用于消除这两个类之间的依赖性,它使用一个方法来决定究竟要实例化哪个具体的类。这里介绍简单工厂模式(动态选择并生成实例)及真正的工厂模式(亦称工厂方法),后者使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
一、简单工厂
拿生产自行车举例子,先看它的类及对应的方法——
var Bicycle = function() {}; Bicycle.prototype = { // 组装 assemble: function() { }, // 清洗 wash: function() { }, // 骑车 ride: function() { }, // 维修 repair: function() { } };
假设你想开几个自行车商店,每个店都有集中型号的自行车出售。这可以用一个类来表示——
var BicycleShop = function() {}; BicycleShop.prototype = { // 卖自行车 sellBicycle: function(model) { var bicycle; // 注:默认这里的每一款自行车都继承Bicycle,且都有自己的方法实现 switch(model) { case 'The Speedster': bicycle = new Speedster(); break; case 'The Lowrider': bicycle = new Lowrider(); break; case 'The Comfort Cruiser': default: bicycle = new Cruiser(); } // 对自行车进行组装 bicycle.assemble(); // 对自行车进行清洗 bicycle.wash(); // 将自行车交付于客户 return bicycle; } }; // 那么,要出售某种型号的自行车,只要调用sellBicycle方法即可 var californiaCruisers = new BicycleShop(); // 购买你所需要的自行车 var youNeedBicycle = californiaCruisers.sellBicycle('The Speedster');
在情况发生之前,这倒也挺管用。但要是你想在供货目录中加入一款新车型又会怎么样呢?你得为此修改BicycleShop的代码,哪怕这个类的实际功能实际上并没有发生改变——依旧是创建一个自行车的新实例,组装它、清洗它,然后把它交给客户。更好的办法是把sellBicycle方法中“创建新实例”这部分工作转交给一个简单工厂对象。
var BicycleFactory = { createBicycle: function(model) { var bicycle; switch(model) { case 'The Speedster': bicycle = new Speedster(); break; case 'The Lowrider': bicycle = new Lowrider(); break; case 'The Comfort Cruiser': default: bicycle = new Cruiser(); } return bicycle; } }; // 这里的BicycleFactory是一个单体,用来把createBicycle方法封装在一个命名空间中。 // 这个方法返回一个实现了Bicycle接口的对象,然后你可以照常对其进行组装和清洗。 var BicycleShop = function() {}; BicycleShop.prototype = { // 卖自行车 sellBicycle: function(model) { var bicycle; // 应用工厂模式创建所需要的自行车 bicycle = BicycleFactory.createBicycle(model); // 对自行车进行组装 bicycle.assemble(); // 对自行车进行清洗 bicycle.wash(); // 将自行车交付于客户 return bicycle; } };
这样,这个BicycleFactory对象可以供各种类来创建新的自行车实例。有关可供车型的所有信息都集中在一个地方管理,所以添加更多车型很容易。从而售车商店BicycleShop与添加具体的自行车车型达到解耦。
以上就是简单工厂的一个很好的例子。这种模式把成员对象的创建工作转交给一个外部对象。这个外部对象可以像本例中一样是一个简单的命名空间,也可以是一个类的实例。如果负责创建吃力的方法的逻辑不会发生变化,那么一般说来用单体或静态类方法创建这些成员实例是合乎情理的。但如果你要提供几种不同品牌的自行车,那么更恰当的做法是把这个创建方法实现在一个类中,并从该类派生出一些子类。
二、工厂模式(工厂方法)
真正的工厂模式与简单工厂模式的区别在于,它不是另外使用一个类或对象来创建自行车,而是使用一个子类。按照正式定义,工厂是一个将其成员对象的实例化推迟到子类中进行的类。
// BicycleShop class (abstract) // 该类不能被实例化,只能用来派生子类。 // 设计一个经销特定自行车生产厂家产品的子类需要扩展BicycleShop,重新定义其中的createBicycle方法 var BicycleShop = function() {}; BicycleShop.prototype = { // 卖自行车 sellBicycle: function(model) { var bicycle = this.createBicycle(model); bicycle.assemble(); bicycle.wash(); return bicycle; }, // 创建自行车 createBicycle: function(model) { throw new Error('Unsupported operation on an abstract class.'); } };
现在我们来定义两个商店,一个商店从Acme公司进货,另一个则从GeneralProducts公司进货。也就是继承BicycleShop的具体子类
// 从Acme公司进货的商店 var AcmeBicycleShop = function() {}; AcmeBicycleShop.prototype = new BicycleShop(); AcmeBicycleShop.prototype.createBicycle = function(model) { var bicycle; switch(model) { case 'The Speedster': bicycle = new AcmeSpeedster(); break; case 'The Lowrider': bicycle = new AcmeLowrider(); break; case 'The Comfort Cruiser': default: bicycle = new AcmeCruiser(); } return bicycle; };
// 从GeneralProducts公司进货的商店 var GeneralProductsBicycleShop = function() {}; GeneralProductsBicycleShop.prototype = new BicycleShop(); GeneralProductsBicycleShop.prototype.createBicycle = function(model) { var bicycle; switch(model) { case 'The Speedster': bicycle = new GeneralProductsSpeedster(); break; case 'The Lowrider': bicycle = new GeneralProductsLowrider(); break; case 'The Comfort Cruiser': default: bicycle = new GeneralProductsCruiser(); } return bicycle; };
选择同一个自行车款式,不同品牌的车子——
var AcmeShop = new AcmeBicycleShop(); AcmeShop.sellBicycle('The Lowrider'); var GeneralProductsShop = new GeneralProductsBicycleShop(); GeneralProductsShop.sellBicycle('The Lowrider');
因为两个生产厂家生产的自行车款式完全相同,所以顾客买车时可以不用关心车究竟是哪家生产的。要是他们只想要Acme生产的自行车,他们可以去Acme专卖店买。
增加对其他生产厂家的支持很简单,只要再创建一个BicycleShop的子类并重定义其createBicycle工厂方法即可。我们也可以对各个子类进行修改,以支持相关厂家其他型号的产品。这是工厂模式最重要的特点。对Bicycle进行一般性操作的代码可以全部写在父类BicycleShop中,而对具体的Bicycle对象进行实例化的工作则被留到子类中。
三、使用原则
1. 动态实现
创建一些用不同方式实现同一接口的对象,那么可以使用一个工厂方法或简单工厂对象来简化选择实现的过程。你通常要与一系列实现了同一个接口、可以被同等对待的类打交道。这是JavaScript中使用工厂模式的最常见的原因。
2. 节省设置开销
如果对象需要进行复杂并且彼此相关的设置,那么使用工厂模式可以减少每种对象所需的代码量。 如果这种设置只需要为特定类型的所有实例执行一次即可,这种作用尤其突出。把这种设置代码放到类的构造函数中并不是一个高效的做法,这是因为即便设置工作已经完成,每次创建新实例的时候这些代码还是会执行,而且这样做会把设置代码分散到不同的类中。工厂方法非常适合于这种场合。它可以在实例化所有需要的对象之前先一次性地进行设置。无论有多少不同的类会被实例化,种方法都可以让设置代码集中在一个地方。 如果所用的类要求加载外部库的话,这尤其有用。工厂方法可以对这些库进行检查并动态加载那些未找到的库。这些设置代码只存在于一个地方,因此以后改起来也方便得多。
四、优势及劣势
优势——
工厂模式的主要好处在于消除对象间的耦合。通过使用工厂方法而不是new关键字及具体类,你可以把所有实例化代码集中在一个位置。这可以大大简化更换所用的类或在运行期间动态选择所用的类的工作。在派生子类时它提供了更大的灵活性。使用工厂模式,你可以先创建一个抽象的父类,然后在子类中创建工厂方法,从而把成员对象的实例化推迟到更专门化的子类中进行。
所有这些好处都与面向对象设计的这两条原则有关:弱化对象间的耦合;防止代码的重复。在一个方法中进行类的实例化,可以消除重复性的代码。这是在用一个对接口的调用取代一个具体的实现。这些都有助于创建模块化的代码。
劣势——
可能有人禁不住想把工厂方法当做万金油去用,把普通的构造函数仍在一起。这并不值得提倡。如果根本不可能另外换用一个类,或者不需要在运行期间在一系列可互换的类中进行选择,那就不应该使用工厂方法。大多数类最好使用new关键字和构造函数公开地进行实例化。这可以让代码更简单已读。你可以一眼就看到调用的是什么构造函数,而不必去查看某个工厂方法以便知道实例化的是什么类。工厂方法在其适用场合非常有用,但切勿滥用。如果拿不定主意,那就不要用,因为以后在重构代码时还有机会使用工厂模式。
源自:JavaScript设计模式(人民邮电出版社)——第七章,工厂模式