小蜗熊的蜂蜜罐
JavaScript设计模式简介之工厂模式
发布于: 2020-06-29 更新于: 2020-07-02 分类于: 技术 > Web 阅读次数: 

工厂模式是JavaScript中最常用的一种用于创建对象的设计模式,其核心就是将逻辑封装在一个函数中不暴露创建对象的具体逻辑。工厂模式适用于对象的构建十分复杂、需要依赖具体环境创建不同实例、处理大量具有相同属性的小对象等情况,但滥用工厂模式也给代码增加不必要的复杂度,使用时需要谨慎。根据抽象程度的不同,工厂模式又可以分为简单工厂、工厂方法和抽象工厂三种。

简单工厂

简单工厂模式又称为静态工厂模式,是由一个工厂函数来实例化多个产品对象,主要用来创建同一类对象。其过程图如下:

classDiagram Client -- Factory Factory <-- productOne Factory <-- productTwo class Factory{ + createProduct() } class productOne{ data method() } class productTwo{ data method() }

下面举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var factory=function(data){
var product = new Object;
product.data = data;
product.method = function(){
console.log("I am Product "+this.data);
};
return product
};
//调用
var product1 = factory(1);
var product2 = new factory(2);
//这里使用了new结果相同
product1.method()
//输出:I am Product 1
product2.method()
//输出:I am Product 2

其中Factory就是一个简单工厂,通过传入不同的参数生成许多同类的对象。对于这个例子而言,由于Factory的返回值是一个对象,所以在使用时无论是否作为构造函数调用(即是否加new)运行结果均相同,都是生成的对象。

值得一提的是,上面的例子中的对象productOneproductTwo在内存中均含一个method方法,二者并不共用,这会导致一定程度的浪费。详见工厂模式与构造函数一节

工厂方法

对于生成不同种类的对象,上面介绍的简单工厂无法做到,可以使用下面的工厂方法。工厂方法模式与上面的不同在于将实际创建对象的工作推迟到子类中,使得核心类就变成了抽象类,只作为接口来调用子类的构造函数。但是由于在JavaScript中很难像传统面向对象那样去实现创建抽象类。所以参考其核心思想可以将工厂方法看作是一个实例化对象的工厂类。具体过程如下:

classDiagram Client -- Factory Factory <-- ConcreteFactoryOne Factory <-- ConcreteFactoryTwo ConcreteFactoryOne <-- ProductOne ConcreteFactoryTwo <-- ProductTwo class ConcreteFactoryOne{ + createProductOne() } class ConcreteFactoryTwo{ + createProductTwo() } class ProductOne{ data method() } class ProductTwo{ data method() }

对上面的例子进行改写,使用Factory同时产生两种对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//使用安全模式创建工厂方法函数
var Factory = function(child,data) {
if(this instanceof Factory) {
var product = new this[child](data);
return product;
} else {
return new Factory(child,data);
}
};
//设置子类构造函数
Factory.prototype = {
ConcreteFactoryOne: function(data) {
this.data = data;
this.method = function(){
console.log("Product 1, data " + this.data)
};
},
ConcreteFactoryTwo: function(data) {
this.data = data,
this.method = function(){
console.log("Product 2, data " + this.data)
};
},
}
Factory('ConcreteFactoryOne',1).method();
//这里没用使用new结果相同
//输出:Product 1, data 1
new Factory('ConcreteFactoryTwo',2).method();
//输出:Product 2, data 2

第二个例子中,Factory主要是起一个接口的作用,真正实例化的操作在其子类之中。如果直接使用switch-case一类的流程判断也可以完成接口逻辑,但添加新子类后也需对接口进行修改。因此使用上例中的方法可以减少改动的位置。

由于两个子类的构造函数保存在Factory.prototype中,所以必须先对父类Factory进行实例化才能调用这两个函数再实例化子类。开头使用的安全模式是为了确保父类被成功实例化,防止因在使用过程中忘记使用new(如例中所示) 而导致的错误。把构造函数保存在Factory.prototype中而不直接在Factory中是因为安全模式可能会实例化多个父类对象Factory,避免同一个函数在多次实例化过程对内存的浪费。这两个问题其实是相互作用相互影响的。

抽象工厂

抽象工厂与上面的两种方法不同,它并不直接生成实例而是生成簇。抽象工厂实际上是一个实现子类继承父类的方法,在这个方法中需要传递子类以及要继承父类的名称。并且在抽象工厂方法中增加了一次对抽象类存在性的一次判断,如果抽象类存在,则将子类通过寄生式继承父类的方法。过程如下:

classDiagram Client -- AbstractFactory AbstractFactory <-- ConcreteFactoryOne AbstractFactory <-- ConcreteFactoryTwo ConcreteFactoryOne <-- ProductOneA ConcreteFactoryOne <-- ProductOneB ConcreteFactoryTwo <-- ProductTwoA ConcreteFactoryTwo <-- ProductTwoB ProductOneA -- AbstractProductA ProductTwoA -- AbstractProductA ProductOneB -- AbstractProductB ProductTwoB -- AbstractProductB class ConcreteFactoryOne{ + createProductOneA() + createProductOneB() } class ConcreteFactoryTwo{ + createProductTwoA() + createProductTwoB() } class ProductOneA{ data method() } class ProductOneB{ data method() } class ProductTwoA{ data method() } class ProductTwoB{ data method() }

在JavaScript中,由于无法像其他传统面向对象的语言中那样使用abstract声明抽象类,需要通过在类的方法中抛出错误来对抽象类进行模拟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//抽象工厂方法
var AbstractFactory = function(subType, superType) {
//判断抽象工厂中是否有该抽象类
if(typeof AbstractFactory[superType] === 'function') {
//缓存类
function F() {};
//继承父类属性和方法
F.prototype = new AbstractFactory[superType] ();
//将子类的constructor指向子类
subType.constructor = subType;
//子类原型继承父类
subType.prototype = new F();
} else {
throw new Error('抽象类不存在!')
}
}

//抽象类Product A
AbstractFactory.AbstractProductA = function() {
this.type = 'ProductA';
}
AbstractFactory.AbstractProductA.prototype = {
getData: function() {
return new Error('抽象方法不能调用');
},
}
//抽象类Product B
AbstractFactory.AbstractProductB = function() {
this.type = 'ProductB';
}
AbstractFactory.AbstractProductB.prototype = {
getData: function() {
return new Error('抽象方法不能调用');
},
}

//抽象工厂实现对抽象类的继承
function ConcreteFactoryOne(data) {
this.data = data;
}
AbstractFactory(ConcreteFactoryOne, 'AbstractProductA');
//子类中重写抽象方法
ConcreteFactoryOne.prototype.getData = function() {
console.log("Data: "+ this.data + " Type: " + this.type);
}
function ConcreteFactoryTwo(data) {
this.data = data;
}
AbstractFactory(ConcreteFactoryTwo, 'AbstractProductB');
ConcreteFactoryTwo.prototype.getData = function() {
console.log("Data: "+ this.data + " Type: " + this.type);
}

//测试
var productOneA = new ConcreteFactoryOne("I am productOneA");
var productTwoB = new ConcreteFactoryTwo("I am productTwoB");
productOneA.getData();
//输出 Data: I am productOneA Type: ProductA
productTwoB.getData();
//输出 Data: I am productTwoB Type: ProductB

在这个例子中AbstractFactory并不直接创建实例,而是通过类的继承进行类簇的管理。抽象工厂模式的优点在于具体产品在应用层代码隔离,无须关心创建细节,但同时也增加了系统的抽象性和理解难度。通常使用场景是:

  • 创建的对象和使用它们的系统是分离的
  • 需要创建的对象是家族式的
  • 创建的众多对象是在一起使用的
  • 具体创建对象的类和系统解耦

工厂模式与构造函数

简单工厂一节中的例子用构造函数可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Product(data){
this.data = data;
};
Product.prototype.method = function(){
console.log("I am Product "+this.data);
};
//调用
var product1 = new Product(1);
var product2 = new Product(2);
product1.method()
//I am Product 1
product2.method()
//I am Product 2

工厂模式与构造函数都可以完成对象的创建,主要区别有以下几点:

  • 构造函数需使用new来创建对象,工厂模式可以直接调用函数(使用new的话结果一样都是返回对象但与工厂模式的思想不符)
  • 构造函数创造的对象有固定的父类(如上面的例子中执行product1 instanceof Product结果为true),工厂模式没有。
  • 构造函数可以定义原型,多个对象可以共用同一个原型函数减少内存开销。简单工厂不行(工厂方法的方式就可以做到)。
  • 构造函数中this指的是当前类,工厂函数中this指全局对象window(严格模式下为undefined)。

后记

本来是计划写一个系列的设计模式介绍,可最近心情不太好,光是这一篇就拖了好几天,硬生生地从六月拖到了七月。下一篇应该会介绍单例模式,就是不知道什么时候能完成了,最近心烦意乱,先冷静一下思考下人生吧。

--- 本文结束 The End ---