构建器模式

一.意图

Builder 是一种创造性设计模式,允许你一步步构建复杂的物体。该模式允许你用相同的构造代码生成不同类型和表示的对象。

在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

将一个复杂对象的构建与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。------《设计模式》GoF

二.问题

想象一个复杂的对象,需要对许多字段和嵌套对象进行繁琐的逐步初始化。这类初始化代码通常埋藏在一个庞大构造器中,包含大量参数。更糟的是:散落在客户端代码中。

举个例子,让我们思考如何创建一个对象。要建造一栋简单的房子,你需要建造四面墙和一层地板,安装一扇门,安装一对窗户,并建造一个屋顶。但如果你想要一栋更大、更明亮的房子,有后院和其他设施(比如供暖系统、管道和电线)呢?

最简单的解决方案是扩展基类,创建一组子类来覆盖所有参数组合。但最终你会有相当数量的子职业。任何新的参数,比如门廊样式,都需要进一步扩大这一层级。

还有另一种不涉及繁殖子职业的方法。你可以直接在基类里创建一个巨大的构造器,包含所有可能控制房屋对象的参数。虽然这种方法确实消除了子职业的需求,但也带来了另一个问题。

大多数情况下,大部分参数都不会被使用,导致构造调用变得相当难看。例如,只有一小部分房屋有游泳池,因此与游泳池相关的参数在十次中有九次都无用。

三.解决方案

构建者模式建议你从自己的类中提取对象构建代码,并将其迁移到称为构建器的独立对象中。

该模式将对象构造组织为一组步骤(buildWalls、buildDoor ,等)。要创建一个对象,你需要在构建对象上执行一系列这样的步骤。重要的是,你不需要打电话给所有步骤。你只能调用生成特定对象配置所需的步骤。

当你需要构建产品的各种表示时,某些构建步骤可能需要不同的实现。例如,小屋的墙壁可以用木头建造,但城堡墙必须用石头建造。

在这种情况下,你可以创建多个不同的建造者类别,实现相同的建造步骤,但方式不同。然后你可以在构建过程中使用这些构建器(即对建造步骤的有序调用)来生成不同类型的对象。

举个例子,想象一个建筑师用木材和玻璃建造一切,第二个建筑师用石头和铁建造一切,第三个建筑师用黄金和钻石。通过重复相同的步骤,你会得到第一个建造者建造的普通房屋,第二个建造者建造一座小城堡,第三个建造者建造一座宫殿。然而,这只有在调用构建步骤的客户端代码能够通过通用接口与构建者交互时才可行。

导演

你还可以进一步,将用于构建产品的构建步骤提取一系列调用,集成到一个叫director的独立类中。director类定义了执行构建步骤的顺序,而构建器则提供这些步骤的实现。

主管知道该执行哪些构建步骤才能让产品正常工作。

你的项目里不一定要有导演课程。你总可以直接从客户代码中按特定顺序调用建造步骤。不过,导演课程可能是个不错的平台,可以把各种建筑流程放进去,方便你在课程中重复使用。

此外,director类完全隐藏了产品构建的细节,无法直接显示客户端代码。客户只需将建筑商与董事关联,启动施工,并从建筑商那里获得结果。

四.结构

五.适合应用场景

  1. 用建造者模式去掉"伸缩建造器"。

    假设你有一个构造器,有十个可选参数。叫这种怪物非常不方便;因此,你会让构造函数过载,创建多个参数更少的短版本。这些构造函数仍然引用主构建子,将部分默认值传递到遗漏的参数中。

    复制代码
    class Pizza {
        Pizza(int size) { ... }
        Pizza(int size, boolean cheese) { ... }
        Pizza(int size, boolean cheese, boolean pepperoni) { ... }
        // ...

    创建此类怪物只有在支持方法重载的语言中才可能,如C#或Java。

    构建者模式允许你一步步建造对象,只使用你真正需要的步骤。实现模式后,你就不必再往构造子里塞入数十个参数了。

  2. 当你希望代码能够创建某些产品(例如石头和木屋)的不同表示时,可以使用构建者模式。

    当构建产品的不同表示包含相似步骤但细节不同时,构建模式可以应用。

    基础构建界面定义了所有可能的构建步骤,混凝土构建者实现这些步骤以构建产品的特定表示。与此同时,导演阶级指导施工顺序。

  3. 使用构建器构建复合树或其他复杂对象。

    构建者模式允许你一步步构建产品。你可以推迟执行某些步骤,同时不破坏最终产品。你甚至可以递归调用步骤,这在需要构建对象树时非常有用。

    施工者在施工过程中不会暴露未完成的成品。这防止客户端代码获取不完整的结果。

六.实现方式

  1. 确保你能够清晰定义构建所有可用产品表示的常见构建步骤。否则,你无法继续实施该模式。

  2. 在基地建造界面中声明这些步骤。

  3. 为每种产品表示创建一个具体建造类,并实现它们的构建步骤。

    别忘了实现获取构建结果的方法。这种方法不能在构建界面中声明的原因在于,不同的构建者可能构建的产品没有统一界面。因此,你不知道这种方法的返回类型是什么。然而,如果你处理的是单一层级的产品,取数据方法可以安全地添加到基础接口中。

  4. 考虑创建一个导演类。它可能包含使用同一构建对象构建产品的多种方式。

  5. 客户端代码同时创建构建器和导演对象。施工开始前,客户必须将建造物交给主管。通常,客户端只通过导演类构造子的参数执行一次。导演在所有后续构建中使用建造对象。还有另一种方式,即将建筑商交由主管负责特定的产品制造方法。

  6. 只有当所有产品都遵循相同界面时,构造结果才能直接从导向器获得。否则,客户应向建造者获取结果。

七.优缺点

  1. 优点:

    • 你可以逐步构建对象,推迟构建步骤,或递归运行步骤。

    • 你可以在构建各种产品表示时重复使用相同的建筑规范。

    • 单一责任原则。你可以将复杂的构建代码与产品的业务逻辑隔离开来。

  2. 缺点

    • 由于模式需要创建多个新类,代码的整体复杂度会增加。

八.与其他模式的关系

  • 许多设计从工厂方法开始(简单且通过子职业更可定制),随后逐步发展为抽象工厂、原型或建造者(更灵活但更复杂)。

  • Builder 专注于一步步构建复杂物体。Abstract Factory 专注于创建相关对象的族。抽象工厂会立即返回产品,而建造者允许你在取产品前运行一些额外的构建步骤。

  • 你可以在创建复杂复合树时使用Builder,因为它的构建步骤可以递归地进行编程。

  • 你可以将Builder和Bridge结合使用:Director类扮演抽象角色,而不同的构建者则作为实现。

  • 抽象工厂、建造者和原型都可以作为单例实现。

九.示例代码

复制代码
#include <iostream>
#include <string>
// 1. 产品类:要构建的复杂对象(核心:只存属性+基础打印方法)
class Person {
private:
    // 包含【必填属性】和【可选属性】,属性私有化保证安全性
    std::string name;  // 必填:姓名
    int age;           // 必填:年龄
    std::string addr;  // 可选:地址
    int height;        // 可选:身高
    std::string phone; // 可选:电话
​
    // 关键:把构造函数私有化!
    // 禁止外部直接创建对象,必须通过「建造者」创建,保证对象构建的统一性
    Person(std::string n, int a, std::string ad, int h, std::string p)
        : name(n), age(a), addr(ad), height(h), phone(p) {}
​
public:
    // 打印对象信息,测试用
    void showInfo() const {
        std::cout << "姓名:" << name << std::endl;
        std::cout << "年龄:" << age << std::endl;
        std::cout << "地址:" << addr << std::endl;
        std::cout << "身高:" << height << "cm" << std::endl;
        std::cout << "电话:" << phone << std::endl;
        std::cout << "-------------------------" << std::endl;
    }
​
    // 2. 建造者类:作为产品类的【内部类】(核心设计,优点:耦合度最低)
    class Builder {
    private:
        // 建造者内部维护和产品类完全一致的属性,用于分步赋值
        std::string m_name;
        int m_age = 0;
        std::string m_addr = "未知地址";
        int m_height = 0;
        std::string m_phone = "未知电话";
​
    public:
        // ① 构造建造者时,传入【必填参数】,保证必填属性一定有值
        Builder(std::string name, int age) : m_name(name), m_age(age) {
            // 可选:属性合法性校验
            if (age < 0 || age > 150) {
                std::cerr << "警告:年龄不合法,默认赋值为0" << std::endl;
                m_age = 0;
            }
        }
​
        // ② 链式设置【可选属性】
        // 核心语法:返回值为 Builder& 引用,实现链式调用
        Builder& setAddr(const std::string& addr) {
            this->m_addr = addr;
            return *this;
        }
​
        Builder& setHeight(int height) {
            if (height > 0 && height < 300) { // 属性校验
                this->m_height = height;
            }
            return *this;
        }
​
        Builder& setPhone(const std::string& phone) {
            this->m_phone = phone;
            return *this;
        }
​
        // ③ 最终构建方法:返回产品对象
        // 调用产品类的私有构造函数,组装属性并返回
        Person build() {
            return Person(m_name, m_age, m_addr, m_height, m_phone);
        }
    };
};
​
// 主函数测试
int main() {
    // 场景1:只传必填属性,创建最简对象
    Person p1 = Person::Builder("张三", 20).build();
    p1.showInfo();
​
    // 场景2:传必填+部分可选属性(链式调用,优雅)
    Person p2 = Person::Builder("李四", 25)
                .setAddr("北京市朝阳区")
                .setHeight(180)
                .build();
    p2.showInfo();
​
    // 场景3:传所有属性,创建完整对象
    Person p3 = Person::Builder("王五", 30)
                .setAddr("上海市浦东新区")
                .setHeight(175)
                .setPhone("13800138000")
                .build();
    p3.showInfo();
​
    return 0;
}

执行结果

复制代码
姓名:张三
年龄:20
地址:未知地址
身高:0cm
电话:未知电话
-------------------------
姓名:李四
年龄:25
地址:北京市朝阳区
身高:180cm
电话:未知电话
-------------------------
姓名:王五
年龄:30
地址:上海市浦东新区
身高:175cm
电话:13800138000
-------------------------

如果你的业务中有「多个同类产品,不同构建规则」

复制代码
#include <iostream>
#include <string>
using namespace std;
​
// 1. 产品类
class Person {
public:
    string name;
    int age;
    string addr;
    void show() { cout << name << "," << age << "," << addr << endl; }
};
​
// 2. 抽象建造者(基类,定义规范)
class AbstractBuilder {
public:
    virtual ~AbstractBuilder() = default;
    virtual void setName(string name) = 0;
    virtual void setAge(int age) = 0;
    virtual void setAddr(string addr) = 0;
    virtual Person build() = 0;
};
​
// 3. 具体建造者-学生建造者
class StudentBuilder : public AbstractBuilder {
private:
    Person p;
public:
    void setName(string name) override { p.name = name; }
    void setAge(int age) override { p.age = age; }
    void setAddr(string addr) override { p.addr = addr; }
    Person build() override { return p; }
};
​
// 测试
int main() {
    AbstractBuilder* builder = new StudentBuilder();
    builder->setName("小明");
    builder->setAge(18);
    builder->setAddr("学校宿舍");
    Person stu = builder->build();
    stu.show(); // 小明,18,学校宿舍
    delete builder;
    return 0;
}
相关推荐
charlie1145141914 个月前
精读《C++20设计模式》——创造型设计模式:构建器系列
c++·设计模式·c++20·构造器模式