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 适配器模式的核心逻辑
下面这张图展示了适配器模式在跨平台中的整体结构。客户端只看到统一的接口,适配器负责跟各平台的具体实现打交道。
3.5 避坑指南
适配器模式虽然好用,但用不好也会踩坑。我分享几个曾经踩过的坑。
我曾经犯过的错:在一个项目中,我为了「灵活」,给每个平台都写了一个适配器,结果适配器数量膨胀到 20 多个。后来发现很多适配器只是参数不同,逻辑完全一样。正确的做法是先抽象出公共逻辑,再用策略模式或模板方法模式减少重复代码。
另外几个常见问题:
- 过度适配:不要为了适配而适配。如果两个平台的接口差异太大,强行适配反而会让代码变得奇怪。这时候可以考虑用外观模式或桥接模式。
- 性能损耗:适配器会引入一层间接调用。在性能敏感的场景(比如游戏渲染循环),要谨慎使用。
- 接口膨胀:适配器接口不要设计得太大。我见过一个适配器接口有 30 多个方法,每个平台实现起来都痛苦不堪。保持接口小而专一。
我的建议:开始写适配器之前,先花时间把统一接口设计好。接口设计得合理,后面写适配器就是体力活。接口设计得不好,后面改起来就是灾难。
适配器模式是跨平台开发的基石之一。它不复杂,但用好了能让代码结构清晰很多。记住一个原则:让适配器去适应平台,而不是让业务代码去适应平台。