理解SDK架构:公共层与平台抽象层的设计原则
做跨平台SDK这几年,我最大的体会是:架构设计的好坏,直接决定了你能走多远。今天聊聊SDK内部的两个核心层次——公共层(Common Layer)和平台抽象层(Platform Abstraction Layer,简称PAL)。
说白了,公共层就是你只写一次的业务逻辑。平台抽象层则是你为每个平台单独实现的“适配器”。二者之间的边界划得越清楚,后续维护就越轻松。
公共层:你只写一次的业务逻辑
公共层是SDK的心脏。它负责所有与平台无关的核心功能。我习惯把它理解为一个“纯净的计算环境”——没有文件路径、没有网络接口、没有UI渲染,只有纯粹的算法和数据流转。
举个例子。一个跨平台登录SDK,公共层里应该放这些:
- 用户认证流程(登录、登出、刷新)
- 数据模型(User、Session、AuthResult)
- 协议解析(JSON/Protobuf序列化与反序列化)
- 状态机管理(登录态切换、超时处理)
我在项目中遇到过一种反面教材:有人把第三方SDK的初始化代码直接写在了公共层里。结果换了支付平台,整个核心逻辑都要拆了重写。嗯,这个坑千万别踩。
平台抽象层:隔离差异的防火墙
平台抽象层是公共层与具体操作系统之间的桥梁。它的职责只有一个:把平台差异封装成统一接口。
想一想,为什么需要这层?因为Windows的线程API、Android的Handler、iOS的GCD,实现思路完全不同。PAL就是这层“翻译官”。
我建议用纯虚类(接口)来定义PAL。例如:
// 平台抽象层定义
class IPlatformFileSystem {
public:
virtual ~IPlatformFileSystem() = default;
virtual bool CreateFile(const std::string& path) = 0;
virtual std::string GetAppDataDir() = 0;
};
// Windows 实现
class WinFileSystem : public IPlatformFileSystem {
public:
bool CreateFile(const std::string& path) override {
HANDLE h = CreateFileA(path.c_str(), ...);
return h != INVALID_HANDLE_VALUE;
}
std::string GetAppDataDir() override {
// 调用 Windows API
}
};
// Linux/macOS 实现
class PosixFileSystem : public IPlatformFileSystem {
// 完全不同的实现,但接口一致
};
你发现没有?公共层永远只跟 IPlatformFileSystem 打交道。它不知道也无需知道底层是Win还是Linux。
我踩过的坑是:早期做跨平台SDK时,把文件路径处理直接写死在公共层里。结果上架后发现iOS和Android的沙箱路径完全不一样,紧急打了补丁。从此以后,所有文件系统操作全部通过PAL路由。
| 维度 | 公共层 | 平台抽象层 |
|---|---|---|
| 依赖方向 | 不依赖任何平台API | 依赖具体OS API |
| 复用性 | 一套代码全平台复用 | 每平台单独实现 |
| 测试难度 | 单元测试易写,可mock | 需模拟器或真机 |
| 变动频率 | 低(业务稳定则少改) | 高(系统升级、新设备适配) |
设计原则:别让架构倒向一边
写了不少SDK后,我总结了几条原则,分享给你:
原则一:依赖倒置
公共层定义接口,PAL实现接口。公共层不直接#include任何PAL的具体实现。
怎么理解?就是公共层只持有IPlatformXxx*指针。初始化时由外部注入具体实现。这样一来,公共层永远不会因为平台升级而重新编译。
原则二:接口最小化
PAL接口越少越好。每个接口只做一件且必做之事。
举个例子。一个IPlatformStorage接口,别把读写文件、数据库操作、偏好设置全塞进去。拆成IPlatformFileSystem和IPlatformPreferences两个接口。为什么?因为不同平台的文件系统和偏好设置实现差异极大,混在一起容易让公共层逻辑跟着乱。
我曾经犯过的错:PAL接口参数里传了平台特有的结构体(比如Windows的HANDLE)。结果导致公共层被迫引入windows.h,整个跨平台设计功亏一篑。切记:PAL接口参数必须是跨平台类型(int、string、vector等)。
原则三:单一职责
每个PAL接口只封装一个维度的平台能力。
按照这个原则,你应该这样拆分:
IPlatformThread—— 线程与异步IPlatformNetwork—— 网络请求IPlatformUI—— 原生UI调用IPlatformSensor—— 传感器数据
你会发现,每个接口的实现文件都很瘦。Windows的线程接口可能就200行,iOS的线程接口也就150行。这才是健康的架构。
公共层与PAL的协作流程
下面的SVG图展示了完整的调用链路。应用层 -> 公共层 -> PAL接口 -> 具体实现。箭头代表了依赖方向和控制流向。
验证架构的“好”与“坏”
怎么判断你的PAL设计是否合理?我提供一个自检清单:
- 能不能替换? 把PAL实现从Windows切换到Linux,公共层代码一行不改吗?
- 接口稳定吗? 过去三个月,PAL接口头文件变动过几次?只有新增,没有删除或重命名,才算稳定。
- 测试友好吗? 单元测试时能不能用一个MockPlatform替换真实实现?能,说明PAL设计到位。
- 有没有平台泄漏? 搜索公共层代码,是否出现了#ifdef WIN32、__APPLE__等宏?如果有,说明平台细节渗入了核心逻辑,需要重构。
避坑指南:我曾经遇到一个团队,为了图省事,在公共层里直接写 #ifdef __ANDROID__ 来处理文件路径。结果后面增加鸿蒙支持时,所有分支都得改。花了三周才把耦合拆干净。相信我,平台宏就是架构的毒药,能不用就不用。
一句话总结
公共层是你的业务护城河,平台抽象层是护城河外的防火墙。把城墙砌在正确的边界上,比什么都重要。