Published on

适配器模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

在软件设计中,适配器模式(Adapter Pattern)是一种结构型设计模式。它的主要作用是将一个接口转换成客户端所期望的另一个接口,使得原本不兼容的接口可以协同工作。适配器模式通常用于现有的类无法直接与系统中的其他部分协作时,通过在中间引入一个适配器,将两个不兼容的类连接起来。

为什么需要适配器模式?

想象一下,你去欧洲旅行途中需要给带去的笔记本充电,但是你发现电源插头与欧洲的电源插座不匹配。这时候,你需要一个适配器,它一边能够插入你的笔记本插头,另一边则能够接入插座。使用适配器之后,电脑就能正常工作,即使它们的插头和插座设计是不一样的,这就实现了接口的适配。

在软件开发中,这个适配器一般是一个对象,它包裹了老系统的接口,并且提供一个符合新系统期待的接口。适配器会接收新系统的调用,将其转换为老系统能够理解的调用方式,并将结果再转换回新系统能够处理的形式。

例如,你可能有一个旧的系统,它返回数据为XML格式,而你的新系统则期望接受JSON格式的数据。一个适配器可以实现一个接口,该接口接收JSON格式的输入,然后内部将这个输入转换为XML,调用旧系统接口,得到XML格式的结果,再将这个结果转换为JSON格式并返回给新系统。

简单来说,适配器模式就像是不同设备或系统之间的桥梁,它帮助它们克服不兼容的差异,让它们可以无缝地合作。

基本概念

适配器模式包括以下几个部分:

  1. 目标 (Target):期望的接口,该接口是客户端真正需要的接口。
  2. 适配者 (Adaptee):需要适配的现有接口,可能与目标接口不兼容。
  3. 适配器 (Adapter):连接目标和适配者的中间类,负责将适配者的接口转换为目标接口。

实现示例

假设我们有一个旧的支付处理器接口 OldPaymentProcessor 和一个新的支付处理器接口 NewPaymentProcessor,我们希望能够利用旧的支付处理器与新接口兼容。

定义目标接口(新接口)

interface NewPaymentProcessor {
  processPayment(amount: number): void;
}

定义适配者类(旧接口)

class OldPaymentProcessor {
  makePayment(amount: number) {
    console.log(`Processing payment of ¥${amount} using old processor.`);
  }
}

实现适配器类

适配器类实现目标接口,并通过包含或继承适配者类来完成接口的适配。

class PaymentAdapter implements NewPaymentProcessor {
  private oldProcessor: OldPaymentProcessor;

  constructor(oldProcessor: OldPaymentProcessor) {
    this.oldProcessor = oldProcessor;
  }

  processPayment(amount: number): void {
    this.oldProcessor.makePayment(amount);
  }
}

使用适配器

const oldProcessor = new OldPaymentProcessor();
const adapter = new PaymentAdapter(oldProcessor);

// 使用新接口处理支付
adapter.processPayment(100); // 输出: Processing payment of ¥100 using old processor.

应用场景

API 接口升级

假设你正在维护一个老旧的项目,该项目调用外部 API 来获取用户数据。外部 API 已经升级到新版,但你的代码依然使用旧版 API 接口。你可以使用适配器模式来避免大规模修改现有代码。

// 新的 API 接口
interface NewAPI {
  fetchUser(): Promise<{ name: string; age: number }>;
}

// 旧的 API 接口
class OldAPI {
  getUserData(callback: (user: { name: string; age: number }) => void) {
    const user = { name: "John Doe", age: 30 };
    callback(user);
  }
}

// 适配器
class APIAdapter implements NewAPI {
  private oldAPI: OldAPI;

  constructor(oldAPI: OldAPI) {
    this.oldAPI = oldAPI;
  }

  fetchUser(): Promise<{ name: string; age: number }> {
    return new Promise((resolve) => {
      this.oldAPI.getUserData((user) => {
        resolve(user);
      });
    });
  }
}

// 使用适配器
const oldAPI = new OldAPI();
const adapter = new APIAdapter(oldAPI);

adapter.fetchUser().then((user) => {
  console.log(`User name: ${user.name}, Age: ${user.age}`);
  // 输出: User name: John Doe, Age: 30
});

浏览器差异兼容

在前端开发中,不同的浏览器可能对 JavaScript API 的实现不尽相同。适配器模式可以用来创建一个统一的 API,让同一段代码可以在多种浏览器上运行而不需要修改。

下面的示例中,UnifiedEvent 接口定义了一组标准的事件处理函数 addHandlerremoveHandler

LegacyEventSystem 类跟旧的浏览器事件绑定接口(例如 attachEventdetachEvent)相对应,这个接口过去常在旧版本的Internet Explorer中使用。

ModernEventSystem 类实现了 UnifiedEvent 接口,并使用现代浏览器中标准的 addEventListenerremoveEventListener 方法。

EventAdapter 类提供了 UnifiedEvent 接口的实现,内部通过 LegacyEventSystem 来处理事件,允许老旧代码能够通过新的 UnifiedEvent 接口使用。

此处的 EventAdapter 类是一个适配器:它“包装”了旧的 LegacyEventSystem 使其看起来像是一个现代的,符合 UnifiedEvent 接口的事件系统。这意味着,使用 EventAdapter 可以让你的代码使用统一的接口与老旧系统兼容,同时你也可以保留使用 ModernEventSystem 的代码,它直接与现代API对接。

interface UnifiedEvent {
  addHandler(element: any, event: string, handler: (event: any) => void): void;
  removeHandler(
    element: any,
    event: string,
    handler: (event: any) => void,
  ): void;
}

class LegacyEventSystem {
  attachEvent(element: any, event: string, handler: (event: any) => void) {
    element.attachEvent(`on${event}`, handler);
  }

  detachEvent(element: any, event: string, handler: (event: any) => void) {
    element.detachEvent(`on${event}`, handler);
  }
}

class ModernEventSystem implements UnifiedEvent {
  addHandler(element: any, event: string, handler: (event: any) => void) {
    element.addEventListener(event, handler);
  }

  removeHandler(element: any, event: string, handler: (event: any) => void) {
    element.removeEventListener(event, handler);
  }
}

class EventAdapter implements UnifiedEvent {
  private legacySystem: LegacyEventSystem;

  constructor(legacySystem: LegacyEventSystem) {
    this.legacySystem = legacySystem;
  }

  addHandler(element: any, event: string, handler: (event: any) => void) {
    this.legacySystem.attachEvent(element, event, handler);
  }

  removeHandler(element: any, event: string, handler: (event: any) => void) {
    this.legacySystem.detachEvent(element, event, handler);
  }
}

// 使用适配器适配老旧浏览器
const legacySystem = new LegacyEventSystem();
const adapter = new EventAdapter(legacySystem);

const element = document.getElementById("myElement");
function eventHandler(event: any) {
  console.log("Event triggered!");
}
adapter.addHandler(element, "click", eventHandler);
adapter.removeHandler(element, "click", eventHandler);

整合第三方库

当你的应用需要用到第三方库,且这些库的 API 不与现有代码结构不兼容时,可以编写适配器来转换接口,让第三方库与现有代码协同工作。

interface ApplicationLogger {
  log(message: string): void;
  error(message: string): void;
}

class ThirdPartyLogger {
  writeLog(message: string) {
    console.log(`ThirdPartyLog: ${message}`);
  }

  writeError(message: string) {
    console.error(`ThirdPartyError: ${message}`);
  }
}

class LoggerAdapter implements ApplicationLogger {
  private thirdPartyLogger: ThirdPartyLogger;

  constructor(thirdPartyLogger: ThirdPartyLogger) {
    this.thirdPartyLogger = thirdPartyLogger;
  }

  log(message: string): void {
    this.thirdPartyLogger.writeLog(message);
  }

  error(message: string): void {
    this.thirdPartyLogger.writeError(message);
  }
}

// 使用适配器
const thirdPartyLogger = new ThirdPartyLogger();
const logger = new LoggerAdapter(thirdPartyLogger);

logger.log("This is a log message.");
logger.error("This is an error message.");

本地存储抽象

适配器模式可以用来抽象本地存储 API,比如 localStorage 和 IndexedDB,提供统一接口,使得存储机制的选择和更换变得更加灵活。 下面示例中StorageProvider 接口定义了一组与数据存储相关的操作——saveload、和remove

然后,LocalStorageProviderIndexedDBProvider 类实现了该StorageProvider接口,提供了各自的详细实现,它们分别使用不同的技术(localStorage 和 IndexedDB)来存储数据。

通过实现统一的StorageProvider接口,这两个类提供了统一的方法调用方式。这就允许你在不更改调用代码的前提下,灵活地切换使用LocalStorageProviderIndexedDBProvider 作为本地存储,这正是适配器模式的目标。

interface StorageProvider {
  save(key: string, value: any): void;
  load(key: string): any;
  remove(key: string): void;
}

class LocalStorageProvider implements StorageProvider {
  save(key: string, value: any): void {
    localStorage.setItem(key, JSON.stringify(value));
  }

  load(key: string): any {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  }

  remove(key: string): void {
    localStorage.removeItem(key);
  }
}

class IndexedDBProvider implements StorageProvider {
  private db: IDBDatabase | null = null;

  constructor() {
    const request = indexedDB.open("MyDatabase", 1);
    request.onsuccess = () => {
      this.db = request.result;
    };
    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      db.createObjectStore("store", { keyPath: "key" });
    };
  }

  save(key: string, value: any): void {
    const transaction = this.db!.transaction(["store"], "readwrite");
    const store = transaction.objectStore("store");
    store.put({ key, value });
  }

  load(key: string): any {
    return new Promise<any>((resolve) => {
      const transaction = this.db!.transaction(["store"], "readonly");
      const store = transaction.objectStore("store");
      const request = store.get(key);
      request.onsuccess = () => {
        resolve(request.result ? request.result.value : null);
      };
    });
  }

  remove(key: string): void {
    const transaction = this.db!.transaction(["store"], "readwrite");
    const store = transaction.objectStore("store");
    store.delete(key);
  }
}

// 使用 LocalStorage 作为存储提供者
const localStorageProvider: StorageProvider = new LocalStorageProvider();
localStorageProvider.save("key1", { name: "John Doe" });

const localData = localStorageProvider.load("key1");
console.log(localData);

localStorageProvider.remove("key1");

// 使用 IndexedDB 作为存储提供者
const indexedDBProvider: StorageProvider = new IndexedDBProvider();
indexedDBProvider.save("key2", { name: "Jane Doe" });

indexedDBProvider.load("key2").then((data) => {
  console.log(data);
});

indexedDBProvider.remove("key2");

优缺点

优点

  1. 兼容性增强:适配器模式使得现有的接口能够与其他系统兼容,不需要修改现有代码。
  2. 灵活性:可以在不改变现有代码的基础上引入新的接口和功能。
  3. 解耦合:通过在客户端和服务端之间引入适配器,降低了代码的耦合度。

缺点

  1. 复杂度增加:引入适配器类会增加系统的复杂度和代码量。
  2. 性能开销:适配过程中可能会有额外的性能开销,尤其是在频繁调用适配器的方法时。

总结

适配器模式是一种结构型设计模式,它允许不兼容的接口能够一起工作。这种模式通常用于系统升级或集成第三方库时,接口不匹配导致无法直接通信,通过一个额外的适配器层来间接连接这些组件,从而不需要修改已有的代码或接口。

在前端开发中,适配器模式可以在多种场景下发挥作用,比如在不同浏览器之间提供统一的API接口,或者在旧代码与新库之间架设一个中间层。它提供了一种优雅的解决方案来处理现有系统与新系统之间的兼容问题,促进了模块间的解耦和可复用性。

总的来看,适配器模式是一种高效的方式来处理系统间的协作问题,它通过对接口进行适配,而不是改变现有代码逻辑,减少了系统兼容性调整的难度,并保持了代码的整洁性和可维护性。在追求快速迭代与持续集成的现代前端开发实践中,适配器模式提供了确保项目稳定性的同时,又不牺牲灵活性和扩展性的可行路径。