Vue/React都在用的设计模式: 工厂模式

1,364 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

设计模式两个词应该分开读,先有设计,后有模式。

  • 设计:设计原则,设计思想
  • 模式:前辈总结出来的固定的套路

为何需要设计?

因为软件规模变大,甚至是一个系统集群,需要先设计,后开发,否则就乱掉。

为何需要模式?

可套用前人经验,降低设计和沟通的成本。

通俗来说,设计(仅指编程设计)就是按照哪一种思路或者标准来实现功能。同样的功能,不同的设计思想都能用不同的方式来实现,前期效果可能一样,但是随着产品功能的增加和扩展,设计的作用才会慢慢的显示出来

五大设计原则

S O L I D 五大设计原则

  • S 单一职责原则
  • O 开放封闭原则
  • L 李氏置换原则
  • I 接口独立原则
  • D 依赖导致原则

相对于前端来说,重点关注单一职责原则开放封闭原则就可以了。

  • 单一职责原则: 一个程序只做好一件事,如果功能过于复杂就拆分开,每个部分保持独立。

  • 开放封闭原则: 对修改封闭,对扩展开放,这是软件设计的终极目标。即要设计一种机制,当需求发生变化时,根据这种机制扩展代码,而不是修改原有的代码。

以常见的 Promise 来解释一下前两个原则。

// 加载图片
function loadImg(src: string) { 
    const promise = new Promise<HTMLImageElement>((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => { 
            resolve(img)
        }
        img.onerror = () => { 
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}

const src = 'https://www.imooc.com/static/img/index/logo_new.png'

const result = loadImg(src)
result.then((img: HTMLImageElement) => {
    console.log('img.width', img.width)
    return img
}).then((img: HTMLImageElement) => {
    console.log('img.height', img.height)
}).catch((err) => {
    console.log(err)
})
  • 单一职责原则:每个then中的逻辑只做好一件事,如果要做多个就用多个then
  • 开放封闭原则:如果这个需求要修改,那去扩展then即可,现有的逻辑不用修改,即对扩展开放、对修改封闭

其实 S 和 O 是相符现成的,相互依赖。开放封闭原则的好处不止于此,从整个软件开发流程看,减少现有逻辑的更改,也会减少测试的成本。比如,你如果更改了其中的一个模块,那么测试的时候只需要测试更改的模块即可,如果没有遵循S/O原则,你更改的代码会影响全部,那么测试的时候就需要测试所有的功能。

前端常用的设计模式

前端最常用的设计模式有如下几种,本文主要讲解工厂模式。

  • 工厂模式
  • 单例模式
  • 原型模式
  • 装饰器模式
  • 代理模式
  • 观察者模式
  • 迭代器模式

工厂模式

简单版本的工厂模式

class Product {
  name: string
  constructor(name: string) {
    this.name = name
  }
  fn1() {
    console.log('product fn1')
  }
  fn2() {
    console.log('product fn2')
  }
}

class Creator {
  create(name: string): Product {
    return new Product(name)
  }
}

const creator = new Creator()
const p1 = creator.create('p1')
const p2 = creator.create('p2')

image.png

标准版本的工厂函数

interface IProduct {
  name: string
  fn1: () => void
  fn2: () => void
}

class Product1 implements IProduct {
  name: string
  constructor(name: string) {
    this.name = name
  }
  fn1() {
    console.log('product fn1')
  }
  fn2() {
    console.log('product fn2')
  }
}

class Product2 implements IProduct {
  name: string
  constructor(name: string) {
    this.name = name
  }
  fn1() {
    console.log('product fn1')
  }
  fn2() {
    console.log('product fn2')
  }
}

class Creator {
  create(type: string, name: string): IProduct {
    if (type === 'p1') {
      return new Product1(name)
    }
    if (type === 'p2') {
      return new Product2(name)
    }
    throw new Error('valid type')
  }
}

image.png

这样写的工厂模式有什么好处呢?

  • 符合依赖倒置原则,即依赖于抽象的接口,而不是依赖于具体的类。create方法返回的值类型是接口IProduct,而不是具体的类Product1或者Product2,这样的好处是当我们进行扩展的时候,比如扩展了Product3或者Product4,那就不需要修改create函数的返回值了。

  • 处理 new 的逻辑放在create函数里面,如果写在外面,每次创建的时候都要进行 if else判断,代码非常的冗余。

5 大设计原则中,最重要的就是:开放封闭原则,对扩展开放,对修改封闭。工厂模式是如何满足开发封闭原则的呢?

工厂和类分离解耦

现在你要得到一个汉堡,你是跟服务员要(买)一个,还是自己动手做一个?这个问题,服务员就是工厂方法,而动手做一个其实就是new A()

可以扩展多个类 我们定义好了IProduct接口,这样就可以扩展出很多的类。

工厂的创建逻辑也可以自由扩展

在工厂函数里面,我们根据用户传入的参数可以创建不同的类。

遇到 new class 时,考虑工厂模式

工厂模式的使用场景

jQuery $('div')

// 注意是大写的Window
declare interface Window { 
  $: (selector: string) => JQuery
}

class JQuery {
  selector: string
  length: number
  constructor(selector: string) {
    const domArr = Array.prototype.slice.call(
      document.querySelectorAll(selector)
    )
    const length = selector.length
    for (let i = 0; i < length; i++) {
      // @ts-ignore
      this[i] = domArr[i]
    }
    this.selector = selector
    this.length = length
  }

  append(ele: HTMLElement): JQuery {
    // ...
    return this
  }
  addClass(key: string, value: string): JQuery {
    return this
  }
}

window.$ = (selector: string) => {
  return new JQuery(selector)
}
  • window下面是没有$,那么需要我们给Window这个接口添加一个$的声明
declare interface Window { 
  $: (selector: string) => JQuery
}
  • 如果开放给用户的不是$,然后让用户自己去new JQuery(selector),带来的问题:
    • 不方便链式操作,如$('div').append($('#p1'))
    • 不宜将构造函数暴露给用户,尽量高内聚、低耦合

Vue _createElementVNode

我们把下面这段代码复制到在线编译 vue-next-template-explorer.netlify.app/这个网站里面

<div>
  <span>静态文字</span>
  <span :id="hello" class="bar">{{ msg }}</span>
</div>

编译后的代码:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "静态文字"),
    _createElementVNode("span", {
      id: _ctx.hello,
      class: "bar"
    }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

可以看到,会编译出很多 _createElementVNode JS 代码,这些函数就是工厂函数,用于创建 vnode,里面主要逻辑就是return new VNode()

React createElement

在 React 中使用 JSX 语法,我们把下面代码复制到在线编译 www.babeljs.cn/repl

const profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>

这是一种语法糖,编译之后就会是

// 返回 vnode
const profile = React.createElement("div", null,
    React.createElement("img", { src: "avatar.png", className: "profile" }),
    React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);

其实React.createElement也是一个工厂,模拟代码

class Vnode(tag, attrs, children) {
    // ...省略内部代码...
}
React.createElement =  function (tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}