Fork me on GitHub

js设计模式Notes--工厂模式(1)

设计模式之道

SOLID原则

  • 单一功能原则(Single Responsibility Principle)
  • 开放封闭原则(Opened Closed Principle)
  • 里式替换原则(Liskov Substitution Principle)
  • 接口隔离原则(Interface Segregation Principle)
  • 依赖反转原则(Dependency Inversion Principle)

JS里面主要还是围绕着“单一功能”和“开放封闭”来展开。

设计模式的核心思想–封装变化

在实际开发中,不发生变化的代码可以说是不存在的。我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定

这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码,它可以经得起变化的考验。而设计模式出现的意义,就是帮我们写出这样的代码。

创建型设计模式

构造器

例如在动态构建公司员工数据(在员工人数较多的时候)的时候可以使用JS里面的构造函数:

1
2
3
4
5
function User(name,age,career){
this.name = name;
this.age = age;
this.career = career;
}

这里的User本质上就是一个构造器。这里使用的是ES5的构造函数(ES6里面的class本质上还是构造函数,class只是一个语法糖而已)

有了这个构造器,在进行员工信息录入的时候,就不用手动去对象里面创建字面量了,自动让程序从数据库里面获取到员工的姓名,年龄等字段,然后来一个简单的调用:

1
const user = new User(name,age,carrer);

那么这个地方就设计到了构造器模式,这个模式就比较简单了,看上去其实就是一个new的过程。

但这里主要需要理解的是在构造过程中,谁发生了变化,而谁没有变化。

在使用构造器模式的时候,本质上是去抽象了每个对象实例的变与不变。而使用工厂模式则就是抽象不同构造函数(类)之间的变与不变

简单工厂模式–理解“变”与“不变”

接着上面的例子扯,如果要对员工的title进行一个区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Coder(name,age){
this.name = name;
this.age = age;
this.career = 'coder';
this.work = ['fix bug','deploy','coding'];
}
function PM(name,age){
this.name = name;
this.age = age;
this.career = 'PM';
this.work = ['design picture','open issue'];
}

这样的话就会出现多个类别,所以这里我们需要从数据库里面拿到数据之后,手动判断title之后让,然后手动分配构造器?这也就成类一个“变”的过程了,我们这里封装一个函数出来:

1
2
3
4
5
6
7
8
9
10
11
function Factory(name,age,career){
switch(career){
case: 'coder':
return new Coder(name,age);
break;
case: 'PM':
return new PM(name,age);
break;
...
}
}

但是这么一来。随着不同title人员的增加,这里case里面的代码数目也会跟着增加。

这里我们就又需要思考“变”与“不变”之间的关系了。

本质上title不同的两类人,他们不同的共性仅仅是字段name,age,career,work取值不同而已,同时work字段会随着career字段的取值不同而变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function User(name,age,career,work){
this.name = name;
this.age = age;
this.career = career;
this.work = work;
}
function Factory(name,age,career){
let work;
switch(career){
case 'coder':
work = ['fix bug','deploy','coding'];
break;
case 'pm':
work = ['design picture','open issue'];
break;
case 'xxx':
// others
...
return new User(name,age,career,work);
}
}

这样我们就不用去思考我们拿到的每组数据是什么工种,我应该怎么分配构造函数,更不用手写无数的构造函数–Factory函数已经帮我们完成了一切,我们只需要无脑的去传参数即可。

那么这样看来,工厂模式的目的本质上就是为了简化我们的构造过程,使我们可以无脑传参即可。

抽象工厂模式–理解开放封闭原则

抽象工厂模式在很长一段时间内都被认为是Java/C++这一类强类型动态语言的专利,因为用这些语言创建对象的时候需要时刻考虑类型之间的解耦,以便于该对象日后可以变现出多态性。但JavaScript作为弱类型语言具有天然的多态性,基本上不需要考虑类型耦合带来的问题。而目前的JavaScript语法也不支持抽象类的直接实现,只能通过模拟来实现。

在实际的业务中,我们往往面对的复杂度并非数个类,一个工厂就能解决的,可能需要多个。

就上节工厂函数的例子来看,工厂函数是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Factory(name,age,career){
let work;
switch(career){
case 'coder':
work = ['fix bug','deploy','coding'];
break;
case 'pm':
work = ['design picture','open issue'];
break;
case 'boss':
work = ['have a meeting','read']
break;
case 'xxx':
// others
...
return new User(name,age,career,work);
}
}

实际上这些代码是经不起推敲的,因为在员工系统里面Boss和普通员工是有明显的区别的。两者基本很难在一套系统里面共存下来,因为本身所具备的权限是不一样的。

那么接下来我们呢应该怎么做呢?去修改Factory的函数体,在里面新增管理层的判断和处理逻辑吗?从处理逻辑来看是没有问题的,但其实从全局来看这是在挖坑,因为员工里面可能还有外包群体和实习生之类但职级差别,这样每次我们需要去新增新的工种的时候,都需要直接对Factory这个函数体本身去进行一个修改,这样会使得Factory变得异常庞大,相当于是在对系统进行挖坑了。这样其他人在维护或者测试在测试的时候很难对这个Factory下手。导致这一系列问题的罪魁祸首还是因为—没有遵循开放封闭原则

我们再次回顾一下开放封闭原则的内容:对拓展开放,对修改封闭。确切而言:是软件实体可以拓展,但是不可以修改。像上面就是在疯狂修改而不是在拓展。

抽象工厂模式

可能上面有些概念对我们来说有些抽象,现在我们重新来进行一个示例:

大家都知道一部手机是由OSHardWare组成。所以一家工厂想实现对手机对量产,那么肯定是既要准备好了操作系统,也要有硬件。考虑到这两者背后也存在不同到厂商,而我现在并不知道我到下一个生产线具体想生产一个怎么样到手机,只知道一个手机主要由这两者组成,因此我们先使用一个抽象类来约定这台手机的基本组成:

1
2
3
4
5
6
7
8
9
10
class MobilePhoneFactory {
// 操作系统接口
createOS () {
throw new Error(`抽象工厂方法不能直接调用,需要重写`);
}
// 提供硬件的接口
createHardWare () {
throw new Error(`抽象工厂方法不能直接调用,需要重写`);
}
}

上面这个类除了约定流水线手机的通用能力之外,啥也不能干了。如果你尝试让他去干点啥,比如new一个实例出来,并且去调用它的实例方法。它还会报错。在抽象工厂模式里面,上面这个类其实就是我们食物链顶端最大的Boss-AbstractFactory(抽象工厂)。

抽象工厂并不会干活,具体工厂(ConcreteFactory)来干活,当我们明确了生产方案,明确了某一条流水线具体要生产怎么样的手机之后,就可以化抽象为具体,比如我现在想要生产一个专门生产Android系统 + 高通硬件的手机的生产线,我给这类手机型号起名为FakeStar,那我就可以为这个手机定制一个工厂:

1
2
3
4
5
6
7
8
9
10
11
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS () {
// 提供android系统实例
return new AndroidOS();
}
createHardWare () {
// 提供高通硬件实例
return new QualcommHardWare();
}
}

这里我们在提供安卓系统的时候,调用了两个构造函数:AndroidOSQualcommHardWare,它们分别用于生产具体的操作系统和硬件系统实例。像这种被我们拿出来用于new出具体对象的类,叫做具体产品类(ConcreteProduct)。具体产品往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如安卓类和苹果系统类,它们都是操作系统,都有着可以操控手机硬件系统这样一个最基本的功能。因此我们可以使用一个抽象产品类来声明这一类产品应该具有的基本功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义操作系统这一类产品的抽象类
class OS {
controlHardWare () {
throw new Error(`抽象产品方法允许直接调用,需要重写`);
}
}
// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare () {
console.log('我会用android的方式去操作硬件');
}
}
class IOS extends OS {
controlHardWare () {
console.log('我会用苹果的方式去操作硬件');
}
}
....

对于硬件类产品也是同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取“根据命令运转”这个共性
operateByOrder () {
throw new Error(`抽象产品方法不允许直接调用,需要重写`);
}
}
// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder () {
console.log(`我会用高通的方式去运转`);
}
}
class MiWare extends HardWare {
operatorByOrder () {
console.log(`我会用小米的方式去运转`);
}
}

这样一来,当我们需要生产一台FakeStar手机时候,我们只需要这样做:

1
2
3
4
5
6
7
8
9
10
// 这是我的手机
const myPhone = new FakeStarFactory();
// 让它拥有系统
const myOS = myPhone.createOS();
// 让它拥有硬件
const myHardWare = myPhone.createHardWare();
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare();
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder();

那么关键时刻来了–假如有一天,FakeStar过气了,我们需投入一款新机进入市场,这时候怎么办?我们是不是需要对抽象工程MobilePhoneFactory做任何修改,只需要拓展它的种类:

1
2
3
4
5
6
7
8
class newStarFactory extends MobilePhoneFactory {
createOS () {
// 操作系统实例化代码
}
createHardWare () {
// 硬件实现代码
}
}

这个操作,对原有对系统不会产生任何潜在对影响,所谓对”对拓展开放,对修改封闭”就这么圆满的实现了。前面我们之所以要实现抽象产品类,也是相同的道理。

总结

现在我们可以对比一下抽象工厂和简单工厂的思路,思考一下:它们之间又哪些异同?
它们的共同点在于,都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度。在简单工厂的使用场景里面,处理的对象是类,并且是一些非常好对付的类–它们的共性容易抽离,同时因为逻辑本身比较简单,故而不苛求代码可拓展性。抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:

  • 抽象工厂(抽象类,它不能用于生成具体的实例)。
  • 具体工厂(用于生成产品族里的一个具体的产品)。
  • 抽象产品(抽象类,他不能用于生成具体实例)。
  • 具体产品(用于生产产品族里的一个具体产品所依赖的更细粒度的产品)。

抽象工厂的定义,主要是围绕一个超级工厂去构建其他的工厂

-------------本文结束感谢您的阅读-------------