建造者模式:模式动机与定义、角色与结构、C++代码实现、与工厂模式的区别

说实话,建造者模式是我在项目中用得最「舒服」的一个创建型模式。为什么?因为它解决了一个很实在的问题:当一个对象有太多配置项,或者构造过程需要分几步走的时候,代码就容易变得又臭又长。我早期写过一个渲染引擎,初始化一个场景对象要传十几个参数,每次调用构造函数都像在填高考志愿——顺序不能错,缺了还不行。后来重构时用了建造者模式,整个世界清净了。

模式动机与定义

先聊聊动机。你想想看,如果一个类的构造函数有七八个参数,甚至有些参数还是可选的,你会怎么写?大部分人可能会写多个重载构造函数,或者用默认参数。但这样做的后果是:构造函数列表越来越长,调用方根本分不清哪个参数对应哪个含义。更糟糕的是,如果参数之间有依赖关系(比如必须先设置A才能设置B),那构造函数根本没法表达这种约束。

建造者模式的动机就是:将一个复杂对象的构建过程与它的表示分离。说白了,就是让同样的构建过程可以创建出不同的对象。比如你做一个电脑配置系统,同样的组装流程(选CPU、选内存、选硬盘),最后可以组装出游戏本、办公本、工作站——这就是建造者模式的精髓。

定义:建造者模式(Builder Pattern)将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。

角色与结构

建造者模式里,有四个核心角色。我习惯把它们记成「导演、建造者、具体建造者、产品」——嗯,就像拍电影一样。

  • 产品(Product):最终要创建的复杂对象。它通常包含多个部件。
  • 抽象建造者(Builder):定义创建产品各个部件的抽象接口。一般会声明几个构建方法,比如 buildPartA()buildPartB()
  • 具体建造者(ConcreteBuilder):实现抽象建造者接口,负责实际组装产品的部件。它还会提供一个 getResult() 方法,返回最终产品。
  • 导演(Director):控制构建过程的顺序。它不关心具体建造者是谁,只负责调用建造者的构建方法。

下面这张图可以帮你快速理清关系:

Director + construct() Builder (接口) + buildPartA() + buildPartB() ConcreteBuilder1 + getResult() ConcreteBuilder2 + getResult() Product 部件A + 部件B 控制构建顺序 定义构建接口 具体实现 具体实现 最终产品

C++代码实现

咱们直接上代码。假设我们要构建一台电脑,电脑有CPU、内存、硬盘三个部件。不同的配置组合可以生成不同定位的电脑。

// 产品类
class Computer {
public:
    void setCPU(const std::string& cpu) { cpu_ = cpu; }
    void setMemory(const std::string& mem) { memory_ = mem; }
    void setStorage(const std::string& storage) { storage_ = storage; }
    
    void showSpecs() const {
        std::cout << "CPU: " << cpu_ << "\n"
                  << "Memory: " << memory_ << "\n"
                  << "Storage: " << storage_ << std::endl;
    }
private:
    std::string cpu_;
    std::string memory_;
    std::string storage_;
};

// 抽象建造者
class ComputerBuilder {
public:
    virtual ~ComputerBuilder() = default;
    virtual void buildCPU() = 0;
    virtual void buildMemory() = 0;
    virtual void buildStorage() = 0;
    virtual Computer getResult() = 0;
};

// 具体建造者:游戏本
class GamingComputerBuilder : public ComputerBuilder {
public:
    void buildCPU() override { computer_.setCPU("Intel i9-13900K"); }
    void buildMemory() override { computer_.setMemory("32GB DDR5"); }
    void buildStorage() override { computer_.setStorage("1TB NVMe SSD"); }
    Computer getResult() override { return computer_; }
private:
    Computer computer_;
};

// 具体建造者:办公本
class OfficeComputerBuilder : public ComputerBuilder {
public:
    void buildCPU() override { computer_.setCPU("Intel i5-13400"); }
    void buildMemory() override { computer_.setMemory("16GB DDR4"); }
    void buildStorage() override { computer_.setStorage("512GB SSD"); }
    Computer getResult() override { return computer_; }
private:
    Computer computer_;
};

// 导演类
class Director {
public:
    void setBuilder(ComputerBuilder* builder) { builder_ = builder; }
    
    Computer construct() {
        builder_->buildCPU();
        builder_->buildMemory();
        builder_->buildStorage();
        return builder_->getResult();
    }
private:
    ComputerBuilder* builder_;
};

// 客户端使用
int main() {
    Director director;
    
    GamingComputerBuilder gamingBuilder;
    director.setBuilder(&gamingBuilder);
    Computer gamingPC = director.construct();
    gamingPC.showSpecs();
    
    OfficeComputerBuilder officeBuilder;
    director.setBuilder(&officeBuilder);
    Computer officePC = director.construct();
    officePC.showSpecs();
    
    return 0;
}
个人习惯:我通常会把 Directorconstruct() 方法设计成可以接受多个建造者参数,或者支持链式调用。这样在测试不同配置时特别方便。

与工厂模式的区别

这个问题我面试时经常被问到。很多人觉得建造者和工厂模式很像,都是创建对象嘛。但它们的关注点完全不同。

对比维度 工厂模式 建造者模式
核心关注点 创建哪个产品(产品族/产品等级) 如何一步步构建产品
产品复杂度 通常创建单一对象,部件较少 创建复杂对象,包含多个部件
构建过程 一步到位,通常一个方法返回产品 分步骤,由导演控制顺序
客户端控制 客户端只需指定类型 客户端可以自定义构建步骤
典型场景 日志记录器、数据库连接 复杂文档、游戏角色、配置对象

说白了,工厂模式是「我要一个XX产品」,建造者模式是「我要按照这个流程做一个产品」。我记得有一次做报表系统,需要生成不同格式的报表(PDF、Excel、HTML)。如果用工厂模式,你得为每种格式写一个工厂类。但用建造者模式,你只需要写一个报表建造者接口,然后让导演控制「添加标题→添加表格→添加图表」这个流程,不同建造者输出不同格式——代码复用率直接拉满。

避坑指南:我曾经在一个项目中过度使用建造者模式,把只有两个属性的简单对象也用建造者来创建。结果代码量翻了三倍,维护起来反而更麻烦。记住:建造者模式是为「复杂对象」准备的。如果对象的构造函数不超过3个参数,或者构建步骤固定不变,直接用构造函数或工厂模式更合适。

另外,还有一个容易混淆的点:建造者模式与工厂模式可以组合使用。比如,你可以用抽象工厂来创建不同系列的建造者,然后再用导演去构建产品。这种组合在大型框架中很常见,比如GUI库中创建不同风格(Windows、Mac、Linux)的对话框。

嗯,总结一下我的经验:当你发现一个对象的构造过程需要「分步执行」且「步骤可能变化」时,就该考虑建造者模式了。它让代码的可读性和可维护性都上了一个台阶。但别滥用,简单场景就用简单方案——这是架构师的基本素养。


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