结构型设计模式:让代码结构更优雅
说到结构型设计模式,我脑子里第一个蹦出来的词就是「搭积木」。说白了,这类模式就是帮我们把零散的类组织起来,形成更灵活、更易维护的结构。今天咱们一口气聊五个:适配器、装饰器、代理、组合、桥接。别被名字吓到,每个模式背后都有很朴素的道理。
适配器模式:让不兼容的接口「握手」
适配器模式,我愿称之为「万能转接头」。你想想看,现实中的电源适配器能把220V转成手机需要的5V,代码里的适配器也是干这活的——让两个本来不兼容的接口能一起工作。
我在项目中遇到过最典型的场景:老系统用XML传数据,新模块只认JSON。总不能把老系统全改了吧?这时候适配器就派上用场了。
// 老系统的接口
class OldXmlParser {
public:
std::string parseXml(const std::string& xmlData) {
return "parsed_xml_data";
}
};
// 新系统期待的接口
class JsonParser {
public:
virtual std::string parseJson(const std::string& jsonData) = 0;
};
// 适配器:把XML解析器包装成JSON解析器
class XmlToJsonAdapter : public JsonParser {
private:
OldXmlParser* xmlParser;
public:
XmlToJsonAdapter(OldXmlParser* parser) : xmlParser(parser) {}
std::string parseJson(const std::string& jsonData) override {
// 先把JSON转成XML格式(假装有转换逻辑)
std::string xmlData = convertJsonToXml(jsonData);
// 调用老系统的XML解析
return xmlParser->parseXml(xmlData);
}
private:
std::string convertJsonToXml(const std::string& json) {
return "<root>" + json + "</root>";
}
};
我的小建议:适配器模式别滥用。如果两个接口差异太大,硬套适配器反而会让代码更混乱。我一般只在「改不动老代码」或者「第三方库接口不匹配」时才用。
装饰器模式:给对象「叠buff」
装饰器模式,说白了就是「俄罗斯套娃」。你有一个基础对象,想给它加功能,但又不想改它的类。怎么办?包一层!再想加功能?再包一层!
我记得有个经典例子:咖啡店卖咖啡,基础咖啡10块,加奶+2块,加糖+1块,加奶油+3块。如果用继承来做,你得写8个类(纯咖啡、咖啡+奶、咖啡+糖、咖啡+奶+糖……)。用装饰器,你只需要写基础咖啡类,然后每个附加品写一个装饰器类。
// 饮料接口
class Beverage {
public:
virtual std::string getDescription() = 0;
virtual double cost() = 0;
virtual ~Beverage() = default;
};
// 基础咖啡
class Coffee : public Beverage {
public:
std::string getDescription() override { return "咖啡"; }
double cost() override { return 10.0; }
};
// 装饰器基类
class CondimentDecorator : public Beverage {
protected:
Beverage* beverage;
public:
CondimentDecorator(Beverage* bev) : beverage(bev) {}
};
// 加奶装饰器
class Milk : public CondimentDecorator {
public:
Milk(Beverage* bev) : CondimentDecorator(bev) {}
std::string getDescription() override {
return beverage->getDescription() + " + 奶";
}
double cost() override {
return beverage->cost() + 2.0;
}
};
// 使用
Beverage* myCoffee = new Milk(new Coffee());
// myCoffee->getDescription() 返回 "咖啡 + 奶"
// myCoffee->cost() 返回 12.0
核心要点:装饰器模式和继承的区别在于——继承是在编译期决定行为,装饰器是在运行期动态组合。你想想看,如果用户点单时随机选加料,用继承你得提前写好所有组合,用装饰器就灵活多了。
代理模式:给对象找个「替身」
代理模式,我习惯叫它「经纪人模式」。明星不会直接接电话,都是经纪人先过滤一遍。代码里也一样——你不想直接操作真实对象,就找个代理来替你干脏活累活。
常见的代理类型有几种:
- 虚拟代理:大对象创建很耗时,先用轻量级代理占位,真正需要时才创建真实对象
- 保护代理:控制访问权限,比如只有管理员才能执行某些操作
- 远程代理:隐藏对象位于不同地址空间的事实
- 智能引用:在访问对象时附加额外操作,比如引用计数、日志记录
// 真实图片加载器
class RealImage {
private:
std::string filename;
public:
RealImage(const std::string& file) : filename(file) {
loadFromDisk(); // 构造函数里就加载,很耗时
}
void display() { std::cout << "显示: " << filename << std::endl; }
void loadFromDisk() { std::cout << "从磁盘加载: " << filename << std::endl; }
};
// 代理:延迟加载
class ProxyImage {
private:
RealImage* realImage;
std::string filename;
public:
ProxyImage(const std::string& file) : filename(file), realImage(nullptr) {}
void display() {
if (!realImage) {
realImage = new RealImage(filename); // 第一次显示时才真正加载
}
realImage->display();
}
~ProxyImage() { delete realImage; }
};
我曾经踩过的坑:代理模式和装饰器模式长得像,但意图完全不同。代理控制访问,装饰器增强功能。我见过有人用代理模式给对象加功能,结果代码越写越拧巴。记住:代理是「替身」,装饰器是「包装」。
组合模式:树形结构的「统一接口」
组合模式,说白了就是「部分-整体」的层次结构。文件系统就是典型例子:文件夹里可以放文件,也可以放子文件夹。对用户来说,双击文件和双击文件夹都是「打开」操作,但内部实现完全不同。
组合模式的核心思想:让客户端用统一的方式处理单个对象和组合对象。
// 组件接口
class FileSystemComponent {
public:
virtual void showDetails() = 0;
virtual ~FileSystemComponent() = default;
};
// 叶子节点:文件
class File : public FileSystemComponent {
private:
std::string name;
public:
File(const std::string& n) : name(n) {}
void showDetails() override {
std::cout << "文件: " << name << std::endl;
}
};
// 组合节点:文件夹
class Folder : public FileSystemComponent {
private:
std::string name;
std::vector<FileSystemComponent*> children;
public:
Folder(const std::string& n) : name(n) {}
void add(FileSystemComponent* component) {
children.push_back(component);
}
void showDetails() override {
std::cout << "文件夹: " << name << std::endl;
for (auto* child : children) {
child->showDetails(); // 递归调用
}
}
};
使用时机:当你发现代码里到处都是「如果是叶子节点就XXX,如果是组合节点就YYY」这种判断时,就该考虑组合模式了。它让客户端代码变得极其简洁——不用关心你操作的是单个对象还是对象集合。
桥接模式:把抽象和实现「解耦」
桥接模式,我理解就是「两个维度独立变化」。举个例子:你要画图形,图形有圆形、方形,颜色有红色、蓝色。如果用继承,你得写 2×2=4 个类。如果再加个三角形、再加个绿色……类数量会爆炸。
桥接模式的做法:把「形状」和「颜色」拆成两个独立的类层次,然后用一个「桥」把它们连接起来。
// 颜色接口(实现部分)
class Color {
public:
virtual void applyColor() = 0;
virtual ~Color() = default;
};
class Red : public Color {
public:
void applyColor() override { std::cout << "红色"; }
};
class Blue : public Color {
public:
void applyColor() override { std::cout << "蓝色"; }
};
// 形状抽象(抽象部分)
class Shape {
protected:
Color* color; // 桥接的关键:持有颜色的引用
public:
Shape(Color* c) : color(c) {}
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
Circle(Color* c) : Shape(c) {}
void draw() override {
std::cout << "画一个";
color->applyColor();
std::cout << "圆形" << std::endl;
}
};
class Square : public Shape {
public:
Square(Color* c) : Shape(c) {}
void draw() override {
std::cout << "画一个";
color->applyColor();
std::cout << "方形" << std::endl;
}
};
// 使用:自由组合
Shape* s1 = new Circle(new Red()); // 红色圆形
Shape* s2 = new Square(new Blue()); // 蓝色方形
五种模式的关系图
下面这张图帮你理清这五个模式的核心逻辑:
如何选择?我的经验之谈
说实话,刚学设计模式时我也经常搞混。后来我总结了一个简单的判断方法:
| 场景 | 推荐模式 | 一句话判断 |
|---|---|---|
| 接口不兼容,需要转换 | 适配器 | 「能不能加个中间层转一下?」 |
| 需要动态给对象加功能 | 装饰器 | 「能不能包一层解决?」 |
| 需要控制对象访问 | 代理 | 「能不能找个替身?」 |
| 有树形层次结构 | 组合 | 「能不能统一对待单个和组合?」 |
| 抽象和实现需要独立变化 | 桥接 | 「能不能拆成两个维度?」 |
最后说一句:设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。记住:模式是工具,不是目的。当你的代码出现「重复的胶水代码」、「类爆炸」、「修改困难」这些信号时,再考虑用模式来重构。嗯,今天就聊到这儿,希望这些经验对你有帮助。