工厂模式 (Factory Pattern),根据不同的输入返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离。
一、生活实例
顾客,饭店,菜品
在类似场景中,这些例子有以下特点:
- 访问者只需要知道产品名,就可以从工厂获得对应实例;
- 访问者不关心实例创建过程;
二、代码实现
如果你使用过 document.createElement 方法创建过 DOM 元素,那么你已经使用过工厂方法了,虽然这个方法实际上很复杂,但其使用的就是工厂方法的思想:访问者只需提供标签名(如 div、img),那么这个方法就会返回对应的 DOM 元素。
function YuXiangRouSi() { this.type = '鱼香肉丝' } YuXiangRouSi.prototype.eat = function () { console.log(this.type + '香喷喷。。。') } function GongBaoJiDing() { this.type = '宫保鸡丁' } GongBaoJiDing.prototype.eat = function () { console.log(this.type + '好辣。。。') } // 工厂:饭店,根据不同类型生产不同菜品实例 function restaurant(type) { switch (type) { case '鱼香肉丝': return new YuXiangRouSi() case '宫保鸡丁': return new GongBaoJiDing() default: return new Error("没有这个菜品") } } const diancan1 = restaurant('鱼香肉丝'); const diancan2 = restaurant('宫保鸡丁'); const diancan3 = restaurant('小炒回锅肉'); diancan1.eat() diancan2.eat()
es6:
class YuXiangRouSi { constructor() { this.type = '鱼香肉丝' } eat() { console.log(this.type + '香喷喷。。。') } }
这样就完成了一个工厂模式,但是这个实现有一个问题:工厂方法中包含了很多与创建产品相关的过程,如果产品种类很多的话,这个工厂方法中就会罗列很多产品的创建逻辑,每次新增或删除产品种类,不仅要增加产品类,还需要对应修改在工厂方法,违反了开闭原则,也导致这个工厂方法变得臃肿、高耦合。
严格上这种实现在面向对象语言中叫做简单工厂模式。适用于产品种类比较少,创建逻辑不复杂的时候使用。
工厂模式的本意是将实际创建对象的过程推迟到子类中,一般用抽象类来作为父类,创建过程由抽象类的子类来具体实现。JavaScript 中没有抽象类,所以我们可以简单地将工厂模式看做是一个实例化对象的工厂类即可.
// 工厂:饭店,产品:菜品,通过菜谱menu,创建不一样的菜品实例 class Restaurant { constructor() { //菜谱,不同菜单,有不同的特色口味,材料颜色口味各不同 this.menuData = {} //‘YuXiangRouSi’: {cailiao: ['','',''],color:'',eat(){}} 对应 class Menu } // 饭店管理新增菜谱 addMenu(menu, type, message) { if (this.menuData[menu]) { console.log('已经有这个菜谱栏') return } this.menuData[menu] = {type, message} } //客户点菜,饭店炒菜(实例化菜谱) getMenu(menu) { if (!this.menuData[menu]) { return new Error('没有这个菜单') } const { type, message } = this.menuData[menu] return new Menu(type, message) } //饭店移除不要的菜谱 removeMenu(menu) { if (!this.menuData[menu]) return delete this.menuData[menu] } } class Menu { constructor(type, message) { this.type = type this.message = message } eat() { console.log(this.type + ',' + this.message) } } const restaurantFactory = new Restaurant(); restaurantFactory.addMenu('YuXiangRouSi','鱼香肉丝','好香') restaurantFactory.addMenu('GaoBaoJiDing', '宫保鸡丁', '好辣') restaurantFactory.getMenu('GaoBaoJiDing').eat()
三、工厂模式的通用实现
饭店可以被认为是工厂类(Factory),菜品是产品(Product),如果我们希望获得菜品实例,通过工厂类就可以拿到产品实例,不用关注产品实例创建流程。主要有下面几个概念:
- Factory :工厂,负责返回产品实例;
- Product :产品,访问者从工厂拿到产品实例;
注意,由于 JavaScript 的灵活,简单工厂模式返回的产品对象不一定非要是类实例,也可以是字面量形式的对象,所以读者可以根据场景灵活选择返回的产品对象形式。
四、码中的工厂模式
-
Vue/React 源码中的工厂模式
和原生的 document.createElement
类似,Vue 和 React 这种具有虚拟 DOM 树(Virtual Dom Tree)机制的框架在生成虚拟 DOM 的时候,都提供了 createElement
方法用来生成 VNode,用来作为真实 DOM 节点的映射:
// Vue createElement('h3', { class: 'main-title' }, [ createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' } }), createElement('p', { class: 'user-desc' }, '长得帅老的快,长得丑活得久') ]) // React React.createElement('h3', { className: 'user-info' }, React.createElement('img', { src: '../avatar.jpg', className: 'avatar' }), React.createElement('p', { className: 'user-desc' }, '长得帅老的快,长得丑活得久') )
createElement 函数结构大概如下
class Vnode (tag, data, children) { ... } function createElement(tag, data, children) { return new Vnode(tag, data, children) }
可以看到 createElement
函数内会进行 VNode 的具体创建,创建的过程是很复杂的,而框架提供的 createElement
工厂方法封装了复杂的创建与验证过程,对于使用者来说就很方便了。
-
vue-router 源码中的工厂模式
工厂模式在源码中应用频繁,以 vue-router 中的源码为例
// src/index.js export default class VueRouter { constructor(options) { this.mode = mode // 路由模式 switch (mode) { // 简单工厂 case 'history': // history 方式 this.history = new HTML5History(this, options.base) break case 'hash': // hash 方式 this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': // abstract 方式 this.history = new AbstractHistory(this, options.base) break default: // ... 初始化失败报错 } } }
mode 是路由创建的模式,这里有三种 History、Hash、Abstract,前两种我们已经很熟悉了,History 是 H5 的路由方式,Hash 是路由中带 # 的路由方式,Abstract 代表非浏览器环境中路由方式,比如 Node、weex 等;this.history 用来保存路由实例,vue-router 中使用了工厂模式的思想来获得响应路由控制类的实例。
源码里没有把工厂方法的产品创建流程封装出来,而是直接将产品实例的创建流程暴露在 VueRouter 的构造函数中,在被 new 的时候创建对应产品实例,相当于 VueRouter 的构造函数就是一个工厂方法。
如果一个系统不是 SPA (Single Page Application,单页应用),而是是 MPA(Multi Page Application,多页应用),那么就需要创建多个 VueRouter 的实例,此时 VueRouter 的构造函数也就是工厂方法将会被多次执行,以分别获得不同实例。
五、工厂模式的优缺点
工厂模式将对象的创建和实现分离,这带来了优点:
- 良好的封装,代码结构清晰,访问者无需知道对象的创建流程,特别是创建比较复杂的情况下;
- 扩展性优良,通过工厂方法隔离了用户和创建流程隔离,符合开放封闭原则;
- 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流;
工厂模式的缺点:带来了额外的系统复杂度,增加了抽象性;
六、工厂模式的使用场景
那么什么时候使用工厂模式呢:
- 对象的创建比较复杂,而访问者无需知道创建的具体流程;
- 处理大量具有相同属性的小对象;
什么时候不该用工厂模式:滥用只是增加了不必要的系统复杂度,过犹不及。