likes
comments
collection
share

使用vite静态导入、工厂模式优雅的实现同构数据、不同业务实现的无侵入扩展

作者站长头像
站长
· 阅读数 5

多年的java编程经历,使我不自觉的在编写前端代码时,同时将一些面向对象的特性应用到前端代码中去。这样的习惯,个人认为比较适合规模相对大一点的前端工程。本篇文章,是在我编写基于websocket的实时通信业务时,遇到的扩展性的问题。虽然硬编码当然可以解决问题,但是总觉得不够优雅,下面是我对前端代码如何扩展,而又遵循开闭原则的思考过程。

一、 适合场景

我分享的思考方法,适用于同一类数据结构通信,但是需要根据其中不同的数据,前端需要采用不同的处理逻辑。在增加新的处理逻辑时,无需修改原有任何代码,只需增加新的处理逻辑代码。这完好的实现开闭原则 —— 对修改关闭、对扩展开放。举实例来说:

我需要在产品中,增加即时通信功能,首先需要支持的是聊天,第一版中,需要支持2类数据:

  1. 文字
  2. 图片

当我们完成功能后,据市场反馈和顾客提出新的要求,需要增加支持新类型,以保持用户对于IM惯有的认知:

  1. 表情
  2. 文件
  3. 外链

ok,后续继续来新的需求:

  1. 音频
  2. 视频

ok,后续继续来新的需求:

  1. ...

ok,物联网、数字孪生场景也来了,要支持:

  1. 更新指定的设备数据
  2. 弹窗告警
  3. 音频告警
  4. 收到相关数据后,通知其他系统的设备做出响应
  5. ...

可以看到,这就是典型的同一种数据结构,不同的业务数据,需要不同的业务实现的场景。要不断实现支持新增业务,最常用的当然是if... else... 或者 switch ... case ...。 但是,这样的代码都在一个通信处理的函数中处理吗?如果其中的某一项或多项业务逻辑存在变更了,怎么修改?想想都头疼!

二、思考点

如何解决上面场景的持续扩展?主要考虑以下2点

2.1 数据接收和数据逻辑处理必须分离

在上面的IM需求中,一个客户端和服务端通过同一种协议连接(websocket),在客户端数据会统一在一个点接收(比如:websocket的onmessage方法),但是,要想实现不同数据有不同业务的无侵入扩展,数据的逻辑处理,必须分离出去,而且,不同的逻辑处理也必须分离。

2.2 数据逻辑处理机制,必须支持不同处理逻辑的动态导入。

如果满足 2.1 的要求,不同的数据处理逻辑彼此分离,比如处理文字的,和处理图片是2个函数。那么,在扩展增加新的出来逻辑时,原有的数据逻辑处理机制,必须能够自动识别新增的处理逻辑,而不需要去修改原有的代码。

这里提供一种思路,基于常用设计模式-工厂模式、单例模式,以及利用vite的静态导入,就可以实现无侵入扩展和修改。具体如下:

三、思路

3.1 通过 vite 的静态导入,实现不同业务处理逻辑的自动动态导入。

我们知道:vite对于静态资源的导入处理有多种方式,import.meta.glob 函数支持从文件系统导入多个模块。前端代码则可以利用此函数,来实现将多个文件(每个文件实现一种数据处理逻辑)集中导入到一个数据结构中。在使用时,只需要从这个数据结构中,取出适合的处理逻辑实例。

3.2 通过工厂模式、接口来分离业务处理逻辑

工厂模式,就是集中创建对象。放在当前的需求场景下,就是利用工厂模式来集中创建和取出所有的不同 数据处理逻辑对象

通过接口来分离业务处理逻辑,一方面可以规范主要的数据结构和属性,一方面可以规范处理动作。好处是谁用谁知道。

四、解决实现

本文使用 typescript实现,js实现是一个原理,不再单独列出。

4.1 文件结构

主要实现有4类文件,如下图所示:

  1. IHandler.ts 业务处理接口定义
  2. HandlerFactory.ts 处理器工厂类
  3. handlers/*.ts 不同的业务处理类
  4. 调用代码,不在此目录

使用vite静态导入、工厂模式优雅的实现同构数据、不同业务实现的无侵入扩展

4.1 定义接口

我们首先,需要定义处理数据的接口。规定,所有的数据业务处理类,必须实现此接口:

export interface IHandler {
  handler(data: string): void;   // 规范处理行为
  cmd(): number;                 // 标识处理行为
}

接口 IHandler,定义的所有数据处理的统一处理函数: hander ,此函数接收一个 string 类型的数据(当然可以是其他你需要的任何类型)。不同的业务处理逻辑,自行实现此函数。

cmd函数,规定返回一个number类型。此函数的目的在于标识不用的实现类。可通过下方代码来理解这一点。

4.2 定义工厂类

工厂类的主要职责,是将不同的业务实现类,根据不同标识,动态导入类中的一个数据结构中,并对外提供接口,来获取不同的业务处理类实例。代码如下:

import { IHandler } from './IHandler';

export default class HandlerFactory {
  // 工厂实例
  private static ins: HandlerFactory;
  // 对象容器
  private handlers = new Map();

  private constructor() {
    //this.initIns();
  }

  // 利用vite的静态导入,动态生成实例,并存入对象容器
  private async initIns() {
    const modules = import.meta.glob('./handlers/*.ts');
    for (const path in modules) {
      const file = await modules[path]();
      const myClass = file.default;
      const ins = new myClass() as IHandler;
      this.handlers.set(ins.cmd(), ins);
    }
  }

  // 获取某个实例
  public getHandler(code: number): IHandler {
    return this.handlers.get(code);
  }

  // 获取工厂实例
  public static getIns(): Promise<HandlerFactory> {
    return new Promise(async (resolve) => {
      if (!this.ins) {
        this.ins = new HandlerFactory();
        await this.ins.initIns();
        resolve(this.ins);
      }
      resolve(HandlerFactory.ins);
    });
  }
}

4.2.1 getIns方法

代码中可以看到,工厂类的使用入口是一个异步的静态方法 getIns,该方法返回工厂类的唯一实例对象。由于initIns是一个异步方法,因此需返回一个Promise对象。此处使用了单例模式。为简化起见,代码中仅处理了正常的情况。

4.2.2 initIns 方法

initIns方法的主要职责,是动态的引入某个目录下(此处是相对路径 : handlers)下的所有ts文件,并将这些ts文件转换为类的实例,存入工厂实例的map中去。

其中,this.handlers.set(ins.cmd(), ins); 此行代码就将该对象实例,以其cmd()的值为key,存入map中,便于通过此key来获取实现类实例。

4.2.3 getHandler 方法

getHandler方法,提供了获取根据不同业务处理类实例标识,获取业务处理类实例对象的功能。

4.3 定义业务实现类

此处,定义2个实例的处理

Handler1 代码:

import { IHandler } from '../IHandler';

export default class Handler1 implements IHandler {
  handler(data: string): void {
    console.log('我是 hander1, 收到参数:', data);
  }
  cmd(): number {
    return 1;
  }
}

Hanlder2 代码

import { IHandler } from '../IHandler';

export default class Handler2 implements IHandler {
  handler(data: string): void {
    console.log('我是 hander2, 收到参数:', data);
  }
  cmd(): number {
    return 2;
  }
}

类 Handler1和Handler2 都实现了 IHandler接口。handler函数根据实际需要来实现不同的业务逻辑,cmd方法,则返回不同的标识。

注意:cmd() 返回的值,必须是唯一的。

4.4 调用代码

调用代码很简单,在某个代码中编写代码如下:

const handlerFactoryInstance = await HandlerFactory.getIns();

function handler(cmd: number, data: string): void {
  handlerFactoryInstance.getHandler(cmd).handler(data);
}


// 测试代码,此处为了说明,直接调用handler方法,手动赋值为 2 
const cmd = 2;
handler(cmd, 'hello world');

代码中可以看到,我们定义了一个handler函数,此函数接收2个参数:cmd和data。当然也可以接收一个data参数,cmd是在data中的一个属性。

首先,我们获取一个工厂类的实例,自定义函数 handler(cmd: number, data: string): void ,就是业务处理函数的统一数据入口,此函数通过调用工厂实例的getHandler方法,来获取指定标识的处理器实例。然后调用实例的handler方法来调用对应的实现逻辑。

下面的测试代码,当手动指定cmd=1时,和 cmd=2时,控制台将分别打印对应的实现信息。

我们扩展新的业务处理逻辑时,只需要在handler目录下,新建实现IHandler接口的文件,并实现对应的业务处理逻辑即可。

至此,我们就实现了同构数据、不同业务实现的无侵入扩展。