设计模式在前端开发中的实践(十五)——外观模式

外观模式

外观模式,其实对于良好的设计系统软件绝对能看到外观模式的身影,如果要说它是一种设计模式,倒不如说它是一种设计思想。

在软件开发中,一旦你理解了封装 、继承 、多态的这些面向对象的特征,你就一定掌握了外观模式,即使在这之前你没有听说过它。

1、基本概念

外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这个子系统更加容易使用。

外观模式是依赖倒置原则和迪米特法则的体现。

依赖倒置原则强调两个点:

  • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

在外观模式中,高层的客户端代码不直接与系统的复杂底层(如多个子系统或模块)交互,而是通过一个外观接口进行交互。这个外观接口提供了一个简化的方式来访问底层功能,而底层模块的复杂性被封装在外观后面。因此,客户端代码依赖于这个抽象的外观接口,而不是具体的实现细节,这正是依赖倒置原则的体现。

迪米特法则又被称为"最少知道原则",其核心思想是"一个对象应该对其他对象有最少的了解。",外观模式就通过把底层一些复杂的实现细节封装起来,降低了系统的耦合性,这正是迪米特法则的体现。

外观模式的UML图如下:

在这个图里面,SubSystem Classes框选起来的那一部分我们都不需要关注,只有一个Facade类对外暴露了这个系统的能力,你就只需要按照API调用即可。

2、代码范式

这个代码范式仅仅是一个非业务场景的例子,因为外观模式展示的是一种设计思想,开发中需要根据自己的业务酌情处理即可。

ts 复制代码
class SubsystemOne {
    public methodOne() {
        console.log('do work in method one');
    }
}

class SubsystemTwo {
    public methodTwo() {
        console.log('do work in method two');
    }
}

class SubsystemThree {
    public methodThree() {
        console.log('do work in method three');
    }
}

class SubSystemFour {
     public methodFour() {
        console.log('do work in method four');
    }
}

class Facade {
    // 全部声明为私有属性,对外隐藏实现细节
    private one = new SubsystemOne();
    
    private two = new SubsystemTwo();
    
    private three = new SubsystemThree();
    
    private four = new SubsystemFour();
    
    // 对外暴露doWork1方法1
    public doWork1() {
        this.one.methodOne();
        this.three.methodThree();
        this.four.methodFour();
    }
    // 对外暴露doWork2方法2
    public doWork2() {
       this.two.methodTwo();
       this.four.methodFour();
    }
}

function bootstrap() {
    const facade = new Facade();
    facade.doWork1();
    facade.doWork2();
}

3、在前端开发中的实践

3.1、LocalForage

localForage 中文文档 (docschina.org)

localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。

localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。

js 复制代码
// 通过 localStorage 设置值 
localStorage.setItem('key', JSON.stringify('value')); doSomethingElse(); 
// 通过 localForage 完成同样功能 
localforage.setItem('key', 'value').then(doSomethingElse); 
// localForage 同样支持回调函数 
localforage.setItem('key', 'value', doSomethingElse);

我们可以保持跟localStorage大致相同的API的前提下,能够满足我们的存储需求。

比如,如果在前端缓存一些二进制数据流,数据可能会比较大,对于localStorage来说,一个域下面仅仅5M左右,(而且流文件转换到字符串还需要进行编码)满足不了我们的需求,localForage自动为我们切换到IndexedDB存储的话,我们使用起来就跟使用localStorage那么简单,在这个过程中,localForage底层是有很复杂的处理流程的,只不过它把这些细节都隐藏起来了,我们看到的就是预期的数据被存储到了指定的位置而已,所以说,这就是一种外观模式的实际场景。

3.2 Axios

这个是每个前端开发者都熟悉的不能再熟悉的一个库了。

浏览器暴露给我们的XMLHttpRequest是很复杂的,如果我们直接使用XHR对象的话,编程效率低。

而经过Axios封装之后,我们再操作XHR就简单的几个配置就完成了,这也是外观模式的实际场景。

还有一个关键点,在不同的环境下,想要发送请求的方式是不一样的,在浏览器中,使用XHR对象或者fetch,在Nodejs环境中,使用node的原生模块http,如果没有axios这个外观类,那我们还需要考虑不同的环境,而有了它之后,我们仅仅只需要无脑的按照axios的配置发送请求即可。

3.3 NestJS中的分层设计

因为严格来说,很多npm包都是为了解决某个领域下编程的便利性而进行的封装,所以它们都是外观模式的体现,我如果继续举这种例子的话,就有点儿水文的感觉了,因此,这节中,给大家一个我的实际编码例子。

我以我的最近所开发的BFF来举例。

业务逻辑层仅仅需要获取数据访问层拿到的数据,至于数据访问层怎么拿?用什么方式拿?业务逻辑层不用管,反正我就只需要给你预期规格的数据即可。

以下是我的代码设计:

首先是,数据访问层的接口定义

ts 复制代码
/**
 * 数据访问层的接口定义
 */
export interface IActEngineRepository {
  /**
   * 获取活动配置
   * @param param0
   * @returns
   */
  getConfig({ userId, actId }: ActGeneralParams): Promise<ConfigsListDto>;

  /**
   * 获取活动的信息
   * @param param
   * @returns
   */
  getActivityInfo({ userId, actId }: ActGeneralParams): Promise<ActInfoDto>;
  
  /**
   * 获取榜单信息
   * @param param0
   */
  getFullRankInfo({ userId, actId, rankId }: ActRankParams): Promise<RankDto>;
}

然后是通过gRPC获取业务服务器的数据的实现(省略了一部分代码,仅仅表达一个业务封装的含义)

ts 复制代码
/*
 * 以gRPC通信方式获取业务后端的数据
 */
@Injectable()
export class ActEngineGrpcRepository
  extends BaseActEngineRepository
  implements IActEngineRepository
{
  @Inject()
  protected readonly actParserService: ActEngineEntityParserService;

  /**
   * 获取活动配置
   */
  @AutoBind
  public async getConfig({
    userId,
    actId,
  }: ActGeneralParams): Promise<ConfigsListDto> {
    const configPromise = promisify<Empty, Metadata, Web.ConfigsReply>(
      this.client.configs,
    );
    const p = configPromise();
    const configs = await this.safetyPromise(p).then((response) =>
      response.toObject(),
    );
    return configs;
  }

  /**
   * 获取活动的信息
   * @param param
   * @returns
   */
  @AutoBind
  public async getActivityInfo({
    userId,
    actId,
  }: ActGeneralParams): Promise<ActInfoDto> {
    const actInfoPromise = promisify<Empty, Metadata, Web.ActInfoReply>(
      this.client.actInfo,
    );
    const p = actInfoPromise();
    const activityInfo = await this.safetyPromise(p).then((response) =>
      response.toObject(),
    );
    return activityInfo;
  }

  /**
   * 获取完整的榜单信息,内聚了获取列表和自己所处该榜单中位置的信息,隐藏了细节
   * @returns
   */
  public async getFullRankInfo({
    userId,
    actId,
    rankId,
    size,
  }: ActRankParams): Promise<RankDto> {
    const rankList = await this.getRankList({ userId, rankId, actId, size });
    let own: ActRankItem;
    if (userId) {
      own = await this.getOwnInfo({ userId, rankId, actId });
    } else {
      own = null;
    }
    return {
      list: rankList.listList,
      own,
    };
  }

  // 私有方法,内部细节,不对外暴露
  private async getRankList({
    userId,
    rankId,
    actId,
    dateTime,
    size = 100,
  }: ActRankParams): Promise<
    Omit<Web.RankDetailReply.AsObject, 'listList'> & {
      listList: Array<ActRankItem>;
    }
  > {
    const rankPromise = promisify<
      Web.RankDetailRequest,
      Metadata,
      Web.RankDetailReply
    >(this.client.rankDetail);
    const request: Web.RankDetailRequest = new Web.RankDetailRequest();
    const p = rankPromise(request);
    const rankInfo = await this.safetyPromise(p).then((response) =>
      response.toObject(),
    );
    return {
      ...rankInfo,
      listList: rankInfo.listList.map((v) => {
        return this.getRankDetail(v);
      }),
    };
  }

  // 私有方法,内部细节,不对外暴露
  private getRankDetail(input: CommonEntity.RankItem.AsObject): ActRankItem {
    return omit(
      {
        ...input,
        detail: input.user || input.work || input.room,
      },
      ['user', 'work', 'room'],
    );
  }
  
  // 私有方法,内部细节,不对外暴露
  private async getOwnInfo({
    userId,
    rankId,
    actId,
    dateTime,
  }: ActRankParams): Promise<ActRankItem> {
    const rankPromise = promisify<
      Web.RankMemberRequest,
      Metadata,
      CommonEntity.RankItem
    >(this.client.rankMember);
    const request: Web.RankMemberRequest = new Web.RankMemberRequest();
    const p = rankPromise(request);
    const rankInfo = await this.safetyPromise(p).then((response) =>
      response.toObject(),
    );
    return this.getRankDetail(rankInfo);
  }
}

然后对外暴露的是实现类,而依赖注入的依据是一个特征的Token信息:

ts 复制代码
@Module({
  providers: [
    {
      provide: ACT_ENGINE_REPO_TOKEN,
      useClass: ActEngineGrpcRepository,
    },
  ],
  exports: [ACT_ENGINE_REPO_TOKEN],
})
export class ActEngineModule {}

最后,业务逻辑层使用数据外观类:

ts 复制代码
@Injectable()
export class CollectionService {
  @Inject(ACT_ENGINE_REPO_TOKEN)
  protected readonly actEngineRepo: IActEngineRepository;
  
  // 获取活动的配置信息
  public getActConfig(params: ActParams) {
      return this.actEngineRepo.getConfig(params);
  }
}

这样的设计,一方面可以根据需求灵活的切换和业务后端的通信方式,也能隔绝业务后端的变更,起到一种"防腐"(业务后端的调整,不至于影响到下游的软件设计,不明白的同学可以查看我的这篇文章->设计模式在前端开发中的实践(六)------适配器模式 - 掘金 (juejin.cn))的作用。

总结

外观模式是一个几乎必用的设计模式,如果你之前不知道什么是外观模式,也能写出可维护性较好的代码,那么请你现在也忘掉什么是外观模式(因为你知道的多了,可能反而会对你形成一种束缚,实战开发中总结出来的经验,要比在书中按图索骥好的多)。

在软件设计的过程中,我们应该有意识的考虑到软件的分层,层与层之间就形成外观模式的Facade;另外,就是我们在维护屎山的时候,我们可以开发一个Facade来包裹住这个屎山(就好比家丑不外扬,哈哈哈),这样在外部调用的时候,可以有效降低出错的可能性。

所谓实践出真知,大家就根据自己的理解去编码吧,Good Luck!

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

相关推荐
定栓4 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga5 小时前
N32G430芯片的flash表示
前端·javascript·html
Data analyse4565 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
NC 王炸5 小时前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出
todoitbo5 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员5 小时前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB5 小时前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
Chloe.Zz5 小时前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋5 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5
silvamux_5 小时前
4款新模型商务邮件翻译横评:0.98到0.46
android·java·javascript