设计模式在前端开发中的实践(十三)——工厂模式

工厂模式

工厂模式,是前端开发中的高频设计模式。

不知道大家在实际开发中遇到过这样的灾难场景没有,本来一个类的参数是参数列表的形式(假设有很多个参数),但是在CodeReview的时候,领导觉得使用参数列表的形式,维护的同学在使用的时候可能会把顺序搞颠倒。然后,我们就有需求将参数列表的形式改造成基于对象的形式(Key-Value形式维护可以显著的降低出问题可能性),此刻搜一下项目里面的引用,Wow~,几十个引用,此刻的心情可能是无比的沮丧...

我不知道我举的这个例子有没有说服力,但是可以肯定的一点是,代码若是用到多少处就改多少处,相比用到多少处,仅仅改动一次,这种维护的代价肯定是无法相提并论的,所以这就是工厂模式存在的意义。

1、基本概念

工厂模式是一种创建型设计模式,它提供了一种抽象的方式来创建对象,使得客户端代码无需关心对象的具体实现,而是只需要知道如何创建对象即可。

工厂模式通常包含一个工厂类,它的职责是创建对象,同时隐藏了对象的创建细节。客户端代码只需要通过工厂类的方法来获取需要的对象即可,而不必知道对象是如何创建的。

工厂模式的优点在于它可以帮助客户端代码避免直接依赖具体的类,从而使得代码更加灵活和可维护。它也支持更好的解耦和更高的内聚性。

工厂模式包括简单工厂模式工厂方法模式抽象工厂模式在实际开发中,最常用的也是简单工厂模式

简单工厂模式:一个工厂类负责创建多个产品类的实例。这些产品类通常具有共同的父类或接口。简单工厂模式通过在工厂类中添加条件判断语句来确定要创建的对象类型。

UML类图如下:

基于这个UML类图所描述的代码设计,外界只需要传入想要生产的操作类类型,工厂负责产出相应的操作类实例,将类的创建收口到了一处,当这些操作类面临调整的时候,仅仅只需要调整一下工程方法内容的代码即可,高效,安全可控。

工厂方法模式:定义一个抽象的工厂类,该工厂类负责定义一个创建对象的接口,而其子类则负责具体实现。客户端代码只需要调用工厂类的方法来获取需要的对象,而不必知道具体的对象类型。

UML类图如下:

上述UML图看起来好像挺复杂,不过,如果我们只看一个就觉得简单了。

假设目前我们只需要加法相关的操作,加法类继承运算类,加法工厂继承自工厂类,加法工厂依赖加法类(因为加法工厂只负责生产加法类的实例),是不是一下就觉得简单了许多?

因为某些场景下,在调用部分已经明确的知道需要的操作是什么,但是我们仍然想保持以抽象的方式创建对象,这个时候使用工厂方法模式就非常合适了。

抽象工厂模式:抽象工厂模式是工厂方法模式的扩展,它支持创建多个产品族,每个产品族可以包含多个产品类。抽象工厂模式定义了多个工厂方法,每个工厂方法负责创建一个产品族中的一组相关对象。客户端代码只需要调用工厂类的方法来获取需要的对象即可。

UML类图如下:

在某些时刻,由于我们有复杂的需求,比如我们代码跨平台(一套代码同时跑AndroidiOS平台),简单工厂模式拿到的是一个一个具像化的业务类,我们不可能把跨平台的处理放到一个业务类里面去以方法名来区分(当然也不可能直接在业务里写平台的判断代码,这样任何时候执行的时候都需要判断一次),这样设计对于使用者来说心智负担太重了,正常的设计是将针对不同的平台的业务逻辑处理抽离到一个类里面去,根据环境决定加载哪个类,这个些类都一套规格(体现在一致的API),对于调用者来说就无关平台了,我们可以直接在程序的入口就知道当前的环境就确定使用某个平台的业务逻辑处理类。像这种一次抽象不够,需要再次抽象的场景就是抽象工厂模式的绝佳使用场景。

在上述UML所描述的内容中,我们是可以在一开始就知道我们当前使用的是那种数据库连接类型(保持了能够支持切换数据库的能力),而业务侧无感知。

2、代码示例

以下是简单工厂模式的代码示例,组件类都有相应的规格(体现在实现Component接口),用户根据传递参数控制工厂生成对应的组件实例。

ts 复制代码
// 简单工厂模式
interface Component {
  render(): void;
}

class Button implements Component {
  constructor(private text: string) {}
  render() {
    console.log(`Rendering button: ${this.text}`);
  }
}

class Input implements Component {
  constructor(private placeholder: string) {}
  render() {
    console.log(`Rendering input: ${this.placeholder}`);
  }
}

class ComponentFactory {
  static create(type: string, props: Record<string, any>): Component {
    switch (type) {
      case "button":
        return new Button(props.text);
      case "input":
        return new Input(props.placeholder);
      default:
        throw new Error(`Type ${type} is not supported`);
    }
  }
}

// 创建按钮组件
const button = ComponentFactory.create("button", { text: "Click me!" });
button.render();

以下是工厂方法模式的代码示例,在某个业务场景,我确实需要用到一个Input组件,但是不太明确将来对Input组件是否会进行调整,为了不影响业务侧,所以此处就不采用硬编码的方式,将来若对Input有调整,只需要调整工厂方法产出的内容。

js 复制代码
// 工厂方法模式
interface Component {
  render(): void;
}

class Button implements Component {
  constructor(private text: string) {}
  render() {
    console.log(`Rendering button: ${this.text}`);
  }
}

class Input implements Component {
  constructor(private placeholder: string) {}
  render() {
    console.log(`Rendering input: ${this.placeholder}`);
  }
}

abstract class ComponentFactory {
  abstract create(props: Record<string, any>): Component;
}

class ButtonFactory extends ComponentFactory {
  create(props: Record<string, any>) {
    return new Button(props.text);
  }
}

class InputFactory extends ComponentFactory {
  create(props: Record<string, any>) {
    return new Input(props.placeholder);
  }
}

// 创建输入框组件
const inputFactory = new InputFactory();
const input = inputFactory.create({ placeholder: 'Please input' });
input.render();

以下是抽象工厂模式的示例,目前暂定使用ElementUI,但是将来的某一天想替换成别的组件库,抽象工厂为我们多加入了这层抽象,所以可以应对未来组件库的替换。

js 复制代码
// 抽象工厂接口
interface UIComponentFactory {
  createButton(): ButtonComponent;
  createInput(): InputComponent;
}

// 抽象按钮组件
interface ButtonComponent {
  render(): void;
  onClick(handler: () => void): void;
}

// 抽象输入框组件
interface InputComponent {
  render(): void;
  onInput(handler: (value: string) => void): void;
}

// 具体工厂:Bootstrap 组件工厂
class BootstrapUIComponentFactory implements UIComponentFactory {
  createButton(): ButtonComponent {
    return new BootstrapButtonComponent();
  }

  createInput(): InputComponent {
    return new BootstrapInputComponent();
  }
}

// 具体工厂:Material 组件工厂
class MaterialUIComponentFactory implements UIComponentFactory {
  createButton(): ButtonComponent {
    return new MaterialButtonComponent();
  }

  createInput(): InputComponent {
    return new MaterialInputComponent();
  }
}

// 具体按钮组件:Bootstrap 按钮
class BootstrapButtonComponent implements ButtonComponent {
  render() {
    console.log("Rendering Bootstrap button component");
  }

  onClick(handler: () => void) {
    console.log("Attaching onClick event for Bootstrap button component");
  }
}

// 具体按钮组件:Material 按钮
class MaterialButtonComponent implements ButtonComponent {
  render() {
    console.log("Rendering Material button component");
  }

  onClick(handler: () => void) {
    console.log("Attaching onClick event for Material button component");
  }
}

// 具体输入框组件:Bootstrap 输入框
class BootstrapInputComponent implements InputComponent {
  render() {
    console.log("Rendering Bootstrap input component");
  }

  onInput(handler: (value: string) => void) {
    console.log("Attaching onInput event for Bootstrap input component");
  }
}

// 具体输入框组件:Material 输入框
class MaterialInputComponent implements InputComponent {
  render() {
    console.log("Rendering Material input component");
  }

  onInput(handler: (value: string) => void) {
    console.log("Attaching onInput event for Material input component");
  }
}

// 客户端代码
const bootstrapFactory = new BootstrapUIComponentFactory();
const button1 = bootstrapFactory.createButton();
const input1 = bootstrapFactory.createInput();
button1.render();
input1.render();

const materialFactory = new MaterialUIComponentFactory();
const button2 = materialFactory.createButton();
const input2 = materialFactory.createInput();
button2.render();
input2.render();

3、在前端开发中的实践

3.1 简单工厂模式处理数据库的连接

简单工厂模式在前端开发中太常见了,为大家举一些常见的例子。

以下是typeorm处理不同数据库连接的实践

ts 复制代码
/**省略了部分代码
 */
export class DriverFactory {
  /**
   * Creates a new driver depend on a given connection's driver type.
   */
  create(connection: DataSource): Driver {
    const { type } = connection.options;
    switch (type) {
      case "mysql":
        return new MysqlDriver(connection);
      case "mssql":
        return new SqlServerDriver(connection);
      default:
        throw new MissingDriverError(type, [
          "mssql",
          "mysql"
        ]);
    }
  }
}

3.2 简单工厂模式创建Virtual DOM

其实在Vue(或React)中,我们每天都在使用简单工厂模式,如果觉得我这句话有问题的人,一定是没有研究或框架底层的处理过程的人。

Vue中有一个方法,叫做$createElement(即render函数的h入参),我们可以用它得到一个一个不同类型的VNode,因为有了这个方法,所以<component />组件才能支持根据不同的参数创建不同类型的组件。

下面代码是我最近在一个组件库里面的实现,榜单需要根据参数创建不同的榜单类型,因为不同的榜单类型需要处理的业务逻辑,差异较大,为了降低业务逻辑处理的复杂度,我将其封装到了对应的组件里面去,但是对于使用者来说,仅仅只需要一个type参数就控制榜单的类型。

jsx 复制代码
import NormalRank from "./NormalRank.vue";
import DoubleRank from "./DoubleRank.vue";
export default {
  name: "Rank",
  props: {
    type: {
      type: String,
      required: true,
      default: "normal",
    },
  },
  inheritAttrs: false,
  components: {
    NormalRank,
    DoubleRank,
  },
  render(h) {
    const componentType = this.getRankType(this.type);
    return h(componentType, {
      // 透传props
      props: this.$attrs,
      // 透传事件
      on: this.$listeners,
      // 透传插槽,需要处理作用域插槽和普通插槽
      scopedSlots: this.$scopedSlots,
      slots: this.$slots,
    });
  },
  methods: {
    getRankType(type) {
      switch (type) {
        case "double":
          return "DoubleRank";
        default:
          return "NormalRank";
      }
    },
  },
};

3.3 工厂方法模式在NestJS中的应用

工厂方法模式一个具有说服力的例子是在NestJS的实践中,我们可以根据实际的需求调用框架封装的方法,决定创建实例。

当我们需要创建一个普通的HttpServer:

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(config.PORT);
}

当我们需要创建一个普通的微服务应用程序(以下代码创建的是一个Kafka的微服务):

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    MqModule,
    {
      transport: Transport.KAFKA,
      options: {
        client: {
          clientId: 'bff',
          brokers: ['localhost:9092'],
        },
        consumer: {
          groupId: 'bff-consumer',
        },
      },
    },
  );
  app.listen();
}

当我们需要创建独立应用时:

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.createApplicationContext(CronModule);
  app.enableShutdownHooks();
  app.init();
}

3.3 抽象工厂模式在SDK开发中的威力

抽象工厂模式,在我7年的开发生涯中,有说服力的场景目前仅用到过一次。

公司有两个App(即两个业务线),我们编写的运营活动,有可能同时运行在两个App中作为内嵌H5页面。

因为两个App属于不同的团队开发的,所以在API的设计上有些许的不同,但是,对于我们前端开发人员来说,我们需要进行的操作都是一样的,我作为团队的基础设施建设负责人,我肯定不会将这种需要对宿主判断的逻辑放到业务侧编写的,我的预期是,业务开发的同学用一套统一的API在H5中完成业务逻辑即可,所以,在我封装基础的宿主环境处理需要使用抽象工厂模式。

以下是一个实际例子的伪代码,首先是抽象我们团队的Bridge和Request,Bridge用户和客户端交互,Request用于和服务端进行交互。

ts 复制代码
// 定义一个Bridge接口,表示跟原生交互的定义
interface Bridge {
    // 开启一个新的webview
    openPage();
    // 关闭当前页面
    closePage();
}
// 定义一个Request对象,表示跟服务器交互的定义
interface Request {
    // 通用请求方法
    reuqest();
    // 发起一个post请求
    post();
    // 发起一个get请求
    get();
}

接着是编写各个平台对Bridge的实现:

ts 复制代码
// App A 对Bridge的实现
class BridgeA implements Bridge {

    openPage() {
        if(!this.inApp) {
            throw new Error('站外环境打开,程序部分功能不可用~');
        }
        console.log('open page in app a');
    }
    
    closePage() {
        if(!this.inApp) {
            throw new Error('站外环境打开,程序部分功能不可用~');
        }
        console.log('close page in app a');
    }

}

// App B 对Bridge的实现
class BridgeB implements Bridge {

    openPage() {
        if(!this.inApp) {
            throw new Error('站外环境打开,程序部分功能不可用~');
        }
        console.log('open page in app b');
    }
    
    closePage() {
        if(!this.inApp) {
            throw new Error('站外环境打开,程序部分功能不可用~');
        }
        console.log('close page in app b');
    }
}

然后是编写各个平台对Request的实现

ts 复制代码
class RequestA implements Request {

    request() {
        // 我实际业务里面的场景是 App A中的默认请求头是application/json,
        // 并且url上需要跟随token和userid作为授权信息,
        // 后端返回的标准结构是{ code: number; msg: string, data: any; }
        console.log('处理 app a中的请求')
    }
    
    post() {
         console.log('处理 app a中的post请求')
    }
    
    get() {
         console.log('处理 app a中的get请求')
    }

}

class RequestB implements Request {

    request() {
        // 我实际业务里面的场景是 App B中的默认请求头是application/x-www-form-urlencoded,
        // 并且Request Header上要跟一个'Bearer Token': 'xxxx',
        // 后端返回的标准结构是{ errcode: number; errormsg: string, data: any; },
        // 在这个位置将其抹平成{ code: number, msg: string, data: any  }
        console.log('处理 app a中的请求')
    }
    
    post() {
         console.log('处理 app b中的post请求')
    }
    
    get() {
         console.log('处理 app b中的get请求')
    }
}

最后,就可以编写工厂了。

ts 复制代码
// 抽象工厂,只负责生成能够操作bridge和request对象,不管当前处于什么App的环境下
abstract class Factory {
  abstract getRequest(): Request

  abstract getBridge(): Bridge;
}

// 工厂A 生成只针对App A的操作类。
class FactoryA extends Factory{

    getRequest(): Request {
        return new RequestA();
    }
    
    getBridge(): Bridge {
        return new BrideA();
    }
}

// 工厂B 生成只针对App B的操作类。
class FactoryB extends Factory {
    getRequest(): Request {
        return new RequestB();
    }
    
    getBridge(): Bridge {
        return new BrideB();
    }
}

// 最后,根据宿主环境,使用简单工厂模式决定创建具体的实现工厂。
function getAppFactory(): Factory {
    let factory: Factory;
    const ua = window.navigator.userAgent;
    switch(ua) {
        case 'A':
            factory = new FactoryA();
            break;
        case 'B':
            factory = new FactoryB();
            break;
        default:
            // 若网页在站外环境打开,则流量就全部统计给了App A
            console.log('站外环境');
            factory = new FactoryA();
            break;
    }
    // 返回外界需要的工具类实例
    return factory;
}

function getAppContext(): { bridge: Bridge; request: Request } {
    const fac = getAppFactory();
    return {
        bridge: fac.getBridge(),
        request: fac.getRequest()
    }
}

经过这样的步骤之后,业务开发的同学写代码就是一件赏心悦目的事儿了。

举个简单的例子:

vue 复制代码
<template>
    <button @click="clickMe">点击发送请求</button>
</template>

<script>
    import { getAppContext } from '@xxx/jssdk';
    const { request } = getAppContext();
    export default {
        name: 'DemoComponent',
        methods: {
            async clickMe() {
                const resp = await request.post('https://www.xxx.com/api/v1/test', {
                    hello: 'world'
                })
                if(resp.code === 1) {
                    alert('接口测试成功')
                } else {
                    alert('接口测试失败,失败原因'+ resp.msg);
                }
            }
        }
    }
</script>

在我的职业生涯中一贯的追求就是,把恶心留给自己,把方便留给别人,这又是一个极具说服力的例子,哈哈哈。

总结

在实际的开发中,每个前端同学都应该完全掌握简单工厂模式和工厂方法模式,对抽象工厂模式的概念最好也要有一定的认知,并且知道其应用场景。

抽象工厂模式在跨平台的开发中很有用,因为它提供的抽象刚好就可以应对平台的不确定性。如果你有远大的技术抱负的话,抽象工厂模式是不得不掌握的设计模式,它在前端基础设施的开发中有用武之地。

以上内容就是我在7年的开发经历中对工厂模式的实践和总结啦~

如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。

相关推荐
yngsqq4 分钟前
c#使用高版本8.0步骤
java·前端·c#
小白不太白95018 分钟前
设计模式之 模板方法模式
java·设计模式·模板方法模式
Myli_ing39 分钟前
考研倒计时-配色+1
前端·javascript·考研
色空大师41 分钟前
23种设计模式
java·开发语言·设计模式
余道各努力,千里自同风41 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
闲人一枚(学习中)42 分钟前
设计模式-创建型-建造者模式
java·设计模式·建造者模式
PandaCave1 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟1 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾1 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript