【03】前端常用的设计模式之工厂模式

147 阅读5分钟

工厂模式

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。所谓工厂模式就是将创建对象的过程单独封装。

概述(大白话)

  • 将 new 操作单独封装
  • 遇到 new 时,就要考虑是否使用工厂模式

根据抽象程度的不同可以分为

抽象:将复杂事物的一个或多个共有特征抽取出来的思维过程。

简单工厂模式(Simple Factory)

简单工厂实际不能算作一种设计模式,它引入了创建者的概念,将实例化的代码从应用代码中抽离,在创建者类的静态方法中只处理创建对象的细节,后续创建的实例如需改变,只需改造创建者类即可,

但由于使用静态方法来获取对象,使其不能在运行期间通过不同方式去动态改变创建行为,因此存在一定局限性。

UML 类图

简单工厂模式

代码示例

// src/demo3/a.ts
type fnType = () => void;

interface IProduct {
  doSomething: fnType;
}

// 定义类
class ProductA implements IProduct {
  doSomething() {
    console.log('A');
    // ...
  }
}
class ProductB implements IProduct {
  doSomething() {
    console.log('B');
    // ...
  }
}
class ProductC implements IProduct {
  doSomething() {
    console.log('C');
    // ...
  }
}
/* 简单工厂 */
class SimpleFactory {
  createProduct(type: string): IProduct | null {
    let instance: IProduct | null = null;
    switch (type) {
      case 'A':
        instance = new ProductA();
        break;
      case 'B':
        instance = new ProductB();
        break;
      case 'C':
        instance = new ProductC();
        break;
    }
    return instance;
  }
}

// 使用
const client = new SimpleFactory();
(client.createProduct('A') as IProduct).doSomething();
(client.createProduct('B') as IProduct).doSomething();
export {};
  • IProduct接口:定义要创建的产品对象的接口
  • ProductAProductBProductC产品类:实现产品接口,具有产品接口特性的具体产品
  • SimpleFactory简单工厂:只有一个工厂,通过静态方法createProduct创建具体的产品对象
  • client客户端:客户端有多个,每个客户端可以通过简单工厂来创建具体的产品对象

优缺点

  • 优点:避免直接创建对象
  • 缺点:违背开闭原则,如果需要新增 Product 类,需要在工厂类新增逻辑判断
  • 适用于业务简单,产品固定不会经常改变的情况。

工厂方法模式(Factory Method)

工厂方法模式实际上是简单工厂模式的升级,工厂方法模式定义除了产品接口外,还定义了一个用于创建对象工厂的接口,让工厂子类再去实例化对应的产品类。

UML 类图

工厂方法模式

  • IProduct接口:和简单工厂相同,提供产品对象的接口
  • ProductAProductBproductC:具体类型的产品对象
  • FactoryAFactoryBFactoryC:具体的产品工厂,实现具体的产品对象
  • AbstractFactory:抽象工厂,可以有多个,其中的方法负责返回创建的产品对象
  • Client:使用该模式的客户端

代码示例

// src/demo03/b.ts
type fnType = () => void;

interface IProduct {
  doSomething: fnType;
}

/* 具体产品实现 */
class ProductA implements IProduct {
  doSomething() {
    console.log('ProductA');
    // ...
  }
}
class ProductB implements IProduct {
  doSomething() {
    console.log('ProductB');
    // ...
  }
}

/* 工厂接口 */
interface AbstractFactory {
  createProduct: () => IProduct;
}

/* A 工厂 */
class FactoryA implements AbstractFactory {
  createProduct() {
    return new ProductA();
  }
}

/* B 工厂 */
class FactoryB implements AbstractFactory {
  createProduct() {
    return new ProductB();
  }
}

// 使用
const clientA = new FactoryA().createProduct();
clientA.doSomething();
const clientB = new FactoryB().createProduct();
clientB.doSomething();

export {};

其中最主要的是AbstractFactory类中的createProduct方法,通过这个方法来生成具体产品,这也是为什么叫工厂方法的原因。和简单工厂的静态方法不同,这里是使用的非静态调用方式。而且可以发现,没有了简单工厂中的 switch逻辑判断,相对而言扩展性也要强的多。

优缺点

  • 优点:完全实现开闭原则,实现了可扩展和更复杂的层次结构。明确了职责,具有多态性,适用于任何实体类。
  • 缺点:如果业务增加,会使得系统中类的个数成倍增加,提高了代码的复杂度

抽象工厂模式(Abstract Factory)

是围绕一个超级工厂创建其他工厂。

UML 类图

抽象工厂模式

  • Product1Product2:定义一种类型的产品对象接口
  • Product1AProduct1B等:各种类型的具体产品对象
  • FactoryAFactoryB:具体产品工厂,负责创建该工厂类型下的产品对象
  • AbstractFactory:抽象工厂接口,定义一类产品对象
  • Client:客户端,使用抽象工厂,调用产品对象

代码示例

// src/demo3/c.ts
type fnType = () => void;

/* Product1 类的产品接口 */
interface IProduct1 {
  doSomething: fnType;
}
class Product1A implements IProduct1 {
  doSomething() {
    console.log('Product1A');
    // ...
  }
}
class Product1B implements IProduct1 {
  doSomething() {
    console.log('Product1B');
    // ...
  }
}

/* Product2 类的产品接口 */
interface IProduct2 {
  doSomething: fnType;
}

class Product2A implements IProduct2 {
  doSomething() {
    console.log('Product2A');
    // ...
  }
}
class Product2B implements IProduct2 {
  doSomething() {
    console.log('Product2B');
    // ...
  }
}

/* 工厂接口 */
interface AbstractFactory {
  createProduct1(): IProduct1;
  createProduct2(): IProduct2;
}

/* A 工厂 */
class FactoryA implements AbstractFactory {
  createProduct1() {
    return new Product1A();
  }
  createProduct2() {
    return new Product2A();
  }
}

/* B 工厂 */
class FactoryB implements AbstractFactory {
  createProduct1() {
    return new Product1B();
  }
  createProduct2() {
    return new Product2B();
  }
}

// 使用
const clientA = new FactoryA().createProduct1();
clientA.doSomething();
const clientB = new FactoryB().createProduct2();
clientB.doSomething();

export {};

优缺点

  • 优点:增加分组比较容易,而且能大大减少工厂类的数量
  • 缺点:因为分组,所以分组中的产品扩展就比较困难,比如再新增一个 Product3,就需要改动AbstractFactoryFactoryAFactoryB几乎所有工厂类

应用场景

JQuery

$('div') 和 new JQuery('div')有何区别?左边是工厂模式,如果选择后者

  • 1、书写麻烦,JQuery 的链式操作将成为噩梦
  • 2、一旦 JQuery 名字发生变化,将是灾难性的
declare interface Window {
  $: (seletor: string) => JQuery;
}

class JQuery {
  seletor: string;
  length: number;
  constructor(seletor: string) {
    const domList = Array.prototype.slice.call(document.querySelectorAll(seletor));
    const length = domList.length;
    for (let i = 0; i < length; i++) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      this[i] = domList[i];
    }
    this.seletor = seletor;
    this.length = length;
  }
  append(elem: HTMLElement): JQuery {
    console.log('append: ', elem);
    // ...相关操作
    return this;
  }
  addClass(className: string): JQuery {
    console.log('addClass: ', className);
    // ...相关操作
    return this;
  }
  // ...
}

// 不用工厂模式
const $div = new JQuery('div');
const $p = new JQuery('p');

// 用工厂模式
function $(seletor: string) {
  return new JQuery(seletor);
}

const $div1 = $('div');
const $p1 = $('p');

window.$ = $;

// 伪代码,演示jquery
// const jquery = {
//   seletor: 'div',
//   length: 3,
//   0: div1,
//   1: div2,
//   2: div3
// }

export {};

React.createElement 和 Vue _createElementVNode

createElement

// https://www.babeljs.cn/repl
// createElement前
<div>
  <h1 id={hello}>hi H1</h1>
  <p>{item?.p} abc</p>
</div>;

// createElement后
('use strict');

var _item;
/*#__PURE__*/ React.createElement(
  'div',
  null,
  /*#__PURE__*/ React.createElement(
    'h1',
    {
      id: hello,
    },
    'hi H1',
  ),
  /*#__PURE__*/ React.createElement('p', null, (_item = item) === null || _item === void 0 ? void 0 : _item.p, ' abc'),
);

_createElementVNode

// https://vue-next-template-explorer.netlify.app/
// _createElementVNode前
<div>
  <h1 :id="hello">hi H1</h1>
  <p>{{item?.p}} abc</p>
</div>

// _createElementVNode后
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("h1", { id: _ctx.hello }, "hi H1", 8 /* PROPS */, ["id"]),
    _createElementVNode("p", null, _toDisplayString(_ctx.item?.p) + " abc", 1 /* TEXT */)
  ]))
}

// Check the console for the AST

设计原则验证

  • 构造函数和创建者分离
  • 符合开放封闭原则

项目地址