精读C++20设计模式——结构型设计模式:适配器模式
精读C++20设计模式——结构型设计模式:适配器模式
前言
现在我们的设计模式学习之路走到了第二个大部分,也就是结构型设计模式,现在我们来到了设计模式的一个核心。前面的创造型,是在关心我们一个子程序的起点——一些对象创建的时候我们如何设计对象的创建,让我们的程序有一个好的起头。那么这里,也就是程序在运行时中,对象的交互组织如何构成的部分。这里的组织更多的笔者看来是一种静态的组织,而不是关心对象之间如何动态的交互。当然,我们也不能太割裂的看待程序本身。它自身是一个整体——我们只是在关心程序的一种侧面而已。
这里,我们马上就要请出来第一个常见的模式——适配器模式。笔者用过一些,曾经开玩笑说到这个是研究史山桥接的设计模式。实际上可能有些过头了,但是他的确就是做类似工作的:正如它的名称所愿——适配器模式
什么是适配器模式
一个经典的场景,我这里需要借用一下《C++20设计模式》的例子。
- 笔者的宿舍的插头只有一个二头插口是好用的,但是我现在需要插入一个三头的插头,这怎么办呢?还好我买了一个转接器,它可以转接二孔到三孔。这个转接器就是一个经典的适配器。我不会砍掉一个头,这样的话我的仪器就用不了了。
- 我们在桥接两个接口不兼容的时候不是硬改两个对接的子系统的接口,而是提供一个桥接器来进行转接。硬改或者增加接口只会让我们的对象变得臃肿不堪——导致一个类即负责自身的工作内容还要负责对象的桥接。
很好,现在我们拿笔者一个绘图的例子。
struct Point2D {int x; int y;}
struct Line {Point2D start, end;}struct AbstractGeoPoly{using Containters_t = std::vector<Line>;virtual Containters_t::iterator begin() = 0;virtual Containters_t::iterator end() = 0;
};struct Rectangle : AbstractGeoPoly
{ Rectangle(const Point2D& left_top, int width, int height){// init the Rectangle::lines actually}virtual Containters_t::iterator begin() override{return line.begin();}virtual Containters_t::iterator end() override{return line.end();}
private: Containters_t lines;
};
我们抽象的正开心,但是另一面,负责OLED驱动的朋友告诉你,很遗憾,我们这边只能给出来画点的接口:
void OLEDDriver::drawPoints(PaintDevice& p, vector<Point>::iterator start, vector<Point>::iterator end){for(vector<Point>::iterator it = start; it != end; it++){p.setPixel(it->x, it->y);}
}
这下怎么办呢?大量的AbstractGeoPoly的图形等着绘制呢。咱们只能想一个办法——将Containters_t和vector<Point>::iterator
桥接起来,这样,我们就能解决OLEDDriver和AbstractGeoPoly可以提供的接口不一样的问题了。
struct ContaintersLineToPointsAdapter
{using target_t = vector<Point>::iterator;ContaintersLineToPointsAdapter(){// Convert Line To Points so Line can be drawable}virtual target_t begin(){ return points.begin(); }virtual target_t end(){ return points.end(); }
private:target_t points;
}
但是有麻烦
但是我们马上想到一个非常麻烦的事情——那就是对于频繁的绘图下,我们难道都要做一次转换嘛?显然是没必要的。在内存和硬存越发便宜的今天,转向采用空间换时间的策略是一个非常明智的选择。比如说,我们今天会优先将HTTP请求的结果缓存起来,下一次发现没有过期的时候接着用。这样的方式来回避频繁的请求。我们这里也可以采用这个小花招。
我们建立一个从Line到Point映射对的缓存,或者说更具体的——采用对象哈希值的办法来唯一标识我们的对象是不是之前处理过——如果我们处理过,直接将适配后的对象返回出去。这里具体的措施同我们的设计模式没有关系。这里只是提醒我们可以这样做。
双向适配器(Bidirectional Adapter)
上面的例子中,我们完成了“一方接口到另一方接口的单向转换”——也就是把Line
→Point
,从几何图形的抽象表示,适配到了OLED驱动能够接受的绘制点接口上。
但是在实际系统设计中,还有一种更加常见的情况:两个子系统都无法修改,但它们都需要互相使用对方的数据结构或接口。常见的就是咱们的MVVM接口这种时候,我们就需要双向适配器(Bidirectional Adapter)。
双向适配的几何系统
假设现在不仅 OLED 驱动需要绘制点,还新增了一个几何计算库,这个库要求使用Line
的形式进行计算:
class GeometryEngine {
public:void calculate(vector<Line>::iterator begin, vector<Line>::iterator end);
};
然而这个几何引擎也有一部分功能要和 OLED 驱动共用:它有时候会接收到“点”的形式的数据,需要将这些点还原为Line
来处理。
这时候我们不能只是提供“Line→Point”的转换,还要支持“Point→Line”!
struct BidirectionalAdapter {BidirectionalAdapter(const vector<Line>& lines) {// 把 Line 转换为 Pointfor (auto& l : lines) {points.push_back({l.start.x, l.start.y});points.push_back({l.end.x, l.end.y});}}BidirectionalAdapter(const vector<Point>& pts) {// 把 Point 转换为 Line(简单起见,两个点一条线)for (size_t i = 0; i + 1 < pts.size(); i += 2) {lines.push_back({pts[i], pts[i + 1]});}}auto points_begin() { return points.begin(); }auto points_end() { return points.end(); }auto lines_begin() { return lines.begin(); }auto lines_end() { return lines.end(); }private:vector<Point> points;vector<Line> lines;
};
这样,无论是 OLED 驱动还是几何引擎,都能通过同一个适配器进行双向数据转换,大大提高了系统的复用性与灵活性。
✅ 优点:
- 适用于双向依赖、双方都不能修改的场景。
- 减少重复代码,集中管理数据结构转换逻辑
总结
我们要解决什么问题?
在软件开发中,经常遇到这样的问题:已有的两个类/子系统接口不兼容,但我们又不能修改它们的源码。
- 比如:绘图模块提供的是
Line
,但硬件驱动只能识别Point
; - 又或者:几何引擎要求
Line
,而 OLED 驱动却只会处理Point
。
这类场景下,直接修改接口会破坏原有设计,增加耦合和维护成本。
我们如何解决的?
解决方案就是引入适配器(Adapter):
- 单向适配器:提供一个中间层,把
Line
转成Point
,让 OLED 驱动正常绘图。 - 双向适配器:当双方都需要互相使用对方接口时,提供“翻译机”,让
Line↔Point
相互转换。 - 优化手段:通过缓存和哈希标识,避免频繁重复转换,提高性能。
此外,我们还区分了适配器与桥接、装饰器、代理、外观等模式的不同意图,避免混淆。
方案的优劣
优点:
- ✅ 不改变原有类/库,符合“开闭原则”。
- ✅ 让原本不兼容的系统能够协作,复用现有代码。
- ✅ 灵活性强,可扩展到双向适配、缓存优化、模板泛化。
缺点:
- ⚠️ 增加了系统复杂度,多了一层间接调用。
- ⚠️ 性能可能受限(频繁转换时需缓存优化)。
- ⚠️ 过度使用会导致“适配器满天飞”,难以维护。
适配器模式本质上是造一台“翻译机”,让两个原本无法对话的世界能够顺畅协作。它带来了灵活性和复用性,但需要在性能与复杂度之间找到平衡。