3、适配器模式详解:类适配器、对象适配器、接口适配器、在跨平台中的应用场景

适配器模式,说白了就是「让不兼容的东西能一起工作」。

我刚开始做跨平台开发时,最头疼的就是不同平台的接口差异。iOS 的推送回调、Android 的权限申请、Windows 的窗口句柄……每个平台都有自己的脾气。适配器模式就是那个「翻译官」,帮我们把各平台的方言翻译成统一的普通话。

3.1 为什么需要适配器?

你想想看,我们写业务代码时,肯定不希望每个平台都写一套逻辑。理想状态是:一套核心代码,适配所有平台。但现实是,每个平台的 SDK 接口长得都不一样。

举个例子:

  • iOS 的本地存储用 UserDefaults
  • Android 用 SharedPreferences
  • Windows 用 Registry

如果业务代码直接调用这些平台 API,那代码里全是 #ifdef 和 if (platform == iOS),维护起来简直噩梦。

核心思想:适配器模式通过一个中间层,将客户端的调用转换成目标接口能理解的形式。客户端只跟适配器打交道,适配器负责跟具体平台沟通。

3.2 三种适配器模式

适配器模式有三种常见实现方式。我分别讲讲它们的区别和适用场景。

3.2.1 类适配器(通过继承)

类适配器使用多重继承来实现。适配器同时继承目标接口和源类。

// 目标接口(客户端期望的)
class IStorage {
public:
    virtual void save(const string& key, const string& value) = 0;
    virtual string load(const string& key) = 0;
};

// 源类(iOS 的 UserDefaults)
class UserDefaults {
public:
    void setObject(const string& key, const string& value);
    string objectForKey(const string& key);
};

// 类适配器
class StorageAdapter : public IStorage, private UserDefaults {
public:
    void save(const string& key, const string& value) override {
        setObject(key, value);  // 直接调用父类方法
    }
    string load(const string& key) override {
        return objectForKey(key);
    }
};

我个人习惯在以下场景用类适配器:

  • 源类的接口比较简单,不需要重写太多方法
  • 适配器需要直接访问源类的 protected 成员
  • 语言支持多重继承(比如 C++)

注意:Java、C# 等单继承语言无法使用类适配器。我在跨平台项目中更常用对象适配器,因为 C++ 虽然支持多重继承,但容易引发菱形继承问题。

3.2.2 对象适配器(通过组合)

对象适配器使用组合而非继承。适配器内部持有源对象的引用。

// 对象适配器
class StorageAdapter : public IStorage {
private:
    UserDefaults* m_userDefaults;  // 持有源对象
public:
    StorageAdapter(UserDefaults* ud) : m_userDefaults(ud) {}
    
    void save(const string& key, const string& value) override {
        m_userDefaults->setObject(key, value);
    }
    string load(const string& key) override {
        return m_userDefaults->objectForKey(key);
    }
};

对象适配器的好处很明显:

  • 灵活性高,运行时可以切换不同的源对象
  • 不依赖继承,适用于所有面向对象语言
  • 可以适配多个不同的源类

我在项目中遇到过这样的情况:同一个存储接口,在 iOS 上需要适配 UserDefaults,在 Android 上需要适配 SharedPreferences。用对象适配器,我只需要传入不同的源对象实例即可。

3.2.3 接口适配器(默认适配器)

接口适配器也叫「缺省适配器」。当接口方法太多,而我们只需要实现其中一部分时,可以用一个抽象类提供默认实现。

// 一个有很多方法的接口
interface IPlatformService {
    void init();
    void login();
    void logout();
    void pay(String orderId, double amount);
    void share(String content);
    void trackEvent(String eventName);
}

// 接口适配器:提供空实现
abstract class PlatformAdapter implements IPlatformService {
    @Override public void init() {}
    @Override public void login() {}
    @Override public void logout() {}
    @Override public void pay(String orderId, double amount) {}
    @Override public void share(String content) {}
    @Override public void trackEvent(String eventName) {}
}

// 具体实现:只重写需要的方法
class iOSPlatform extends PlatformAdapter {
    @Override
    public void init() {
        // iOS 初始化逻辑
    }
    @Override
    public void pay(String orderId, double amount) {
        // 调用 Apple Pay
    }
}

嗯,这里要注意:接口适配器不是用来解决「不兼容」问题的,而是用来简化接口实现的。我在写跨平台 SDK 时经常用这个模式,让各平台只关注自己需要实现的方法。

3.3 在跨平台中的应用场景

适配器模式在跨平台开发中无处不在。我挑几个典型的场景说说。

3.3.1 平台桥接层

这是最直接的应用。我们定义一个统一的接口,然后为每个平台写一个适配器。

统一接口 iOS 适配器 Android 适配器 Windows 适配器
getDeviceId() [[UIDevice currentDevice] identifierForVendor] Settings.Secure.ANDROID_ID WMI 查询
showToast(msg) UIAlertController Toast.makeText() MessageBox
getBatteryLevel() UIDevice.batteryLevel BatteryManager GetSystemPowerStatus

我曾经用这种方式重构过一个项目。原来代码里到处都是 #ifdef __APPLE__,改完以后,核心代码零平台判断,所有差异都封装在适配器里。

3.3.2 第三方 SDK 适配

不同平台的推送服务、支付服务、社交分享服务,接口千差万别。适配器模式可以把它们统一起来。

// 统一推送接口
interface IPushService {
    void register();
    void unregister();
    void setAlias(String alias);
    void onMessageReceived(String message);
}

// 极光推送适配器
class JPushAdapter implements IPushService {
    @Override
    public void register() {
        JPushInterface.init(context);
    }
    // ...
}

// 个推适配器
class GetuiAdapter implements IPushService {
    @Override
    public void register() {
        PushManager.getInstance().initialize(context);
    }
    // ...
}

这样做的好处是:哪天想换推送服务商,只需要换一个适配器实现,业务代码完全不用动。

3.3.3 旧代码兼容

项目迭代过程中,接口经常变化。适配器模式可以让我们在不修改旧代码的情况下,让新旧接口共存。

我的经验:在重构遗留代码时,我习惯先写一个适配器,让新代码调用适配器,适配器内部调用旧接口。等所有调用方都迁移完成后,再逐步替换适配器内部的实现。这样风险最小。

3.4 适配器模式的核心逻辑

下面这张图展示了适配器模式在跨平台中的整体结构。客户端只看到统一的接口,适配器负责跟各平台的具体实现打交道。

客户端代码 统一接口 适配器层(对象适配器) iOS 适配器 Android 适配器 Windows 适配器 各平台原生 SDK UserDefaults SharedPreferences Registry 客户端只依赖统一接口,适配器封装平台差异

3.5 避坑指南

适配器模式虽然好用,但用不好也会踩坑。我分享几个曾经踩过的坑。

我曾经犯过的错:在一个项目中,我为了「灵活」,给每个平台都写了一个适配器,结果适配器数量膨胀到 20 多个。后来发现很多适配器只是参数不同,逻辑完全一样。正确的做法是先抽象出公共逻辑,再用策略模式或模板方法模式减少重复代码。

另外几个常见问题:

  • 过度适配:不要为了适配而适配。如果两个平台的接口差异太大,强行适配反而会让代码变得奇怪。这时候可以考虑用外观模式或桥接模式。
  • 性能损耗:适配器会引入一层间接调用。在性能敏感的场景(比如游戏渲染循环),要谨慎使用。
  • 接口膨胀:适配器接口不要设计得太大。我见过一个适配器接口有 30 多个方法,每个平台实现起来都痛苦不堪。保持接口小而专一。

我的建议:开始写适配器之前,先花时间把统一接口设计好。接口设计得合理,后面写适配器就是体力活。接口设计得不好,后面改起来就是灾难。

适配器模式是跨平台开发的基石之一。它不复杂,但用好了能让代码结构清晰很多。记住一个原则:让适配器去适应平台,而不是让业务代码去适应平台。

可可精品资料库