要准确的理解工厂模式并不简单。

INFO

JavaScript 中没有接口和抽象类的概念,因此基于 JavaScript 理解工厂模式,在实现上与其他语言有所不同。因此学习时要注意区分

假设我有一个手机工厂,工厂里能生产各种手机。小米、苹果、华为等。

每一种手机的生产流程基本相同,但是需要的原材料不一样。

于是我们按照普通的思维定义类时,就会出现一种情况,他们只是在创建时传入的参数不同,但是其他的方法都相同。

code.ts
1
class Xiaomi {
2
constructor() {
3
this.materials = {
4
1: 'xiaomi_material1',
5
2: 'xiaomi_material2',
6
3: 'xiaomi_material3',
7
}
8
}
9
step1() {}
10
step2() {}
11
step3() {}
12
step4() {}
13
}
14
15
class IPhone {
16
constructor() {
17
this.materials = {
18
1: 'iphone_material1',
19
2: 'iphone_material2',
20
3: 'iphone_material3',
21
}
22
}
23
step1() {}
24
step2() {}
25
step3() {}
26
step4() {}
27
}
28
29
class Huawei {
30
constructor() {
31
this.materials = {
32
1: 'huawei_material1',
33
2: 'huawei_material2',
34
3: 'huawei_material3',
35
}
36
}
37
step1() {}
38
step2() {}
39
step3() {}
40
step4() {}
41
}

这样封装没什么问题。不过我们在实践时,可能会遇到一些维护上的小问题。

时光飞逝,类 Xiaomi 已经在代码中用了很久,项目中有几十处代码使用 new Xiaomi() 创建了大量的实例。可是后来我们发现,Xiaomi 已经出了很多种品牌了,例如 小米6,小米7,小米8,而且这些小米手机使用的材料也不一样。而我们最开始使用的 Xiaomi,其实是想要声明的是 小米 4。

为了适应场景的变动和调整,我们需要修改代码。但是 Xiaomi 类已经变成了祖传代码,此时如果轻易修改,风险非常大。即使只是改一个类名 Xiaomi -> Xiaomi4,就要改动几十处。因此我们在设计之初,如何避免未来修改代码的风险呢?

工厂模式就是这里提供的一个解决方案。

工厂模式用于封装和管理对象的创建。工厂模式期望我们在创建对象时,不会对外暴露创建逻辑,并且是通过使用一个共同的接口来创建新的对象。

首先,创建一个工厂方法,通过传入不同的参数,然后声明不同的类。

code.ts
1
function factory(type) {
2
if (type == 'xiaomi') {
3
return new Xiaomi()
4
}
5
if (type == 'iphone') {
6
return new IPhone()
7
}
8
if (type == 'huawei') {
9
return new Huawei()
10
}
11
}

这样,我们就通过工厂方法,使用不同的字符串,与具体的类之间,建立了一个映射关系。

那么,我们在使用时,就不再直接通过 new Xiaomi() 的方式直接创建实例了。而是使用 factory 方法进行创建。

code.ts
1
const xm = factory('xiaomi')
2
const ip = factory('iphone')
3
const hw = factory('huawei')

未来需要将类名进行更改时,例如将 Xiaomi 修改为 Xiaomi4,那么只需要在类的声明和工厂方法里进行修改即可。而其他使用的地方,可以不做修改。

js
1
- class Xiaomi {
2
+ class Xiaomi4 {
3
constructor() {
4
this.materials = {
5
1: 'xiaomi_material1',
6
2: 'xiaomi_material2',
7
3: 'xiaomi_material3',
8
}
9
}
10
step1() {}
11
step2() {}
12
step3() {}
13
step4() {}
14
}
15
16
17
function factory(type) {
18
if (type == 'xiaomi') {
19
- return new Xiaomi()
20
+ return new Xiaomi4()
21
}
22
if (type == 'iphone') {
23
return new IPhone()
24
}
25
if (type == 'huawei') {
26
return new Huawei()
27
}
28
}

这就是简单工厂模式。

这样能够解决一部分问题。

进一步思考,后续手机的品种会越来越多,小米8,小米9, 小米10,华为 mete10,华为 p40 等等。那这个时候,我们会发现,除了要新增一个类之外,工厂方法 factory 也会持续被更改。

INFO

违背了开闭原则

那我们应该怎么解决这个问题呢?有没有一种方式,能够让工厂方法在后续的迭代过程中,不进行修改?

当然有:最简单的方式如下

code.ts
1
function factory(type) {
2
// window 表示声明的类 挂载的对象,可能是window,可能是global,可能是其他自定义的对象
3
return new window[type]()
4
}

这样处理之后,那么传入的 type 字符串,就必须与类名保持一致。因此在使用时会有一些限制

code.ts
const hw = factory('Huawei')

当然,我们也可以维护一份配置文件,该配置文件就是显式的标明类型字符串与类名的映射关系。

我们可以将这份配置文件,定义在工厂函数的原型对象中。

于是,上面的工厂函数可以演变成为工厂类。并且具备了自己的方法,config 配置文件维护在工厂对象的原型中,被所有实例共享。

code.ts
1
function Factory() {}
2
Factory.prototype.create = function(type) {
3
var cur = this.config[type]
4
if (cur) {
5
return new cur()
6
}
7
}
8
Factory.prototype.config = {}
9
Factory.prototype.setConfig = function(type, sub) {
10
this.config[type] = sub
11
}

之后,每新增一个类,都需要使用工厂对象修改存储在原型对象中的配置

code.ts
1
class Xiaomi5 {
2
constructor() {
3
this.materials = {
4
1: 'xiaomi_material1',
5
2: 'xiaomi_material2',
6
3: 'xiaomi_material3',
7
}
8
}
9
step1() {}
10
step2() {}
11
step3() {}
12
step4() {}
13
}
14
15
new Factory().setConfig('xiaomi5', Xiaomi5)

我们也可以专门手动维护一个单独的模块作为配置文件。这样的方式更直观。

code.ts
1
import Xiaomi from './Xiaomi'
2
import Xiaomi5 from './Xiaomi5'
3
4
export default {
5
xiaomi: Xiaomi,
6
xiaomi5: Xiaomi5
7
}
8
code.ts
1
import config from './config'
2
3
export default function factory(type) {
4
if (config[type]) {
5
return new config[type]()
6
}
7
}

很显然,在代码层面,还可以对类型声明进行优化。

我们分析上面三个类的情况,都是生成手机,所以所有的方法都完全相同。但是因为每一种手机的原材料不一样,因此构造函数里会不一样。利用封装的思维,我们可以将这三个类,合并成为一个类,不同的手机在构造函数中进行判断。

code.ts
1
class PhoneFactory {
2
constructor(type) {
3
if (type == 'xiaomi') {
4
this.materials = {
5
1: 'xiaomi_material1',
6
2: 'xiaomi_material2',
7
3: 'xiaomi_material3',
8
}
9
}
10
if (type == 'iphone') {
11
this.materials = {
12
1: 'iphone_material1',
13
2: 'iphone_material2',
14
3: 'iphone_material3',
15
}
16
}
17
if (type == 'huawei') {
18
this.materials = {
19
1: 'huawei_material1',
20
2: 'huawei_material2',
21
3: 'huawei_material3',
22
}
23
}
24
}
25
step1() {}
26
step2() {}
27
step3() {}
28
step4() {}
29
}
30
31
const xm = new PhoneFactory('xiaomi')
32
const ip = new PhoneFactory('iphone')
33
const hw = new PhoneFactory('huawei')

这种方式的底层思维是将所有的手机抽象成为同一种类型,然后在构造函数时针对不同的细节进行区分。之所以能够这样处理的原因,是因为 Xiaomi,IPhone,Huawei 这几个类高度相似,因此可以抽象成为同一种类型。但是如果只有部分相似,就需要区别对待。

在 jQuery 的封装里,也有同样的场景。例如 jQuery 的构造函数 jQuery.fn.init 中有这样的逻辑判断

code.ts
1
init = jQuery.fn.init = function (selector, context, root) {
2
var match, elem;
3
4
// $(""), $(null), $(undefined), $(false)
5
if (!selector) {
6
return this;
7
}
8
9
// $('.wrapper')
10
if (typeof selector === "string") {
11
12
//...
13
14
// $(DOMElement)
15
} else if (selector.nodeType) {
16
17
// $(function)
18
// Shortcut for document ready
19
} else if (jQuery.isFunction(selector)) {
20
//....
21
}
22
23
return jQuery.makeArray(selector, this);
24
};

为了扩展时,不直接修改对象而是修改配置文件,可以进一步调整一下

code.ts
1
const config = {
2
xiaomi: {
3
1: 'xiaomi_material1',
4
2: 'xiaomi_material2',
5
3: 'xiaomi_material3',
6
},
7
iphone: {
8
1: 'iphone_material1',
9
2: 'iphone_material2',
10
3: 'iphone_material3',
11
},
12
huawei: {
13
1: 'huawei_material1',
14
2: 'huawei_material2',
15
3: 'huawei_material3',
16
}
17
}
18
19
class PhoneFactory {
20
constructor(type) {
21
this.materials = config[type]
22
}
23
step1() {}
24
step2() {}
25
step3() {}
26
step4() {}
27
}
28
29
const xm = new PhoneFactory('xiaomi')
30
const ip = new PhoneFactory('iphone')
31
const hw = new PhoneFactory('huawei')

但是如果这几个类只是部分相似,只有部分接口是一样的,那么就需要区别对象,而不能直接合在一起。同样的方法使用继承的方式来简化

code.ts
1
class Phone {
2
step1() {}
3
step2() {}
4
step3() {}
5
step4() {}
6
}
7
8
class Xiaomi extends Phone {
9
constructor() {
10
this.materials = {
11
1: 'xiaomi_material1',
12
2: 'xiaomi_material2',
13
3: 'xiaomi_material3',
14
}
15
}
16
}
17
18
class IPhone extends Phone {
19
constructor() {
20
this.materials = {
21
1: 'iphone_material1',
22
2: 'iphone_material2',
23
3: 'iphone_material3',
24
}
25
}
26
}
27
28
class Huawei extends Phone {
29
constructor() {
30
this.materials = {
31
1: 'huawei_material1',
32
2: 'huawei_material2',
33
3: 'huawei_material3',
34
}
35
}
36
}
37
38
const config = {
39
xiaomi: Xiaomi,
40
iphone: IPhone,
41
huawei: Huawei
42
}
43
44
function factory(type) {
45
if (config[type]) {
46
return new config[type]()
47
}
48
}
49
50
const xm = factory('xiaomi')
51
const ip = factory('iphone')
52
const hw = factory('huawei')

工厂模式的核心思维在于不直接通过 new 来创建实例,而是使用工厂方法进行一层封装,隐藏实例的创建细节。因此上面提到的许多方式,都是能够基本满足这个特点,那么对应到实践场景中,就需要结合场景选择最适合的方式灵活使用。

专栏首页
到顶
专栏目录