阅读 100

JavaScript设计模式第2篇:工厂模式

分类

这里工厂模式分为2类:简单工厂工厂方法,下一节会介绍第3类工厂模式:抽象工厂

简单工厂

定义

简单工厂:定义一个类来创建其他类的实例,根据参数的不同返回不同类的实例,通常这些类拥有相同的父类。

例子

假设现在有 3 款车,Benz、Audi 和 BMW,他们都继承自父类 Car,并且重写了父类方法 drive:

class Car {
    drive () {
        console.log('Car drive');
    }
}

class Benz extends Car {
    drive () {
        console.log(`Benz drive`)
    }
}

class Audi extends Car {
    drive () {
        console.log(`Audi drive`)
    }
}

class BMW extends Car {
    drive () {
        console.log(`BMW drive`)
    }
}
复制代码

我们定义了一个父类 Car,包含一个方法 drive,Benz、Audi 和 BMW 继承自共同的父类 Car。

那么我们在实例化这 3 款车的时候,就需要如下调用:

let benz = new Benz();
let audi = new Audi();
let bmw = new BMW();

benz.drive();       // Benz drive
audi.drive();       // Audi drive
bmw.drive();        // BMW drive
复制代码

这种写法就很繁琐,这时候就用到我们的简单工厂了,提供一个工厂类:

class SimpleFactory {
    static getCar (type) {
        switch (type) {
            case 'benz':
                return new Benz();
            case 'audi':
                return new Audi();   
            case 'bmw':
                return new BMW();  
        }
    }
}
复制代码

简单工厂类 SimpleFactory 提供一个静态方法 getCar,我们再实例化 3 款车的时候,就变成下面这样了:


let benz = SimpleFactory.getCar('benz');
let audi = SimpleFactory.getCar('audi');
let bmw = SimpleFactory.getCar('bmw');

benz.drive();       // Benz drive
audi.drive();       // Audi drive
bmw.drive();        // BMW drive
复制代码

这么一看,使用简单工厂后代码行数反而变多了,这种写法真的有优势吗?

我们要知道,设计模式并不是为了减少代码行数而出现的,它是为了使我们的代码更好扩展,更好维护,更方便使用而出现的。那么使用简单工厂后,有什么好处呢?

简单工厂,用户不需要知道具体产品的类名,只需要知道对应的参数即可,对于一些复杂的类名,可以减少用户的记忆量,同时用户无需了解这些对象是如何创建及组织的,有利于整个软件体系结构的优化。

所以,使用简单工厂,是将类的实例化交给工厂函数去做,对外提供统一的方法。我们要养成一个习惯,在代码中 new 是一个需要慎重考虑的操作,new 出现的次数越多,代码的耦合性就越强,可维护性就越差,简单工厂,就是在上面做了一层抽象,将 new 的操作封装了起来,向外提供静态方法供用户调用,这样就将耦合集中到了工厂函数中,而不是暴露在代码的各个位置。

缺陷

简单工厂有它的好处,必然也有它的缺点,比如下面这种情况:

我们又新增了一类车 Ferrai,就需要在简单工厂类 SimpleFactory 中再新增一个 case,添加 Ferrari 的实例化过程。每新增一类车,就要修改简单工厂类,这样做其实违背了设计原则中的开闭原则:对扩展开放,对修改封闭,同时如果每类车在实例化之前需要做一些处理逻辑的话,SimpleFactory 会变的越来越复杂。

所以简单工厂适用于产品类比较少并且不会频繁增加的情况,那么有什么方法能解决简单工厂存在的问题呢?

工厂方法

工厂方法模式是简单工厂的进一步优化,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂,也就是说每个对象都有一个与之对应的工厂。

定义

工厂方法模式又称为工厂模式,它的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中执行”。

例子

光看概念还是很难理解,我们沿着上面的简单工厂来做修改,举例说明。

还是先定义一个父类 Car,提供一个方法 drive:

class Car {
    drive () {
        console.log('Car drive');
    }
}
复制代码

每一款车都继承自父类 Car,并重写方法 drive:

class Benz extends Car {
    drive () {
        console.log(`Benz drive`)
    }
}

class Audi extends Car {
    drive () {
        console.log(`Audi drive`)
    }
}

class BMW extends Car {
    drive () {
        console.log(`BMW drive`)
    }
}
复制代码

然后按照定义,我们提供一个创建对象的接口:

class IFactory {
    getCar () {
        throw new Error('不允许直接调用抽象方法,请自己实现');
    }
}
复制代码

每款车都提供一个工厂类,实现自上述接口,因为 JavaScript 中没有 implements,所以用 extends 代替:

class BenzFactory extends IFactory {
    getCar () {
        return new Benz();
    }
}

class AudiFactory extends IFactory {
    getCar () {
        return new Audi();
    }
}

class BMWFactory extends IFactory {
    getCar () {
        return new BMW();
    }
}
复制代码

这样当我们需要实例化每款车的时候,就按如下操作:

let benzFactory = new BenzFactory();
let benz = benzFactory.getCar();

let audiFactory = new AudiFactory();
let audi = audiFactory.getCar();

let bmwFactory = new BMWFactory();
let bmw = bmwFactory.getCar();

benz.drive();       // Benz drive
audi.drive();       // Audi drive
bmw.drive();        // BMW drive
复制代码

我们再来对比一下简单工厂的实例化过程:

let benz = SimpleFactory.getCar('benz');
let audi = SimpleFactory.getCar('audi');
let bmw = SimpleFactory.getCar('bmw');

benz.drive();       // Benz drive
audi.drive();       // Audi drive
bmw.drive();        // BMW drive
复制代码

我们用 UML 类图来描述一下工厂方法模式:

可以看到,同样是为了实例化一个对象,怎么就变得这么复杂了呢····我们来总结一下工厂方法模式的步骤:

  1. 定义产品父类 -- Car
  2. 定义子类实现父类,并重写父类方法 -- BenzCar、AudiCar、BMWCar
  3. 定义抽象接口,以及抽象方法 -- IFactory
  4. 定义工厂类,实现自抽象接口,并且实现抽象方法 -- BenzFactory、AudiFactory、BMWFactory
  5. new 工厂类,调用方法进行实例化

那么工厂方法增加了如此多的流程,提高了复杂度,究竟解决了简单工厂的什么问题呢?

通过上文可以知道,简单工厂在新增一款车的时候,需要修改简单工厂类 SimpleFactory,违背了设计模式中的**“开闭原则”**:对扩展开放,对修改封闭。

当工厂方法需要新增一款车的时候,比如 Ferrari,只需要定义自己的产品类 Ferrari 以及自己的工厂类 FerrariFactory:

class Ferrari extends Car {
    drive () {
        console.log(`Ferrari drive`)
    }
}

class FerrariFactory extends IFactory {
    getCar () {
        return new Ferrari();
    }
}

let ferrariFactory = new FerrariFactory();
let ferrari = ferrariFactory.getCar();

ferrari.drive();       // Ferrari drive
复制代码

完全不用修改已有的抽象接口 IFactory,只需要扩展实现自己需要的就可以了,不会影响已有代码。这就是对扩展开放,对修改封闭

总结

  1. 简单工厂模式
    • 解决了用户多次自己实例化的问题,屏蔽细节,提供统一工厂,将实例化的过程封装到内部,提供给用户统一的方法,只需要传递不同的参数就可以完成实例化过程,有利于软件结构体系的优化;
    • 但不足之处是,增加新的子类时,需要修改工厂类,违背了“开闭原则”,并且工厂类会变得越来越臃肿;
    • 简单工厂模式适用于固定的,不会频繁新增子类的使用场景
  2. 工厂方法模式
    • 通过在上层再增加一层抽象,提供了接口,每个子类都有自己的工厂类,工厂类实现自接口,并且实现了统一的抽象方法,这样在新增子类的时候,完全不需要修改接口,只需要新增自己的产品类和工厂类就可以了,符合“开闭原则”;
    • 但不足之处也正是如此,持续的新增子类,导致系统类的个数将成对增加,在一定程度上增加了系统的复杂度,同时有更多的类需要编译和运行,会给系统代理一些额外的开销;
    • 工厂方法模式适用于会频繁新增子类的复杂场景;