三言两语说透设计模式的艺术-适配器模式

在前端开发中,我们经常会遇到不同模块、库或 API 之间的接口不兼容的情况。这可能是由于接口的变更、不同技术栈之间的差异,或是迁移项目时遗留下来的问题。为了解决这些问题,适配器模式提供了一种有效的解决方案。

什么是适配器模式?

假设你在中国买了一个新的iPhone,但是充电器的接口是美国标准,不能直接插入中国的电源插座。这时候你可以使用一个电源适配器,它一端是美标插头,可以连接iPhone充电器,另一端是中国标准插头,可以插进中国的电源插座。

这个电源适配器就扮演了适配器模式中的角色:

  • 目标(Target)接口:中国标准电源插座
  • 适配者(Adaptee):iPhone充电器的美标接口
  • 适配器(Adapter):电源适配器,实现了目标接口,同时封装了适配者
  • 客户(Client):你和你的iPhone 通过适配器,原本不能匹配使用的美标充电器和中国电源接口可以一起工作,所以你可以给iPhone充电。

这就像在代码中使用适配器模式可以让不同的接口一起工作一样。

适配器模式是一种结构性设计模式,旨在将一个类的接口转换为客户端期望的另一种接口。这使得原本因接口不兼容而无法一起工作的类可以一起协同工作。适配器模式通过创建一个中间适配器类来实现接口的转换,从而使得不同接口之间能够进行交互。

适配器模式的结构和使用场景

适配器模式包含以下主要角色:

  • 目标(Target) - 定义客户端所需的和适配器需要实现的接口
  • 适配器(Adapter) - 转换接口到目标接口的适配器
  • 适配者(Adaptee) - 需要适配的接口
  • 客户(Client) - 通过目标接口调用适配器

适配器实现了目标接口,同时封装了适配者。客户端通过目标接口调用适配器,适配器再调用适配者接口,这样就实现了接口的转换。

适配器模式通常用于:

  • 复用已有的类,而接口不匹配时. 通过适配器可以使得原类和接口匹配。
  • 希望复用一些现存的类,但是接口与业务要求不一致时。
  • 需要访问业务领域中的多个子系统的功能,而多个子系统具有不一致的接口时。 可以使用适配器模式构建一个统一的接口,使多个子系统功能对外以统一的接口呈现,提高子系统的透明度和复用度。

前端开发中使用适配器模式

适配器模式在前端开发中使用广泛,主要通过编写适配器组件来解决不同接口不兼容的问题。下面我通过几个例子来具体介绍。

第三方库的接口不兼容

假设你正在使用两个不同的图表库,每个库都有自己独特的数据格式和 API。然而,你希望在一个页面上同时使用这两个库来呈现不同类型的图表。通过创建适配器,你可以将一个库的数据格式转换为另一个库所需的格式,从而实现两者的协同工作。

假设我们有两个图表库,一个是名为 ChartJS 的库,另一个是名为 Highcharts 的库。每个库都有自己不同的数据格式和 API,我们希望能够在同一个项目中使用这两个库。

首先,我们定义 ChartJS 和 Highcharts 两个库的接口:

// ChartJS 接口
interface ChartJS {
    render(data: number[]): void;
}

// Highcharts 接口
interface Highcharts {
    draw(data: number[]): void;
}

然后,我们创建适配器类来适配 ChartJS 到 Highcharts:

// ChartJS 到 Highcharts 的适配器
class ChartJSAdapter implements Highcharts {
    private chartJS: ChartJS;

    constructor(chartJS: ChartJS) {
        this.chartJS = chartJS;
    }

    draw(data: number[]): void {
        // 将 ChartJS 的 render 方法适配成 Highcharts 的 draw 方法
        this.chartJS.render(data);
    }
}

最后,我们可以在客户端代码中使用适配器来实现图表的绘制:

// 使用适配器创建 Highcharts 实例
const chartJSInstance: ChartJS = {
    render: (data: number[]) => {
        console.log(`ChartJS rendering: ${data}`);
    }
};

const chartAdapter = new ChartJSAdapter(chartJSInstance);

// 绘制图表
const data = [10, 20, 30, 40, 50];
chartAdapter.draw(data);

在这个示例中,我们创建了一个适配器类 ChartJSAdapter,它实现了 Highcharts 接口,但在内部使用了 ChartJS 实例。适配器的 draw 方法将 ChartJS 的 render 方法适配成了 Highcharts 的 draw 方法,从而使得我们可以在不同的库之间进行适配。

不同平台之间的兼容性

在不同浏览器平台可能具有不同的界面和 API 要求,通过创建适配器可以用来抹平这些差异,你可以根据目标平台的需求适配相应的界面元素和功能,从而实现代码的重用和跨平台开发。

例如,我们需要编程式获取页面滚动位置:

interface ScrollPositionReader {
  getScrollPosition(): {x: number, y: number}; 
}

而不同浏览器有不同的获取滚动位置方法:

// Chrome, Firefox等
window.scrollX
window.scrollY

// IE8及以下
document.body.scrollLeft
document.body.scrollTop

为了统一接口,我们可以编写适配器:

class ScrollPositionAdapter implements ScrollPositionReader {

  getScrollPosition() {
    if (window.scrollX != null) {
      return {
        x: window.scrollX,
        y: window.scrollY  
      }
    } else {
      return {
        x: document.body.scrollLeft,
        y: document.body.scrollTop
      }
    }
  }

}

然后就可以通过统一的ScrollPositionReader接口获取滚动位置了:

const positionReader = new ScrollPositionAdapter();
const pos = positionReader.getScrollPosition();

这样,适配器帮我们解决了不同浏览器接口的不兼容问题。在前端工程化配置中,babel和ployfill也使用了适配器模式,将代码进行编译,来实现对不同浏览器版本的兼容。

适配后端接口变更

当后端 API 发生变更时,前端可能需要进行大量修改以适应新的数据结构和字段。通过创建适配器,你可以将新的 API 响应转换为前端旧代码所期望的数据格式,从而避免全面修改现有代码。

假设我们的应用中使用了一个名为 OldAPI 的旧版 API,但由于后端的变更,API 的响应数据格式发生了改变。我们希望在不改变现有代码的情况下,适应新的数据格式。

首先,我们定义 OldAPI 的旧版和新版接口:

// 旧版 OldAPI 接口
interface OldAPI {
    requestData(): string;
}

// 新版 OldAPI 接口
interface NewAPI {
    requestNewData(): string;
}

然后,我们创建适配器类来适配旧版 OldAPI 到新版 NewAPI:

// 适配旧版 OldAPI 到新版 NewAPI 的适配器
class OldAPIToNewAdapter implements NewAPI {
    private oldAPI: OldAPI;

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

    requestNewData(): string {
        const oldData = this.oldAPI.requestData();
        // 对旧数据进行适配转换
        const newData = `${oldData} (adapted)`;
        return newData;
    }
}

最后,我们可以在客户端代码中使用适配器来请求新版数据:

// 使用适配器创建 NewAPI 实例
const oldAPIInstance: OldAPI = {
    requestData: () => {
        return "Old data";
    }
};

const apiAdapter = new OldAPIToNewAdapter(oldAPIInstance);

// 请求新版数据
const newData = apiAdapter.requestNewData();
console.log(newData);

在这个示例中,我们创建了一个适配器类 OldAPIToNewAdapter,它实现了新版 NewAPI 接口,但在内部使用了旧版 OldAPI 实例。适配器的 requestNewData 方法将旧版 API 的响应数据进行了适配转换,使得旧版 API 的响应能够适应新版接口的需求。

用适配器进行Mock模拟

当涉及使用适配器来进行 Mock 模拟时,我们可以考虑一个场景:一个应用需要从后端获取用户信息,但是在开发阶段,后端可能还没有完全实现,或者我们希望在测试中使用模拟的数据。我们可以使用适配器来模拟后端 API,以便在开发和测试中使用。

首先,我们定义一个用户信息的接口,用于后端 API 和适配器的标准:

interface UserInfo {
    id: number;
    name: string;
    email: string;
}

然后,我们创建一个后端 API 接口,模拟后端实际返回的数据:

interface BackendAPI {
    getUserInfo(userId: number): UserInfo;
}

接下来,我们可以创建一个适配器来模拟后端 API,以便在开发和测试中使用:

class MockBackendAdapter implements BackendAPI {
    getUserInfo(userId: number): UserInfo {
        // 模拟返回用户信息
        return {
            id: userId,
            name: "Mock User",
            email: "mock@example.com"
        };
    }
}

最后,我们可以在应用中使用适配器来获取用户信息:

function getAppUserInfo(api: BackendAPI, userId: number): UserInfo {
    return api.getUserInfo(userId);
}

// 在开发阶段使用模拟的后端适配器
const mockBackend = new MockBackendAdapter();
const userInfo = getAppUserInfo(mockBackend, 123);
console.log(userInfo);

在这个示例中,我们使用适配器 MockBackendAdapter 来模拟后端 API。适配器实现了 BackendAPI 接口,但在内部返回了模拟的用户信息数据。通过这种方式,我们可以在开发阶段使用模拟数据来测试应用的功能,而无需等待实际后端开发完成。

适配器模式的优缺点

适配器模式是一个有力的设计工具,可以帮助我们处理不同接口之间的兼容性问题,提高代码的可维护性和可扩展性。然而,开发者需要在使用适配器时谨慎权衡其优缺点,确保在特定情况下它能够真正带来价值。以下是适配器模式的优缺点。

1、优点

  • 解耦代码: 适配器模式可以帮助解耦不同模块之间的依赖关系,使它们能够独立演化和维护。这有助于降低代码的耦合度,提高代码的可维护性和可扩展性。

  • 重用既有代码: 适配器模式允许我们在不修改现有代码的情况下,适应新的接口或需求。这使得我们能够重用既有的代码,减少重复劳动和开发成本。

  • 平滑迁移: 当项目需要进行迁移或升级时,适配器模式可以帮助我们平滑过渡。通过创建适配器,我们可以将旧的接口适配成新的接口,从而避免全面修改现有代码。

  • 灵活性和扩展性: 适配器模式可以为系统引入一层灵活的中间层,从而使得系统更具有扩展性。新的适配器可以轻松添加,以适应未来可能出现的变化。

2、缺点:

  • 引入复杂性: 使用适配器模式可能会引入一些额外的类和层级,增加代码的复杂性。开发者需要仔细权衡是否值得引入适配器来解决接口兼容性问题。

  • 运行时开销: 在运行时执行适配转换可能会引入一些运行时开销,特别是在大规模数据转换时。这可能会对应用的性能产生影响。

  • 设计合理性: 使用适配器模式时,需要确保适配器的设计合理性,以确保适配器类的职责清晰,并且不会引入额外的复杂性。

总结

适配器模式是前端开发中的一个重要设计模式,可以帮助我们解决不同接口之间的兼容性问题。通过创建适配器类,我们可以将不兼容的接口转换为可互操作的形式,实现模块之间的协同工作。在 TypeScript 中,适配器模式可以通过创建中间适配器类来实现,从而实现代码的解耦和重用。

在实际开发中,适配器模式常用于处理第三方库的接口兼容性问题、应对 API 的变更以及实现跨平台开发。然而,开发者需要在使用适配器模式时权衡其优缺点,确保其对项目的长期维护和可扩展性没有负面影响。

通过合理的设计和实践,适配器模式将成为前端开发中的有力工具,帮助我们更好地管理和整合不同模块和技术。

一川说

觉得文章不错的读者,不妨点个关注,收藏起来上班摸鱼的时候品尝。

欢迎关注笔者公众号「宇宙一码平川」,助你技术路上一码平川。

全部评论

相关推荐

程序员猪皮:看不到八股什么意思
点赞 评论 收藏
分享
头像
11-06 10:58
已编辑
门头沟学院 嵌入式工程师
双非25想找富婆不想打工:哦,这该死的伦敦腔,我敢打赌,你简直是个天才,如果我有offer的话,我一定用offer狠狠的打在你的脸上
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务