工厂模式 (Factory Pattern),根据不同的输入返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离

简单代码实现

如果你使用过 document.createElement 方法创建过 DOM 元素,那么你已经使用过工厂方法了,虽然这个方法实际上很复杂,但其使用的就是工厂方法的思想:访问者只需提供标签名(如 divimg),那么这个方法就会返回对应的 DOM 元素。

我们可以简单来实现一个实例:

当你去外面吃饭点菜的时候,菜单上可能有几十种菜,而你只需要跟服务员说你想吃什么,等会菜就会做好端到你面前,而关于烧菜的过程不需要你去管,你只需要确认服务员上菜的时候准确无误即可。

  1. function restaurant(menu) {
  2. switch (menu) {
  3. case '汉堡包':
  4. return new Hamburger();
  5. case '薯条':
  6. return new Chips();
  7. case '鸡块':
  8. return new ChickenNuggets();
  9. default:
  10. throw new Error('本店没有此道菜')
  11. }
  12. }
  13. class Hamburger {
  14. constructor() {
  15. this.food = '汉堡包';
  16. // TODO 开始制作...
  17. }
  18. }
  19. class Chips {
  20. constructor() {
  21. this.food = '薯条';
  22. // TODO 开始制作...
  23. }
  24. }
  25. class ChickenNuggets {
  26. constructor() {
  27. this.food = '鸡块';
  28. // TODO 开始制作...
  29. }
  30. }

这个实现存在一个问题:工厂方法中包含了很多与创建产品相关的过程,如果产品种类很多的话,这个工厂方法中就会罗列很多产品的创建逻辑,每次新增或删除产品种类,不仅要增加产品类,还需要对应修改在工厂方法,违反了开闭原则,也导致这个工厂方法变得臃肿、高耦合。

严格上这种实现在面向对象语言中叫做简单工厂模式。适用于产品种类比较少,创建逻辑不复杂的时候使用。

工厂模式的本意是将实际创建对象的过程推迟到子类中,一般用抽象类来作为父类,创建过程由抽象类的子类来具体实现。JavaScript 中没有抽象类,所以我们可以简单地将工厂模式看做是一个实例化对象的工厂类即可。关于抽象类的有关内容,可以参看抽象工厂模式。

然而作为灵活的 JavaScript,我们不必如此较真,可以把易变的参数提取出来:

  1. class Restaurant {
  2. constructor() {
  3. this.menu = [
  4. {food: '汉堡包', method/* 做菜方法 */: {}}
  5. ]
  6. }
  7. // 点菜
  8. getMenu(food) {
  9. const foodInfo = this.menu.find(item => item.food === food);
  10. if(!foodInfo) throw new Error('本店没有此道菜')
  11. return new Cooking(food, foodInfo.method);
  12. }
  13. // 增加菜品
  14. addMenu(food, method) {
  15. const foodInfo = this.menu.find(item => item.food === food);
  16. if(foodInfo) throw new Error('已经有这道菜了');
  17. this.menu.push({food, method})
  18. }
  19. // 删除菜品
  20. removeMenu(foo) {
  21. this.menu = this.menu.filter(item => item.food !== foo);
  22. }
  23. }
  24. // 做菜
  25. class Cooking {
  26. constructor(food, method) {
  27. // TODO 开始制作...
  28. this.food = food;
  29. }
  30. }

我们给 Restaurant 类增加了 addMenu/removeMenu 私有方法,以便于扩展。

当然这里如果菜品参数不太一致,可以在 addMenu 时候注册构造函数或者类,创建的时候返回 new 出的对应类实例,灵活变通即可。

通用实现

根据上面的例子我们可以提炼一下工厂模式,饭店可以被认为是工厂类(Factory),菜品是产品(Product),如果我们希望获得菜品实例,通过工厂类就可以拿到产品实例,不用关注产品实例创建流程。主要有下面几个概念:

  • Factory :工厂,负责返回产品实例;
  • Product :产品,访问者从工厂拿到产品实例;

factory.png

  1. // 工厂类
  2. class Factory {
  3. static getInstance(type) {
  4. switch (type) {
  5. case 'Product1':
  6. return new Product1();
  7. case 'Product2':
  8. return new Product2();
  9. default:
  10. throw new Error('当前没有这个产品')
  11. }
  12. }
  13. }
  14. class Product1 {
  15. constructor() {
  16. this.type = 'Product1'
  17. }
  18. }
  19. class Product2 {
  20. constructor() {
  21. this.type = 'Product2'
  22. }
  23. }

由于 JavaScript 的灵活,简单工厂模式返回的产品对象不一定非要是类实例,也可以是字面量形式的对象,所以读者可以根据场景灵活选择返回的产品对象形式。

源码中的工厂模式

Vue/React 源码中的工厂模式

和原生的 document.createElement 类似,Vue 和 React 这种具有虚拟 DOM 树(Virtual Dom Tree)机制的框架在生成虚拟 DOM 的时候,都提供了 createElement 方法用来生成 VNode,用来作为真实 DOM 节点的映射:

  1. // Vue
  2. createElement('h3', { class: 'main-title' }, [
  3. createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' } }),
  4. createElement('p', { class: 'nickname' }, 'OUDUIDUI')
  5. ])
  6. // React
  7. React.createElement('h3', { className: 'main-title' },
  8. React.createElement('img', { src: '../avatar.jpg', className: 'avatar' }),
  9. React.createElement('p', { className: 'nickname' }, 'OUDUIDUI')
  10. )

createElement 函数结构大概如下:

  1. class Vnode (tag, data, children) { ... }
  2. function createElement(tag, data, children) {
  3. return new Vnode(tag, data, children)
  4. }

可以看到 createElement 函数内会进行 VNode 的具体创建,创建的过程是很复杂的,而框架提供的 createElement 工厂方法封装了复杂的创建与验证过程,对于使用者来说就很方便了。

vue-router 源码中的工厂模式

工厂模式在源码中应用频繁,以 vue-router 中的源码为例

代码位置:vue-router/src/index.js

  1. export default class VueRouter {
  2. constructor(options) {
  3. this.mode = mode // 路由模式
  4. switch (mode) { // 简单工厂
  5. case 'history': // history 方式
  6. this.history = new HTML5History(this, options.base)
  7. break
  8. case 'hash': // hash 方式
  9. this.history = new HashHistory(this, options.base, this.fallback)
  10. break
  11. case 'abstract': // abstract 方式
  12. this.history = new AbstractHistory(this, options.base)
  13. break
  14. default:
  15. // ... 初始化失败报错
  16. }
  17. }
  18. }

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 的构造函数也就是工厂方法将会被多次执行,以分别获得不同实例。

优缺点

工厂模式将对象的创建和实现分离,这带来了优点:

  1. 良好的封装,代码结构清晰,访问者无需知道对象的创建流程,特别是创建比较复杂的情况下;
  2. 扩展性优良,通过工厂方法隔离了用户和创建流程隔离,符合开放封闭原则
  3. 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流;

工厂模式的缺点:带来了额外的系统复杂度,增加了抽象性

使用场景

  1. 对象的创建比较复杂,而访问者无需知道创建的具体流程;
  2. 处理大量具有相同属性的小对象;