工厂模式

今天我们主要介绍工厂模式(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"]);

// 不同的店决定我会拿到不同品牌的车
// 就算我们给它相同的参数