结构型设计模式:让代码结构更优雅

说到结构型设计模式,我脑子里第一个蹦出来的词就是「搭积木」。说白了,这类模式就是帮我们把零散的类组织起来,形成更灵活、更易维护的结构。今天咱们一口气聊五个:适配器、装饰器、代理、组合、桥接。别被名字吓到,每个模式背后都有很朴素的道理。

适配器模式:让不兼容的接口「握手」

适配器模式,我愿称之为「万能转接头」。你想想看,现实中的电源适配器能把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());  // 蓝色方形

五种模式的关系图

下面这张图帮你理清这五个模式的核心逻辑:

结构型设计模式核心逻辑 适配器模式 让不兼容的接口协作 旧接口 → 适配器 → 新接口 「万能转接头」 装饰器模式 动态添加功能 基础对象 → 装饰器1 → 装饰器2 「俄罗斯套娃」 代理模式 控制对象访问 客户端 → 代理 → 真实对象 「经纪人模式」 组合模式 树形结构统一处理 叶子节点 ↔ 组合节点 「部分-整体」 桥接模式 抽象与实现分离 抽象类 ↔ 桥 ↔ 实现类 「两个维度独立变化」 共同目标 让代码结构更灵活 减少类之间的耦合 提高可扩展性

如何选择?我的经验之谈

说实话,刚学设计模式时我也经常搞混。后来我总结了一个简单的判断方法:

场景 推荐模式 一句话判断
接口不兼容,需要转换 适配器 「能不能加个中间层转一下?」
需要动态给对象加功能 装饰器 「能不能包一层解决?」
需要控制对象访问 代理 「能不能找个替身?」
有树形层次结构 组合 「能不能统一对待单个和组合?」
抽象和实现需要独立变化 桥接 「能不能拆成两个维度?」

最后说一句:设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。记住:模式是工具,不是目的。当你的代码出现「重复的胶水代码」、「类爆炸」、「修改困难」这些信号时,再考虑用模式来重构。嗯,今天就聊到这儿,希望这些经验对你有帮助。

公众号:蓝海资料掘金营,微信deep3321