详解TS的适配器模式

290 阅读7分钟

适配器模式(Adapter Pattern)是一种结构设计模式,通过在现有类周围提供 "封装 "或 "适配器",可以使不兼容的接口协同工作。

背景

在现实中的支付网关或第三方服务通常会设计自己独特的接口,这就导致无法直接互换使用。而适配器模式正是解决这些问题的利器。

假设您有一个现有系统依赖的支付网关接口 PayPal,后来业务需要支持 Stripe。直接重构整个系统去适配新接口不仅成本高,而且容易引入新的错误。这时适配器模式可以帮助我们无缝集成不同支付网关,且不影响现有代码。

该服务当前使用 PayPalPaymentProcessor 处理付款。需求需要支持 Stripe 等其他支付提供商,因此需要实现一个新的支付网关。我们姑且称之为 StripePaymentProcessor,但它的接口与现有的 PayPalPaymentProcessor 不同。

步骤 1:定义目标接口

PaymentProcessor 接口是对外提供服务的目标接口。

interface PaymentProcessor {
    pay(amount: number): void;
}

步骤 2:现有 PayPal 支付处理器

这是应用程序已经使用的现有支付处理器。正如你所看到的,PayPalPaymentProcessor 实现了 PaymentProcessor 目标接口中的 pay() 方法,所有支付处理器类都将实现该方法。

class PayPalPaymentProcessor implements PaymentProcessor {
    public pay(amount: number): void {
        console.log(`Paid $${amount} using PayPal.`);
    }
}

步骤 3:新的 Stripe 支付处理器

这就是你要集成的新支付处理器,但它有一个不同的接口,有一个 PaypalPaymentProcessor 中没有的新方法 makePayment()

class StripePaymentProcessor {
    public makePayment(amountInCents: number): void {
        console.log(`Paid $${amountInCents / 100} using Stripe.`);
    }
}

步骤 4:创建适配器

因此,为了在应用程序中实现这一功能,我们需要实现一个适配器。这个新的适配器类实现了目标接口(PaymentProcessor),并封装了新的 StripePaymentProcessor。它基本上是将 StripePaymentProcessor 的接口转换成应用程序能理解的接口。

class StripePaymentAdapter implements PaymentProcessor {
    private stripePaymentProcessor: StripePaymentProcessor;

    constructor(stripePaymentProcessor: StripePaymentProcessor) {
        this.stripePaymentProcessor = stripePaymentProcessor;
    }

    public pay(amount: number): void {
        // Convert dollars to cents and call the Stripe payment method
        this.stripePaymentProcessor.makePayment(amount * 100);
    }
}

步骤 5:在应用程序中使用适配器

现在,应用程序可以通过适配器使用 StripePaymentProcessor,而无需更改现有代码库。

// Application code
const amountToPay = 100; // $100

// 使用现有的PayPal支付处理器
const paypalProcessor: PaymentProcessor = new PayPalPaymentProcessor();
paypalProcessor.pay(amountToPay);

// 通过适配器使用新的Stripe支付处理器
const stripeProcessor: PaymentProcessor = new StripePaymentAdapter(new StripePaymentProcessor());
stripeProcessor.pay(amountToPay);

输出:

Paid $100 using PayPal.
Paid $100 using Stripe.

要点解析:

  • PaymentProcessor 接口是两个支付处理器都需要遵守的目标接口。
  • PayPalPaymentProcessor 类已经实现了这个接口。
  • StripePaymentProcessor 类有不同的接口,因此我们创建了 StripePaymentAdapter,使其适应 PaymentProcessor 接口。

会不会觉得多此一举?为何不让StripePaymentProcessor 直接实现PaymentProcessor的接口呢?

这是一个很好的问题!

让 StripePaymentProcessor 直接实现 PaymentProcessor 虽然可以解决兼容性问题,但会带来以下缺点:

  • 破坏开闭原则和单一职责原则。
  • 增加类的耦合性,降低灵活性。
  • 不利于第三方代码的集成。
  • 增加了系统复杂度,不利于维护。

适配器模式的设计初衷就是避免直接修改现有类,而增加一个中间层实现兼容,保持系统的灵活性和可扩展性。类本来就应对扩展开放,对修改封闭。如果一个类被频繁的修改或者迭代,那必然会导致代码的膨胀,历史逻辑的堆积,功能职责臃肿。

由于采用了适配器模式,应用程序现在可以使用任一种支付处理器,而无需对现有代码进行任何修改。

我们可以再封装一个调用函数:

function processPayment(paymentProcessor: PaymentProcessor, amount: number): void {
    paymentProcessor.pay(amount);
}

// Example usage
const amountToPay = 100; // $100

// 使用现有的PayPal支付处理器
const paypalProcessor: PaymentProcessor = new PayPalPaymentProcessor();
processPayment(paypalProcessor, amountToPay);

// 通过适配器使用新的Stripe支付处理器
const stripeProcessor: PaymentProcessor = new StripePaymentAdapter(new StripePaymentProcessor());
processPayment(stripeProcessor, amountToPay);

这是一个在项目中应用适配器模式的例子。真实的业务可能更复杂,但它能让你深入了解在哪些情况下应该考虑使用适配器模式。

更丰富的适配器

上面的适配器是为了统一接口,它还可以增加灵活性,比如在适配器中添加一些数据校验逻辑、日志记录、错误处理、或将接口与配置文件集成等。

  • 多支付网关:一个系统支持 PayPal、Stripe、WeChat Pay 等多种支付接口。
  • 多日志系统:支持多种日志记录工具(如 Log4j、Winston、Bunyan)。
  • 多存储系统:支持本地存储、云存储(如 AWS S3、Google Cloud Storage)。
  • 多第三方服务集成:如邮件服务(支持 SendGrid、Mailgun 等)。

多适配器的时候,我们可以引入工厂模式统一管理,通过工厂类,根据支付场景或参数动态生成适配器实例。

// 定义支付适配器的目标接口
interface PaymentProcessor {
    pay(amount: number): void;
}

// 工厂类管理多个适配器
class PaymentAdapterFactory {
    static getPaymentProcessor(type: string, version?: string): PaymentProcessor {
        if (type === 'alipay') {
            if (version === 'v1') {
                return new AlipayV1Adapter(new AlipayV1Processor());
            } else if (version === 'v2') {
                return new AlipayV2Adapter(new AlipayV2Processor());
            }
        } else if (type === 'stripe') {
            return new StripeAdapter(new StripeProcessor());
        }
        throw new Error(`Unsupported payment type: ${type}`);
    }
}

// 使用工厂获取适配器
const alipayProcessorV1 = PaymentAdapterFactory.getPaymentProcessor('alipay', 'v1');
alipayProcessorV1.pay(100);

const alipayProcessorV2 = PaymentAdapterFactory.getPaymentProcessor('alipay', 'v2');
alipayProcessorV2.pay(200);

适配器还可以嵌套使用,适配器嵌套是指一个适配器的实现中依赖另一个适配器,从而形成适配器链条或嵌套关系。通过这种方式,可以逐步将多个接口或逻辑适配到目标接口上。适配器嵌套通常用于处理复杂的转换需求,或者在已有适配器的基础上进行二次封装以满足新的业务需求。

class StripeAdapter implements PaymentProcessor {
    private stripeProcessor: StripePaymentProcessor;

    constructor(stripeProcessor: StripePaymentProcessor) {
        this.stripeProcessor = stripeProcessor;
    }

    public pay(amount: number): void {
        this.stripeProcessor.makePayment(amount * 100); // 转换为分
    }
}

class CurrencyFormatterAdapter implements PaymentProcessor {
    private paymentProcessor: PaymentProcessor;

    constructor(paymentProcessor: PaymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public pay(amount: number): void {
        const formattedAmount = parseFloat(amount.toFixed(2)); // 保留两位小数
        console.log(`Formatted amount: $${formattedAmount}`);
        this.paymentProcessor.pay(formattedAmount);
    }
}

const stripeProcessor = new StripeAdapter(new StripePaymentProcessor());
const formattedStripeProcessor = new CurrencyFormatterAdapter(stripeProcessor);

// 使用嵌套适配器
formattedStripeProcessor.pay(123.456); 
// 输出:
// Formatted amount: $123.46
// Paid $123.46 using Stripe.

注意,每个适配器只负责一个具体的转换逻辑,不要让适配器变得过于复杂。对于复杂的场景,务必要考虑合理的嵌套顺序,确保保证逻辑清晰,以及必要的日志确保调试的便捷性。

适配器模式的误区

适配器模式虽然是设计模式中的重要一环,但在实际使用中可能存在一些常见误区,需要注意避免:

1. 适配器模式总是有益的,可以无条件使用

不可否认适配器模式会增加系统复杂性,复杂性主要是来源于过多的嵌套调用,所以需要我们去控制适配器的数量。在设计时,优先考虑统一接口设计,从源头上减少不兼容问题。避免因引入过多的适配器会让系统变得难以维护和调试,尤其是在适配器嵌套使用的情况下。

2. 适配器模式就是简单地把一个类封装到另一个类中

适配器模式不仅仅是封装,更重要的是转换接口,让两个不兼容的接口能够协同工作。普通封装只关注隐藏内部实现细节,而适配器更关注接口的兼容性和适应性。

3. 适配器模式能解决所有兼容性问题

适配器模式只能解决接口不兼容的问题,对于逻辑不兼容、业务差异等更深层次的问题,适配器模式无能为力。例如支付接口要求传递加密的用户数据,而原系统的接口只处理未加密数据。这种场景下,适配器无法简单解决,因为业务逻辑差异较大。

对于复杂业务差异,可能需要结合其他设计模式(如装饰器模式)或调整业务逻辑。

4. 一旦实现适配器,就不需要再进行维护和更新

随着系统需求的变化,接口可能会发生变动,适配器需要不断调整。如果没有及时更新适配器,可能会导致运行时错误或功能失效。

对于第三方库,我们应该及时跟踪第三方库的版本变化,确保适配器逻辑与新版本兼容。同时为适配器编写全面的测试用例,确保其功能正确性。

5. 适配器模式会降低性能

使用适配器模式必然会引入额外的性能开销,但这种性能开销通常很小,更多是逻辑层次的接口转换。性能影响在绝大多数场景下是可以忽略的,但如果适配器需要频繁调用或在高性能场景中使用,确实可能带来一定的开销。

6. 使用适配器会削弱代码的可读性

如果设计得当,适配器不仅不会削弱可读性,反而会使系统的接口更加统一,提升代码的清晰度。对于适配器类的命名应清晰表明其功能和适配关系。并添加文档或注释,说明适配器的作用及使用方法。

7. 可以随意选择适配器模式

适配器模式并非推荐大家随时随地使用,随意使用适配器模式可能会掩盖设计问题,导致系统架构不合理。适配器模式应作为补救措施而非首选方案。优先考虑重构,当代码结构混乱时,尽量通过重构优化接口设计,而不是滥用适配器。只有当现有接口无法修改或替换时,才使用适配器。

总结

适配器模式的本质是解决接口不兼容问题,正确使用可以显著提高代码的灵活性和复用性。尤其是在以下情况下,请使用适配器模式:

  • 遗留代码:在不修改现有代码的情况下,将新系统或程序库纳入现有代码库。

  • 第三方库:封装第三方库或应用程序接口,使其与您的应用程序兼容。

  • 多种实现:支持服务的多种实现方式(如支付处理器),而无需更改客户端代码。

  • 单一责任:从现有类中卸载数据转换或方法调整的责任。

适配器模式与生活中最接近的案例就是电子产品的电源适配器了。

电源适配器的作用是将不同电压或插头标准的电源转换成目标设备可以使用的形式。例如,一台笔记本电脑可能需要 19V 的直流电,而墙上的插座输出的是 220V 的交流电。如果没有电源适配器,笔记本无法直接使用插座的电力。

同样,在软件开发中,适配器模式就像是这个“电源适配器”,它的任务是将不兼容的接口“转换”为系统可以接受的格式,而无需修改原有的类或系统设计。

如果没有适配器,我们可能需要重新设计整个电源系统,或者修改每个目标设备的内部结构,既耗时又复杂。适配器模式则提供了一种简单、优雅的解决方案,使得我们可以无缝整合不同的类或系统,同时保持代码的解耦与灵活性。

正如电源适配器让各种设备都能从同一个插座获得电力,软件中的适配器模式也能帮助我们在复杂系统中实现兼容性与扩展性,为开发者带来更多便捷和可能性。