怎么利用适配器模式修改默认行为(架构师基础)

188 阅读4分钟

各位大佬早上好、中午好、晚上好

今天给大家带来一篇关于设计模式之适配器模式在Angular开发中的应用实例,特别是当我们需要调整第三方UI组件库的行为时,以及如何灵活运用设计模式来解决问题。

问题背景

在用Angular开发项目时,我们经常会用到各种UI组件库,比如ng-zorro。这些组件库提供了丰富的组件和便捷的功能,但有时它们的表现并不完全符合我们的需求。比如,我曾经遇到一个问题:在使用ng-zorro的消息组件(message)时,它有时会出现在模态框(modal)下面,导致用户体验不佳。

问题分析

问题的根源在于消息组件的定位问题。默认情况下,消息组件的定位是position: static,但在某些场景下,它需要变为position: relative。那么,我们能不能在消息组件初始化的时候,动态地设置它的定位呢?

首先我们来看下message消息组件的使用方式:

import { Component } from '@angular/core';
// 引入消息组件的module
import { NzMessageService } from 'ng-zorro-antd/message';

@Component({
  selector: 'nz-demo-message-info',
  template: ` <button nz-button [nzType]="'primary'" (click)="createBasicMessage()">Display normal message</button> `
})
export class NzDemoMessageInfoComponent {
  constructor(private message: NzMessageService) {}

  createBasicMessage(): void {
    this.message.info('This is a normal message');
  }
}

在页面中通过依赖注入了ng-zorro的消息服务组件,通过函数的方式调用来创建消息实例。那么,我们能不能在调用info方法之前,先做点手脚呢?

利用providers

providers 数组中通常包含的是服务类的构造函数。当Angular创建一个组件树时,它会使用依赖注入来提供这些服务。每当组件或者其它服务需要这些服务时,Angular的DI系统都会创建或者返回一个实例。

一句话:在Angular中,providers是用来提供服务供组件消费的。我们可以通过useClass来改写服务的实现:

@NgModule({
  providers: [
     // 使用CustomMessageService作为NzMessageService服务的实现
    { provide: NzMessageService, useClass: CustomMessageService }
  ]
})
export class AppModule {}

CustomMessageService的实现

下面是我们如何实现CustomMessageService

import { Injectable, TemplateRef } from '@angular/core';
import { NzMessageDataOptions, NzMessageRef, NzMessageService } from 'ng-zorro-antd/message';

@Injectable()
export class CustomMessageService extends NzMessageService {
  isHandleStyle: boolean = false;

  success(content: string | TemplateRef<void>, options?: NzMessageDataOptions): NzMessageRef {
    const messageRef = super.success(content, options);
    this.handle();
    return messageRef;
  }

  error(content: string | TemplateRef<void>, options?: NzMessageDataOptions): NzMessageRef {
    const messageRef = super.error(content, options);
    this.handle();
    return messageRef;
  }

  warning(content: string | TemplateRef<void>, options?: NzMessageDataOptions): NzMessageRef {
    const messageRef = super.warning(content, options);
    this.handle();
    return messageRef;
  }

  info(content: string | TemplateRef<void>, options?: NzMessageDataOptions): NzMessageRef {
    const messageRef = super.info(content, options);
    this.handle();
    return messageRef;
  }

  private handle() {
    if (!this.isHandleStyle) {
      this.isHandleStyle = true;

      const messageOverlayWrapper = $('.ant-message').parent().parent().parent();
      if (messageOverlayWrapper.hasClass('cdk-global-overlay-wrapper')) {
        messageOverlayWrapper.css('position', 'static');
      }
    }
  }

}

在这个服务中,我们使用extends关键字继承了message消息服务,然后在新的服务里面实现各个方法,在通过super.xxx(content, options)的方式调用原来的方法。handle方法用于修改消息组件外层容器的css:messageOverlayWrapper.css('position', 'static')。在这个新的类中,首次创建消息服务将会检查是否设置了css的样式。

最后,我们只需要在模块中提供这个新的服务即可:

import { CustomMessageService } from '@shared/nz-message';

providers: [
    ...
    { provide: NzMessageService, useClass: CustomMessageService },
    ...
  ],
  

这样在消息创建的时候会使用新的服务。

志哥我想说

适配器模式(Adapter Pattern)是一种设计模式,它允许不兼容的接口协同工作。在软件工程中,适配器模式通常用于将一个类的接口转换成客户端期望的另一个接口。这样做可以使得原本因接口不兼容而不能一起工作的类可以一起工作。

在前端开发中,适配器模式可以用于多种场景,例如:

  1. 兼容不同的库或框架:当你需要将一个旧的应用程序集成到一个新的技术栈中时,可以使用适配器模式来转换旧的接口,使其与新的系统兼容。
  2. 封装第三方服务:当你使用第三方API或服务时,这些服务的接口可能与你的应用程序期望的不一致。使用适配器模式可以创建一个中间层,将第三方服务的接口转换为你自己的接口。
  3. 处理遗留代码:在重构过程中,你可能需要逐步替换旧的代码。适配器模式可以让你创建一个适配器来封装旧代码,使其与新代码的接口保持一致。
  4. 实现跨浏览器兼容性:在不同的浏览器中,某些API的调用方式可能不同。适配器模式可以用于创建一个统一的接口,隐藏底层的浏览器差异。

上面的示例就是封装第三方服务,它有几个优点:

  • 能提高类的透明性和复用,但现有的类复用不需要改变。
  • 实现类和第三方类解耦,提高程序的扩展性。
  • 符合开闭原则。

希望这个例子能帮助大家在开发过程中遇到类似问题时,能快速找到解决方案。