1. 符合模式到底是个什么模式
先说个容易劝退新人的事:你搜"符合模式"四个字,大概率搜不到标准定义,搜到的多半是被纠正成"适配器模式(Adapter Pattern)"。别怀疑自己记错了,这俩就是一个东西。
"符合模式"是早年某些中文译法里对 Adapter 一词的直译残留,Gang of Four 在《设计模式》里把 Adapter Pattern 正式译为"适配器模式",但早年间各种培训视频、内部文档里"符合模式""适应模式"的说法混着传,一直传到现在。所以你在团队代码注释里看到"符合模式"三个字,别觉得奇怪,它和适配器模式是同一件事。
适配器模式的核心思想只用一句话就能讲透:把一个类的接口转换成客户端期望的另一个接口,让原本因为接口不匹配而无法协作的类能够一起工作。
生活里最好理解的就是电源转换插头。你从别的国家带回来的笔记本,插头是两脚扁脚,国内墙插是三脚圆孔,直接怼不进去,中间垫一个转换插座,两头就都通上了。那个转换插座就是适配器——它不改动电器本身,也不改动墙插,只是把两边接口差异翻译掉。
在 Windows 编程里,适配器模式的应用比你想的普遍得多。甚至可以这么说:整个 Win32 API 的面向对象封装史,就是一部符合模式的演进史。
今天你用 C++ 写std::fstream读写文件,完全感觉不到底层在走CreateFile、ReadFile、WriteFile这一整套 C 风格 API——中间那层标准库实现,本质上就是一个巨大的适配器。你用 MFC 时CWnd包着HWND,你用 WPF/WinForms 时控件对象内部持有窗口句柄,你用 Qt 时它把原生事件循环翻译成自己的信号槽机制——全是同一个模式在换皮。
所以这篇就专门拆一拆:符合模式在 Windows 编程里到底解决什么问题、核心实现长什么样、实际项目里哪些坑是没写够适配层才踩进去的。看懂这一篇,你再看那些"封装 Win32 API"的开源库,整个实现思路都是通的。
2. 为什么 Windows 编程如此依赖符合模式
2.1 Win32 API 是纯 C 过程式接口,和面向对象天生拧巴
Windows 原生 API 是几十年前的 C 语言设计。没有类、没有封装、没有命名空间、没有异常,有的只是一堆全局函数和裸指针。打开任意一个windows.h头文件,满眼都是这种东西:
HANDLE hFile = CreateFileW(L"test.txt", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);资源全部用HANDLE表示,本质是void*,你传错句柄类型编译器根本拦不住。注册表是HKEY,窗口是HWND,事件是HANDLE,文件也是HANDLE,虽然类型名不同但底层都是指针。而且返回值含义不一致:文件句柄失败返回INVALID_HANDLE_VALUE(注意不是nullptr),窗口句柄失败返回nullptr,内存分配失败返回nullptr……每个 API 都有自己的"失败返回值约定",这本身就是在逼你写一个适配层来统一错误处理。
面向对象的代码习惯是什么?资源该在构造函数里获取、析构函数里释放,方法调用有明确的接收者,异常能顺着调用栈往上抛。这两者之间如果不垫一层适配,你的业务代码会被GetLastError()、CloseHandle()这些细节搞得千疮百孔。我自己见过很多项目里同一个文件操作逻辑散落七八处,每处都是CreateFile后忘掉CloseHandle,这在没有 RAII 概念的过程式 API 下几乎是必然结果。
2.2 一个功能两套 API:Windows 的双接口设计本身就是适配
Windows 从 NT 时代起,几乎所有涉及字符串的 API 都有两个版本:带A后缀的 ANSI 版(如MessageBoxA)和带W后缀的宽字符版(如MessageBoxW)。MessageBox这个不带后缀的名字,实际上是预处理宏,编译时根据你是否定义了UNICODE宏来决定展开成哪个。
这套宏适配机制就是编译期适配器:
#ifdef UNICODE #define MessageBox MessageBoxW #else #define MessageBox MessageBoxA #endif你写代码时可以假装只有一个统一接口,编译系统帮你决定到底走哪套底层实现。这听起来很方便,但新人常在这里翻车——某个第三方库内部用的是 ANSI 版本接口,你传进去一个宽字符串,链接仍然能过,运行却输出乱码或者直接崩溃。
还有一套重要的"兼容性适配":Windows 提供GetProcAddress动态获取函数地址,允许你在一个平台上同时支持新旧两代 API。比如某些系统函数在新版本才可用,你不能直接静态链接,就得运行时LoadLibrary+GetProcAddress去取地址,取不到就回退到旧方案。这本质上也是一个运行时适配器。这类技巧在写插件、写通用工具时非常常用。
2.3 各种 UI 框架互认窗口句柄,翻译工作需要适配层
Windows 上的 UI 框架多到令人发指:原生 Win32、MFC、WTL、Qt、WPF、WinForms、wxWidgets……它们实现机制完全不同,但都有一个共通的锚点:HWND窗口句柄。
MFC 里是CWnd包着HWND,消息通过消息映射宏(ON_MESSAGE、ON_COMMAND)分发到各成员函数;Qt 里是QWidget包着底层窗口,事件先走原生消息循环再被翻译成QEvent进入信号槽;WPF 里你甚至可以创建一个HwndSource对象,把传统 Win32 窗口嵌进 WPF 的视觉树里。
当你需要在不同框架之间互通时,没有适配器几乎寸步难行。比如你在 WPF 应用里要嵌入一个基于 Win32 的第三方控件,就得靠HwndSource这个官方适配器;你要在 MFC 框架里调用 WPF 的控件,得用HwndHost。这些官方辅助类本质上都是符合模式的实现——不修改两边的接口,通过中间层完成通信。
我自己印象最深的一次:某项目需要一个自绘控件,用了 WPF 实现,但宿主程序是 MFC 老框架。当时就是靠HwndHost把 WPF 内容包成一个可以被 MFC 消息系统接受的HWND子窗口,两边互不知道对方的存在,但协作得好好的。这就是适配层的价值——让两个语言不通的模块各走各的,中间一个翻译官解决问题。
2.4 过程式 API 与面向对象代码之间的"翻译工序"
Win32 API 的调用方式极其过程化。以创建一个窗口为例,你先得注册窗口类(RegisterClassEx),再创建窗口(CreateWindowEx),然后手动进入消息循环(GetMessage/DispatchMessage),窗口收到消息后又靠一个全局的回调函数WNDPROC处理。整个过程和"创建对象→调用方法”的思维模式差距极大。
面向对象代码想要的窗口对象是这样的:window.Show()、window.SetTitle(L"hello")、window.OnClick(handler)。从 Win32 API 到这种形态,中间需要的不是一行两句的简单映射,而是对整个消息循环和回调机制的再封装。
以 WPF 为例,它内部把 Win32 的消息循环封装成了Dispatcher,把WM_PAINT、WM_SIZE等消息映射成了布局和渲染事件,把WM_COMMAND映射成了命令路由。你写 WPF 时永远不需要碰WNDPROC,因为聚集器(aggregator)式的大型适配层帮你把这些细节全吃了。理解这一点,你在排查"WPF 程序为什么收不到某个 Win32 消息"时会少走很多弯路——你会直接想到去HwndSource上挂AddHook,而不是在 XAML 里瞎找。
3. 从零实现一个符合模式封装:以文件句柄 RAII 为例
3.1 不用符合模式的代码长什么样
先看一段典型的、没有做任何适配的原始代码。假设你要读取一个配置文件:
void ReadConfigOldWay() { HANDLE hFile = CreateFileW(L"config.ini", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { // 处理失败,但你想知道为什么失败就得调 GetLastError() DWORD err = GetLastError(); wprintf(L"Open failed: %lu\n", err); return; } char buffer[4096]; DWORD bytesRead = 0; BOOL ok = ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, nullptr); if (!ok) { DWORD err = GetLastError(); wprintf(L"Read failed: %lu\n", err); CloseHandle(hFile); // 容易忘 return; } // 正常处理 buffer ... CloseHandle(hFile); // 还得记得关句柄 }这段代码的问题非常典型:
- 异常安全问题:如果
buffer处理过程中抛出异常,CloseHandle(hFile)永远不会执行,句柄泄漏。 - 错误处理重复:
GetLastError()的调用分散在各处,错误码没有一个统一的存储位置。 - 接口语义不统一:文件句柄失败返回
INVALID_HANDLE_VALUE,但如果你在别处用nullptr判断,就漏判了。 - 重复代码:每次打开文件都要写一整套"打开→判断→使用→关闭"流程。
这还只是一个文件,扩大到注册表、套接字、事件、内存映射文件,你会发现 90% 的代码都在重复同样的事。
3.2 用符合模式封装成 RAII 对象
我们现在做一个适配器,把原始的 Win32 文件句柄接口包装成符合 C++ 资源管理习惯的类。核心目标有三个:生命周期自动管理、错误码统一存储、接口贴近现代 C++ 习惯。
class WinFile { public: WinFile() noexcept : handle_(INVALID_HANDLE_VALUE), lastError_(ERROR_SUCCESS) {} // 打开文件的适配方法,内部隐藏 CreateFile 的各种细节 bool Open(const std::wstring& path, DWORD accessMode = GENERIC_READ, DWORD shareMode = FILE_SHARE_READ) { CloseIfNeeded(); handle_ = CreateFileW(path.c_str(), accessMode, shareMode, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle_ == INVALID_HANDLE_VALUE) { lastError_ = GetLastError(); return false; } lastError_ = ERROR_SUCCESS; return true; } // 读取数据的适配方法,屏蔽 ReadFile 的 DWORD 与 size_t 差异 size_t Read(void* buffer, size_t maxBytes) noexcept { if (handle_ == INVALID_HANDLE_VALUE) return 0; DWORD bytesRead = 0; if (!::ReadFile(handle_, buffer, static_cast<DWORD>(maxBytes), &bytesRead, nullptr)) { lastError_ = GetLastError(); return 0; } lastError_ = ERROR_SUCCESS; return static_cast<size_t>(bytesRead); } DWORD LastError() const noexcept { return lastError_; } bool IsOpen() const noexcept { return handle_ != INVALID_HANDLE_VALUE; } // 移动语义:句柄所有权跟着对象走 WinFile(WinFile&& other) noexcept : handle_(other.handle_), lastError_(other.lastError_) { other.handle_ = INVALID_HANDLE_VALUE; other.lastError_ = ERROR_SUCCESS; } WinFile& operator=(WinFile&& other) noexcept { if (this != &other) { CloseIfNeeded(); handle_ = other.handle_; lastError_ = other.lastError_; other.handle_ = INVALID_HANDLE_VALUE; other.lastError_ = ERROR_SUCCESS; } return *this; } // 禁止拷贝 WinFile(const WinFile&) = delete; WinFile& operator=(const WinFile&) = delete; ~WinFile() noexcept { CloseIfNeeded(); } private: void CloseIfNeeded() noexcept { if (handle_ != INVALID_HANDLE_VALUE) { ::CloseHandle(handle_); // 统一关闭入口,调一次就行 handle_ = INVALID_HANDLE_VALUE; } } HANDLE handle_; DWORD lastError_; };适配后的调用方式变成这样:
void ReadConfigNewWay() { WinFile file; if (!file.Open(L"config.ini")) { wprintf(L"Open failed: %lu\n", file.LastError()); return; } std::string buffer(4096, '\0'); size_t n = file.Read(buffer.data(), buffer.size()); if (n == 0 && file.LastError() != ERROR_SUCCESS) { wprintf(L"Read failed: %lu\n", file.LastError()); return; } // 正常处理 buffer,不需要手动关闭文件 }看出来区别没有?关句柄这件事彻底消失了。函数走到哪一步 return 都不会泄漏,因为WinFile析构函数一定会执行。这就是 RAII 适配的核心价值——让资源管理从"人工记着做"变成"编译器替你保证"。
3.3 为什么 RAII 封装是符合模式的"最佳形态"
你可能会说:这个类不过是把CreateFile、CloseHandle包了一层函数,算适配器吗?算,而且是最经典的适配器。
理由在于:它把原本面向过程的接口(打开→判断句柄→使用→手动关闭)转换成了面向对象的形态(构造→方法调用→自动析构)。接口转换正是符合模式的定义。调用方不再关心句柄的无效值语义、不再关心错误码该去哪取、不再关心资源释放时机,这些全部由适配层消化掉了。
在设计这类适配器时有几个经验点值得说:
- 失败状态的保留:Win32 API 的错误状态是一次性的消息残留,
GetLastError()返回后如果被其他 API 覆盖就丢了。适配器要在失败发生的当下立刻读取并保存到成员变量里,调用方随时可以查询,这个细节极其重要。 - 移动语义是必须的:现代 C++ 容器、
std::optional等工具依赖移动构造和移动赋值,你封装的资源类如果不支持移动,就无法放进std::vector,无法安全返回。而移动语义实现的前提,就是句柄用INVALID_HANDLE_VALUE表示"空状态",这样才能保证移动后源对象可安全析构。 - 禁止拷贝而不是隐式浅拷贝:如果两个
WinFile对象持有同一个HANDLE,析构时会释放两次,这是 Undefined Behavior。直接把拷贝构造/赋值 delete 掉是最安全的做法。 - 析构不要抛异常:
CloseHandle本身不抛异常,但析构函数加noexcept声明能避免编译器在栈展开时遇到二次异常直接terminate的隐患。
4. 进阶实战:在 COM 互操作中用符合模式驯服接口
4.1 COM 接口的本质是"二进制合约"
如果说 Win32 API 是过程式接口,那 COM(组件对象模型)就是 Windows 世界里另一套"语言无关的接口约定"。COM 接口的定义方式非常特殊:一个 COM 对象暴露的每个接口,本质上是一张指向虚函数表的指针,而这张虚表的内存布局是写死在 ABI(应用二进制接口)层面的。
每个 COM 接口都从IUnknown派生,IUnknown有三个方法:QueryInterface、AddRef、Release。QueryInterface负责"问对象要其它接口",AddRef/Release负责引用计数。这套契约是为了解决跨模块、跨语言的对象生命周期问题——不管调用方是 C++、C#、VB 还是 Delphi,只要懂这个虚表布局,就能调用。
问题来了:C++ 类的成员函数调用约定和虚函数表和 COM 接口并不天然一致。C++ 类默认没有引用计数,虚表布局在大多数编译器上是"非标准但事实兼容"的(在 Windows 上 MSVC 布局和 COM 刚好兼容,所以你能用interface关键字模拟)。但如果你要在自己的 C++ 类里实现一个 COM 接口,光靠"继承"还不够,你得自己实现AddRef、Release的生命周期管理,这就是需要适配的地方。
4.2CComPtr就是最常见的 COM 适配器
裸 COM 指针的写法是痛苦的:
IUnknown* pUnknown = nullptr; HRESULT hr = CoCreateInstance(CLSID_MyClass, nullptr, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown); if (SUCCEEDED(hr) && pUnknown) { // 用完必须 Release pUnknown->Release(); }一旦中间有函数提前 return,或者有人忘了Release,就会造成 COM 对象泄漏,而且这种泄漏很难查。你以为Release是"删除对象"的函数?它只是引用计数减一,只有计数归零才真正释放。于是多层引用传递时,数不清谁持有引用,成了 COM 程序最常见的崩溃来源。
解决方案是用标准库里常用的适配器模式——把裸指针包成智能指针:
#include <atlbase.h> // 用 CComPtr 包装,Release 由智能指针自动调用 CComPtr<IXMLDOMDocument> spDoc; HRESULT hr = spDoc.CoCreateInstance(CLSID_DOMDocument); if (SUCCEEDED(hr)) { // 用 spDoc 调接口,不再手动 Release }CComPtr<T>就是 COM 的符合模式实现——它把Release从"你必须记得做的事"变成了"离开作用域自动做的事"。它和标准库的shared_ptr相似,但适配的是 COM 引用计数语义。也正因为是适配器,用CComPtr时永远没有"与你无关的裸 COM 接口指针",因为裸指针不会自动管理你的生命周期。
4.3 把 COM 事件回调适配成现代 C++ 回调
如果说CComPtr是生命周期的适配器,那事件回调的适配就更考设计能力。
很多 COM 组件通过"连接点"(Connection Point)机制向外部发送事件。它要求你实现一个IDispatch接口,然后调用方组件通过IConnectionPoint::Advise把你的接口拿过去,内部用Invoke触发事件。
IDispatch接口总共四个方法,但核心就一个Invoke。如果你要监听某个 COM 组件的OnProgress事件,你得写一个类,实现这整套接口,然后在Invoke里根据DISPID判断是哪个事件、从DISPPARAMS里抠出参数、再调用业务逻辑。一个事件一个类,写起来极其繁琐。
用符合模式包装之后,可以把这一整套 COM 回调机制翻译成std::function:
template <typename... Args> class ComCallbackAdapter : public IDispatch { public: explicit ComCallbackAdapter(std::function<void(Args...)> handler) : handler_(std::move(handler)), refCount_(1) {} // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override { if (riid == IID_IUnknown || riid == IID_IDispatch) { *ppv = static_cast<IDispatch*>(this); AddRef(); return S_OK; } *ppv = nullptr; return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&refCount_); } STDMETHODIMP_(ULONG) Release() override { ULONG ref = InterlockedDecrement(&refCount_); if (ref == 0) { delete this; return 0; } return ref; } // IDispatch(简化版) STDMETHODIMP GetTypeInfoCount(UINT* pctinfo) override { *pctinfo = 0; return S_OK; } STDMETHODIMP GetTypeInfo(UINT, LCID, ITypeInfo**) override { return E_NOTIMPL; } STDMETHODIMP GetIDsOfNames(REFIID, LPOLESTR*, UINT, LCID, DISPID*) override { return E_NOTIMPL; } STDMETHODIMP Invoke(DISPID dispid, REFIID, LCID, WORD, DISPPARAMS* pParams, VARIANT* pResult, EXCEPINFO*, UINT*) override { if (dispid == DISPID_PROGRESS && handler_) { handler_(ExtractValue(pParams)); // 按实际签名设计 } return S_OK; } private: std::function<void(Args...)> handler_; volatile LONG refCount_; };这样你在业务代码里只需要:
ComCallbackAdapter<int> cb([](int percent) { wprintf(L"Progress: %d%%\n", percent); }); // 把 cb 传给 IConnectionPoint::Advise回调对象在传出去之前必须保证存活,这一步仍然需要你小心管理生命周期——但至少IDispatch那套繁琐的接口签名你永远不需要再碰了。这套适配方案我在多个 COM 组件交互的项目里用过,省掉的时间非常可观。
5. 踩坑实录:符合模式在 Windows 项目里的典型翻车现场
5.1 坑一:资源句柄的"双重释放"
我接手过一个老项目,里面的代码用std::shared_ptr去管理HANDLE。乍一看挺合理:
std::shared_ptr<void> fileHandle( CreateFileW(...), [](void* p) { if (p != INVALID_HANDLE_VALUE) CloseHandle(p); });崩溃发生在某一处:CreateFileW返回失败时返回的是INVALID_HANDLE_VALUE(即(HANDLE)(LONG_PTR)-1,不是nullptr)。用shared_ptr管理时,这个INVALID_HANDLE_VALUE被塞了进去,后来自定义 deleter 里有判断p != INVALID_HANDLE_VALUE,看起来没问题。但实际上,shared_ptr的析构逻辑是先判断内部指针是否为nullptr,不为空才调用 deleter,而INVALID_HANDLE_VALUE显然不是nullptr,于是 deleter 还是被调用了,好在删除了判断,闭了门。
但另一处就没这么幸运:代码里把一个INVALID_HANDLE_VALUE和一个有效句柄都装进了同一个shared_ptr的容器,某个分支判断失误把有效的句柄Release了两次,程序直接handle double close崩溃。
这个坑的根源就是:Windows 句柄的"无效值"有两套语义,nullptr是一种,INVALID_HANDLE_VALUE是另一种,不能想当然地用标准库智能指针去适配,必须像我们第三节写的WinFile一样,把句柄封装在一个专门处理无效值的类里。适配器要处理的不仅是接口转换,还有目标系统的"边角语义"。
5.2 坑二:把整个 Win32 API 封装成"万金油适配类"
我见过一个项目里有个巨大的静态类,名字叫WinApiHelper,里面有两百多个静态方法,几乎把所有常用 Win32 API 都"适配"了一遍。WinApiHelper::CreateProcess、WinApiHelper::ReadRegistry、WinApiHelper::GetSystemTime……代码写起来方便,谁都能调,但维护起来是灾难。
问题在哪?适配器应该按"业务能力"划分,而不是按"被包装的 API 来源"划分。按来源划分,就是把整个windows.h换个名字再抄了一遍,没有任何抽象价值。如果你发现自己的"适配层"只是在 API 名字前加个前缀,毫无类型转换、错误处理、生命周期管理、接口重构,那你只是在写"转发函数",不是在做适配。
正确做法是:按你业务里的"领域能力"来拆分——文件操作、注册表访问、进程管理、窗口交互、网络通信,各自一个独立的适配器类,每个类只暴露业务需要的方法签名,隐藏掉不适用的参数。这样代码的调用方关心的是"我能不能打开这个配置",而不是"我该调CreateFile还是OpenFile还是NtCreateFile"。
5.3 坑三:线程亲和性——UI 消息适配到工作线程
Windows UI 有一个铁律:窗口消息必须在创建它的线程里处理。哪怕你拿到了同一个HWND,从工作线程里直接调用它的某个方法,大概率崩溃。
某个模块里,开发者写了一个适配类,把某个控件的SetText方法适配成线程安全的包装,内部调用了PostMessage。看起来没问题,但另一个接口GetText是同步函数,工作线程里直接访问了窗口的数据结构,结果就是间歇性闪退,而且只在大量并发时出现。查了两周才定位到是线程亲和性问题。
符合模式做接口适配时,一定要把目标 API 的线程模型写给调用方。适配器解决接口转换,但不解决并发模型差异;如果你没有同时把"跨线程投递"这个能力也适配进去,调用方会误以为这个接口是线程安全的。Windows 里有几个成熟的适配手段:PostMessage把消息投递到 UI 线程、SetTimer定时器配合WM_TIMER、RegisterWindowMessage自定义消息、std::async启动独立线程后内部再同步回 UI。选哪一种依赖目标场景,说到底是适配器设计里信息透明的问题。
5.4 常见问题速查表
| 症状 | 常见原因 | 排查思路 | 适配层解决方案 |
|---|---|---|---|
| 文件句柄泄漏 | 忘记 CloseHandle 或异常路径未释放 | 用调试器看句柄表,或开启 Gflags 句柄追踪 | RAII 类统一管理,析构释放 |
| 双击释放崩溃 | 两个对象共用一个 HANDLE 引用 | 检查拷贝构造/赋值是否被禁用 | 删除拷贝语义,只用移动语义 |
| 宽窄字符串乱码 | 调用 ANSI API 但传入 UTF-8/宽字符串 | 检查 API 后缀 A/W,检查编译期 UNICODE 宏 | 统一使用宽字符 API,或中间层做编码转换 |
| COM 对象不释放 | Release 次数不匹配 | 检查引用计数调用点 | CComPtr/适配回调对象统一管理 |
| 跨线程访问 UI 崩溃 | 工作线程直接更新窗口 | 检查是否通过 PostMessage/SendMessage 分发 | 适配器内统一投递消息到 UI 线程 |
| 接口文件太大难维护 | 适配层按 API 来源堆方法 | 统计方法分组,看是否按业务划分 | 按领域能力拆分,一个类只管一件事 |
5.5 字符集是"隐形适配器"最容易翻车的点
Windows 编程里最容易被人忽视的适配需求就是字符编码。C 风格的char*默认是 ANSI,wchar_t*是 UTF-16,而现代代码里很多库吐出来的是 UTF-8。
当你没有适配层时,代码里会散落一地MultiByteToWideChar和WideCharToMultiByte的调用,且窗口消息、注册表、文件名、套接字互传的数据,每一处都要小心转换。做一个封好的适配层时,建议把"外界输入的字符串统一转成 UTF-16 再调用 Win32 API,内部数据统一使用 UTF-8"作为默认策略,所有编码转换集中在一个StringCvt之类的工具类里。这样写业务代码的人不需要考虑编码问题,真正把编码差异关在适配层里。
6. 我在实际工程里对符合模式的两个心得
第一个体会是:符合模式在 Windows 编程里不是"锦上添花"的设计模式,而是必需品。只要你想在 Win32 API、COM、MFC、WPF 这些彼此不相容的接口之间搭桥,你就必须写适配层。绕不开。你不主动写,就等于把"适配"工作散落在业务代码的每一个角落,最后到处是相似又不同的黏合逻辑,改一处崩三处。
第二个体会是:适配层的边界一定要划清楚。它不是越厚越好,也不是越薄越好。薄了,错误处理、资源管理、字符编码这些细节还是会漏到业务代码里;厚了,它会变成另一个"系统",人人都在上面加方法,最后没人用它,或者人人都在骂它。我总结出一个判断标准:如果业务代码里可以直接看到 Win32 API 函数名,那适配层就是不够的;如果适配层的类名和业务领域词完全无关,那适配层就是过度设计的。
最后分享一个实用的小技巧:做任何适配前,先把目标接口的"失败语义"列清楚。Win32 API 里错误码的种类多到问你"失败后发生了什么"往往是排查的第一步。把错误保存、错误分类、错误转字符串的方法放进适配层最底部,统一暴露,会大幅降低你写业务逻辑时的认知负担。这个习惯帮我省了不知道多少次 Debug 时间。