qrcode

加入前端交流群,阿里、腾讯和京东大佬都在的群里

TypeScript 设计模式之抽象工厂

创建了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 “semlinker”,备注 “1” 。阿里、京东、腾讯的大佬都在群里等你哟。

semlinker/awesome-typescript 1.8K

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。

作为一名 Web 软件开发工程师,在软件系统的设计与开发过程中,我们可以利用设计模式来提高代码的可重用性、可扩展性和可维护性。在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。

工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式。本文阿宝哥将详细介绍抽象工厂模式,不过在介绍该模式之前,我们先来回顾一下简单工厂模式和工厂方法模式。

一、简单工厂模式

1.1 简单工厂模式简介

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

对于刚接触简单工厂模式的小伙伴来说,看到以上的描述可能会觉得有点抽象。因此为了让小伙伴更好地理解简单工厂模式,阿宝哥以用户买车为例,来介绍一下 BMW 工厂如何使用简单工厂模式来生产小汽车。

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂会先判断用户选择的车型,然后按照对应的模型进行生产并在生产完成后交付给用户。接下来,我们来看一下如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

1
2
3
4
5
6
7
8
9
10
11
12
class BMWFactory {
public static produceBMW(model: "730" | "840"): BMW {
if (model === "730") {
return new BMW730();
} else {
return new BMW840();
}
}
}

const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");

在以上代码中,我们定义一个 BMWFactory 类,该类提供了一个静态的 produceBMW() 方法,用于根据不同的模型参数来创建不同型号的车子。看完以上的代码,相信很多小伙伴会觉得很熟悉,因为在一些项目中就使用了简单工厂模式。该模式很简单,在满足以下条件下可以考虑使用简单工厂模式:

  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

了解完简单工厂模式的应用场景,接下来我们来看一下该模式的优缺点。

1.2 简单工厂模式优缺点

1.2.1 优点
  • 将创建实例与使用实例的任务分开,使用者不必关心对象是如何创建的,实现了系统的解耦;
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
1.2.2 缺点
  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,也有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

在一些实际的项目中,为了避免简单工厂模式带来的问题,我们可以考虑使用工厂方法模式。

二、工厂方法模式

2.1 工厂方法模式简介

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车子,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。同样,我们来看一下如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface BMWFactory {
produceBMW(): BMW;
}

class BMW730Factory implements BMWFactory {
produceBMW(): BMW {
return new BMW730();
}
}

class BMW840Factory implements BMWFactory {
produceBMW(): BMW {
return new BMW840();
}
}

const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();

const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();

在以上代码中,我们分别创建了 BMW730Factory 和 BMW840Factory 两个工厂类,然后使用这两个类的实例来生产不同型号的车子。相比前面的简单工厂模式,工厂方法模式通过创建不同的工厂来生产不同的产品。下面我们来看一下工厂方法有哪些优缺点。

2.2 工厂方法优缺点

2.2.1 优点
  • 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,更加符合 “开闭原则”。而简单工厂模式需要修改工厂类的判断逻辑。
  • 符合单一职责的原则,即每个具体工厂类只负责创建对应的产品。而简单工厂模式中的工厂类存在一定的逻辑判断。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
2.2.2 缺点
  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 一个具体工厂只能创建一种具体产品。

这里我们知道在工厂方法模式中,一个具体工厂只能创建一种具体产品。很明显这限制该模式的使用场景,那么我们是否能突破该限制呢?答案是可以的。下面我们来介绍抽象工厂模式。

三、抽象工厂模式

3.1 抽象工厂简介

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。 但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。

在上图中,阿宝哥模拟了用户购车的流程,小王向 BMW 工厂订购了 BMW730,工厂按照 730 对应的模型进行生产并在生产完成后交付给小王。而小秦向 BMW 工厂订购了 BMW840,工厂按照 840 对应的模型进行生产并在生产完成后交付给小秦。接下来,我们来看一下如何使用抽象工厂来描述上述的购车过程。

3.2 抽象工厂实战

1.定义 BMW 抽象类

1
2
3
abstract class BMW {
abstract run(): void;
}

2.创建 BMW730 类(BMW 730 Model)

1
2
3
4
5
class BMW730 extends BMW {
run(): void {
console.log("BMW730 发动咯");
}
}

3.创建 BMW840 类(BMW 840 Model)

1
2
3
4
5
class BMW840 extends BMW {
run(): void {
console.log("BMW840 发动咯");
}
}

4.定义 BMWFactory 抽象工厂

1
2
3
4
abstract class BMWFactory {
abstract produce730BMW(): BMW730;
abstract produce840BMW(): BMW840;
}

5.创建 ConcreteBMWFactory 类

1
2
3
4
5
6
7
8
9
class ConcreteBMWFactory extends BMWFactory {
produce730BMW(): BMW730 {
return new BMW730();
}

produce840BMW(): BMW840 {
return new BMW840();
}
}

6.生产并发动 BMW730 和 BMW840

1
2
3
4
5
6
7
const bmwFactory = new ConcreteBMWFactory();

const bmw730 = bmwFactory.produce730BMW();
const bmw840 = bmwFactory.produce840BMW();

bmw730.run();
bmw840.run();

以上代码运行后的输出结果为:

1
2
[LOG]: BMW730 发动咯 
[LOG]: BMW840 发动咯

通过观察以上的输出结果,我们可以知道我们的 ConcreteBMWFactory 已经可以正常工作了。在 ConcreteBMWFactory 类中,阿宝哥定义了 produce730BMW()produce840BMW() 这两个方法,它们分别用于生产 BMW730 和 BMW840 车子。

看到这里相信有的小伙会有疑问 —— 抽象工厂模式与工厂方法模式有什么区别呢?

抽象工厂模式与工厂方法模式最大的区别在于:工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。 当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。

了解完抽象工厂模式与工厂方法模式的区别,最后我们来一起看一下抽象工厂模式的优缺点。

3.3 抽象工厂优缺点

优点
  • 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
缺点
  • 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。

四、总结

根据模式是用来完成什么工作来划分,这种方式可分为 创建型模式、结构型模式和行为型模式 3 种。本文介绍的工厂方法和抽象工厂属于创建型模式,该模式用于描述 “怎样创建对象”,它的主要特点是 “将对象的创建与使用分离”。

GoF(Gang of Four)中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。其中阿宝哥已经介绍过单例、工厂方法和抽象工厂,在后续的文章中阿宝哥将介绍建造者模式及如何基于建造者设计模式实现以下功能,感兴趣的小伙伴记得关注哟。

1
2
3
4
5
6
let builder: QueryBuilder = new QueryBuilder();

let query: Query = new Query();
builder.query = query;
builder.bool().shouldMatch("lot_number", 307).bool().mustMatch("expiry_date", "September 2020");
console.log(query.toString());

1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在本教程中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的 “四人组”(Gang of Four,GoF)匿名著称。

五、参考资源


欢迎小伙伴们订阅全栈修仙之路,及时阅读 TypeScript、Node/Deno、Angular 技术栈最新文章。

qrcode