【JavaScript 设计模式】构造器模式和工厂模式
前言
1. 创建型模式封装了创建对象过程中的变化,它做的事情就是`将创建对象的过程抽离`
2. 结构型模式封装的是`对象之间组合方式的变化`,目的在于灵活地表达对象间的配合与依赖关系
3. 行为型模式则是`将对象千变万化的行为进行抽离`,确保我们能够更安全、更方便地对行为进行更改
面试官常常会问:你知道哪些设计模式?有没有在实战中使用过?看完这个系列,你就可以在面试官侃侃而谈。
设计模式的基本原则:软件实体(类、模块、函数)可以扩展,但是不可修改。
本文是这个系列的第一篇文章,主要学习的是工厂模式,将按照从易到难的顺序,从构造器模式到简单工厂模式,再到抽象工厂模式进行学习。
设计模式
为什么会出现设计模式?
我们开发一个业务,随着业务的不断迭代和扩展,代码量会变得无比庞大,逻辑也会变得非常复杂。这一点相信大家在工作中已经深有体会,如果不使用一些方法进行控制,那么最终会变得无法维护并且不可扩展。
而设计模式就是用来解决这些问题的,设计模式通过将变与不变分离,确保变化的部分灵活、不变的部分稳定,来帮助我们写出“健壮”的代码,保证代码的可扩展性和高维护性。
JavaScript 设计模式的原则
在JavaScript 设计模式中,主要用到的设计模式基本都围绕“单一功能”和“开放封闭”这两个原则来展开。接下来让我们了解一下这两个原则在设计模式中的实践。
构造器模式
需求:录入新员工信息
这个模式大家是非常熟悉的,我们平时使用的构造函数就是应用了构造器模式。
像 User 这样当新建对象的内存被分配后,用来初始化该对象
的特殊函数,就叫做构造器。
function User(name , age, career) {
this.name = name
this.age = age
this.career = career
}
在使用构造器模式的时候,我们本质上是去抽象了每个对象实例的变与不变
。每个对象都具备 name
、age
、career
这些属性,是不变的;而name、age、career 各自的值是不同的,这是变化的。
构造器模式和构造函数的命名是非常类似的,那么到底是先有了构造器模式,才有了构造函数,还是有了构造函数之后才有了构造器的模式。这个问题就像是先有鸡还是先有蛋的问题,谜底是什么我们不得而知,有兴趣的小伙伴可以自己去探索一下。
构造器模式和工厂模式本质还是相同的。构造器模式解决的是多个对象实例的问题,工厂模式解决的是多个类的问题。 构造器处理的是对象实例的变与不变,工厂模式处理的是构造函数的变与不变。
工厂模式
需求:为不同的工种添加不同的职能
工厂模式其实就是将创建对象的过程单独封装。使用工厂模式时,我们要做的就是去抽象不同构造函数(类)之间的变与不变。
应用场景:在使用构造函数的地方,我们就应该想到工厂模式;在写了大量构造函数、调用了大量的 new
的时候。
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 = ['写代码','写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...
return new User(name, age, career, work)
}
使用工厂模式 1.0 写出来的代码依然有局限性,随着业务的推移,员工职责可能会频繁变动和修改,那么频繁在这里进行改动对于整个团队都是不友好的。
这个时候就需要用到工厂模式 2.0 了,把每个不同的模块都独立出来,各自负责自己的功能,互不影响。这样就可以保证软件的高扩展性和可维护性。
抽象工厂模式
大家可以结合我画的这幅图进行阅读,更便于理解
大家知道一部智能手机的基本组成是操作系统(OS)和硬件(HardWare)组成。假设我们现在是一个手机厂商,那么我们就需要有生产操作系统的厂商和生产硬件的厂商,才可以量产手机。
class MobilePhoneFactory {
// 提供操作系统的接口
createOS() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}
上面这个类的功能只有一个,就是约定手机流水线的通用能力。如果你想要 new 一个 MobilePhoneFactory
实例,并尝试调用它的实例方法,它会提醒你“我是 Boss,不是让你用来 new 一个实例的”。在抽象工厂模式里,上面这个类就处于我们食物链最顶端的位置,我们称之为 AbstractFactory
(抽象工厂)。
说完抽象工厂,再来说一下抽象工厂的下游——具体工厂(ConcreteFactory
)。抽象工厂已经规划好了公司的流水线,那么具体工厂就是用来实现具体的功能,生成各自的软件和硬件。比如我现在想要一个专门生产 Android 系统 + 高通硬件的手机的生产线,我们给这类手机型号起名叫 FakeStar,那我就可以为 FakeStar 定制一个具体工厂:
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}
这里我们的 FakeStar 工厂调用了两个构造函数:AndroidOS 和 QualcommHardWare,它们分别用于生成具体的操作系统和硬件实例。像这种被我们拿来用于 new 出具体对象的类,叫做具体产品类(ConcreteProduct
)。具体产品类往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如安卓系统类和苹果系统类,它们都是操作系统,都有可以操控手机硬件系统基本的功能。因此我们可以用一个抽象产品(AbstractProduct
)类来声明这一类产品应该具有的基本功能。(这里的具体产品类和抽象产品类也是这个设计模式的一部分,下文会有解释的)
// 定义操作系统这类产品的抽象产品类
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS {
controlHardWare() {
console.log('我会用🍎的方式去操作硬件')
}
}
// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
硬件类产品同理:
// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}
class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
好了,手机流水线和软件硬件都准备好了,当我们需要生产一台FakeStar手机时,只需要这样做:
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()
关键时刻来了——假如有一天,FakeStar过气了,我们需要生产一款新机投入市场,这时候我们不需要对抽象工厂MobilePhoneFactory做任何修改,只需要拓展一条新的手机生产线即可。
class newStarFactory extends MobilePhoneFactory {
createOS() {
// 操作系统实现代码
}
createHardWare() {
// 硬件实现代码
}
}
我们新的流水线对原有的系统不会造成任何潜在影响,所谓的“对拓展开放,对修改封闭”就这么圆满实现了。前面我们之所以要实现抽象产品类,也是同样的道理。
总结
同样是工厂模式,抽象工厂和简单工厂的思路有哪些异同呢?
两者的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于应用场景的复杂度。
- 在简单工厂的使用场景里,处理的对象是一些逻辑简单,共性明显的类,故而不必苛求代码可扩展性。
- 但在抽象工厂是用来处理那些逻辑复杂,并且有很多种扩展性可能性的类,复杂多变的场景使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分。
所以抽象工厂模式就有了这样的四个关键角色:
- 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个(大家可以想象我们的手机厂后来被一个更大的厂收购了,这个厂里除了手机抽象类,还有平板、游戏机抽象类等等),每一个抽象工厂对应的这一类的产品,被称为“产品族”。
- 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。
- 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
- 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如我们上文中具体的一种操作系统、或具体的一种硬件等。
学习设计模式也跟使用抽象工厂模式一样,当前对于我们的提升可能并不是很大;但是等到我们负责的业务越来越复杂,接手的工作也越来越多的时候,设计模式的重要性就会逐渐凸显出来。(怎么听起来像画饼???人只愿意相信自己愿意相信的东西,我相信学习设计模式会对我以后的职业生涯有所帮助,所以我就学习了)
转载自:https://juejin.cn/post/7144369545216524301