[设计模式] Builder 建造者模式

news/2025/2/24 17:38:40

目录

意图

问题

解决

Applying the Builder pattern

主管

结构

伪代码

生成器模式适合应用场景

实现方法

生成器模式优缺点

与其他模式的关系

C++代码

main.cc:概念示例

Output.txt:执行结果


意图
  • Builder 是一种创建性设计模式,可让您 逐步构建复杂对象。
  • 该模式允许您使用相同的构造代码生成对象的不同类型和表示形式。

问题
  • 想象一个 复杂的对象,它需要费力地逐步初始化许多字段和嵌套对象。
  • 这样的初始化代码通常被埋在一个具有大量参数的怪物构造函数中。或者更糟糕的是:分散在客户端代码中。

  • 通过为 对象的每种可能配置 创建子类,可能会使程序过于复杂。例如,要建造一个简单的房子,您需要建造四面墙和一块地板,安装一扇门,安装一对窗户,并建造一个屋顶。
  • 但是,如果您想要一个更大、更明亮的房子,有后院和其他好东西(如供暖系统、管道和电线),该怎么办?
  • 最简单的解决方案是 扩展基类 并创建一组子类以涵盖参数的所有组合。但最终你会得到相当多的子类。任何新参数(如 porch 样式)都需要进一步增加此层次结构。
  • 还有另一种不涉及繁殖子类的方法。您可以直接 在基类中创建一个巨大的构造函数其中包含控制 house 对象的所有可能参数。虽然这种方法确实消除了对子类的需求,但它产生了另一个问题。

  • 具有大量参数的构造函数有其缺点:并非始终需要所有参数。在大多数情况下,大多数参数都是未使用的,这使得构造函数调用非常难看。
解决
  • Builder 模式建议 将 对象构造代码 从其自己的类中提取出来,并将其 移动到称为 builder 的单独对象中。

  • Builder 模式允许 您逐步构建复杂对象。Builder 不允许其他对象在构建产品时访问产品。
  • 该模式将对象构造组织为一组步骤 (buildWalls, buildDoor 等)。
  • 要创建对象,您需要对生成器对象执行一系列步骤。重要的是,您不需要调用所有步骤。
  • 您只能调用生成对象的特定配置所需的步骤。
Applying the Builder pattern
  • 当您需要构建产品的各种表示形式时,某些构造步骤可能需要不同的实现。例如,小屋的墙壁可以用木头建造,但城堡的墙壁必须用石头建造。
  • 在这种情况下,您可以创建多个不同的构建器类,这些类 实现同一组构建步骤,但方式不同。
  • 然后,您可以在构造过程中使用这些 构建器(即对构建步骤的一组有序调用)来生成不同类型的对象。

  • 不同的构建器以不同的方式执行相同的任务。例如,想象一个建筑商用木头和玻璃建造一切,第二个建筑商用石头和铁建造一切,第三个建筑商使用黄金和钻石建造一切。
  • 通过 调用 相同的一组步骤,但是 因为 使用材料不同,可以从第一个建造者那里获得一座普通房屋,从第二个建造者那里获得一座小城堡,从第三个建造者那里获得一座宫殿。
  • 但是,仅当调用构建步骤的 客户端代码能够使用通用接口与构建器交互时,这才有效。
主管
  • 可以更进一步,将用于构建产品的构建器步骤的一系列调用提取到一个名为 director的单独类中。
  • director 类定义执行顺序,而构建器提供这些步骤的实现。

  • 主管知道要执行哪些构建步骤才能获得有效的产品。
  • 在您的计划中开设主管课程并不是绝对必要的。您始终可以直接从客户端代码中 按特定顺序调用构建步骤。但是,director 类可能是放置各种构造例程的好地方,以便您可以在整个程序中重用它们。
  • 此外,director 类在客户端代码中完全隐藏了产品构造的细节。客户只需将构建者与导向器关联,与导向器一起启动构建,然后从构建者那里获得结果。
结构

  • Builder 界面 声明了所有类型的生成器通用的 产品构建步骤。
  • Concrete Builders 提供了 施工步骤的不同实现。混凝土建筑商生产的产品可能不遵循通用界面。
  • 产品 是结果对象。由不同构建器构建的产品不必属于同一类层次结构或接口。
  • Director 类 定义调用构造步骤的顺序,以便您可以创建和重用产品的特定配置。
  • Client 必须将其中一个构建器对象与 director 关联
  • 通常,它只通过 director 的构造函数的参数完成一次。然后,director 使用该 builder 对象进行所有进一步的构造。但是,当客户端将 builder 对象传递给 director 的 production 方法时,还有另一种方法。在这种情况下,每次使用 director 制作内容时,您都可以使用不同的构建器。
伪代码
  • 示例说明了在构建不同类型的产品(例如汽车)时如何重用相同的对象构造代码,并为它们创建相应的手册。

  • 汽车是一个复杂对象, 有数百种不同的制造方法。 我们没有在 汽车类中塞入一个巨型构造函数, 而是将汽车组装代码抽取到单独的汽车生成器类中。 该类中有一组方法可用来配置汽车的各种部件。
  • 如果客户端代码需要组装一辆与众不同、 精心调教的汽车, 它可以直接调用生成器。 或者, 客户端可以将组装工作委托给主管类, 因为主管类知道如何使用生成器制造最受欢迎的几种型号汽车。
  • 你或许会感到吃惊, 但确实每辆汽车都需要一本使用手册 (说真的, 谁会去读它们呢?)。 使用手册会介绍汽车的每一项功能, 因此不同型号的汽车, 其使用手册内容也不一样。 因此, 你可以复用现有流程来制造实际的汽车及其对应的手册。 当然, 编写手册和制造汽车不是一回事, 所以我们需要另外一个生成器对象来专门编写使用手册。 该类与其制造汽车的兄弟类都实现了相同的制造方法, 但是其功能不是制造汽车部件, 而是描述每个部件。 将这些生成器传递给相同的主管对象, 我们就能够生成一辆汽车或是一本使用手册了。
  • 最后一个部分是获取结果对象。 尽管金属汽车和纸质手册存在关联, 但它们却是完全不同的东西。 我们无法在主管类和具体产品类不发生耦合的情况下, 在主管类中提供获取结果对象的方法。 因此, 我们只能通过负责制造过程的生成器来获取结果对象。
// 只有当产品较为复杂且需要详细配置时,使用生成器模式才有意义。下面的两个
// 产品尽管没有同样的接口,但却相互关联。
class Car is
    // 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车(
    // 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。

class Manual is
    // 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。


// 生成器接口声明了创建产品对象不同部件的方法。
interface Builder is
    method reset()
    method setSeats(……)
    method setEngine(……)
    method setTripComputer(……)
    method setGPS(……)

// 具体生成器类将遵循生成器接口并提供生成步骤的具体实现。你的程序中可能会
// 有多个以不同方式实现的生成器变体。
class CarBuilder implements Builder is
    private field car:Car

    // 一个新的生成器实例必须包含一个在后续组装过程中使用的空产品对象。
    constructor CarBuilder() is
        this.reset()

    // reset(重置)方法可清除正在生成的对象。
    method reset() is
        this.car = new Car()

    // 所有生成步骤都会与同一个产品实例进行交互。
    method setSeats(……) is
        // 设置汽车座位的数量。

    method setEngine(……) is
        // 安装指定的引擎。

    method setTripComputer(……) is
        // 安装行车电脑。

    method setGPS(……) is
        // 安装全球定位系统。

    // 具体生成器需要自行提供获取结果的方法。这是因为不同类型的生成器可能
    // 会创建不遵循相同接口的、完全不同的产品。所以也就无法在生成器接口中
    // 声明这些方法(至少在静态类型的编程语言中是这样的)。
    //
    // 通常在生成器实例将结果返回给客户端后,它们应该做好生成另一个产品的
    // 准备。因此生成器实例通常会在 `getProduct(获取产品)`方法主体末尾
    // 调用重置方法。但是该行为并不是必需的,你也可让生成器等待客户端明确
    // 调用重置方法后再去处理之前的结果。
    method getProduct():Car is
        product = this.car
        this.reset()
        return product

// 生成器与其他创建型模式的不同之处在于:它让你能创建不遵循相同接口的产品。
class CarManualBuilder implements Builder is
    private field manual:Manual

    constructor CarManualBuilder() is
        this.reset()

    method reset() is
        this.manual = new Manual()

    method setSeats(……) is
        // 添加关于汽车座椅功能的文档。

    method setEngine(……) is
        // 添加关于引擎的介绍。

    method setTripComputer(……) is
        // 添加关于行车电脑的介绍。

    method setGPS(……) is
        // 添加关于 GPS 的介绍。

    method getProduct():Manual is
        // 返回使用手册并重置生成器。


// 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
// 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
// 不是必需的。
class Director is
    // 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通
    // 过这种方式改变最新组装完毕的产品的最终类型。主管可使用同样的生成步
    // 骤创建多个产品变体。
    method constructSportsCar(builder: Builder) is
        builder.reset()
        builder.setSeats(2)
        builder.setEngine(new SportEngine())
        builder.setTripComputer(true)
        builder.setGPS(true)

    method constructSUV(builder: Builder) is
        // ……


// 客户端代码会创建生成器对象并将其传递给主管,然后执行构造过程。最终结果
// 将需要从生成器对象中获取。
class Application is

    method makeCar() is
        director = new Director()

        CarBuilder builder = new CarBuilder()
        director.constructSportsCar(builder)
        Car car = builder.getProduct()

        CarManualBuilder builder = new CarManualBuilder()
        director.constructSportsCar(builder)

        // 最终产品通常需要从生成器对象中获取,因为主管不知晓具体生成器和
        // 产品的存在,也不会对其产生依赖。
        Manual manual = builder.getProduct()

生成器模式适合应用场景

使用生成器模式可避免 重叠构造函数 telescoping constructor 的出现

假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。

class Pizza {
    Pizza(int size) { …… }
    Pizza(int size, boolean cheese) { …… }
    Pizza(int size, boolean cheese, boolean pepperoni) { …… }
    // ……

只有在 C# 或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。

生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。

当你希望使用代码创建不同形式的产品 例如石头或木头房屋 可使用生成器模式

  • 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
  • 基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。

使用生成器构造组合树或其他复杂对象

  • 生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
  • 生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。

实现方法

  1. 清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 否则你将无法进一步实施该模式。
  2. 在基本生成器接口中声明这些步骤。
  3. 为每个形式的产品创建具体生成器类, 并实现其构造步骤。

不要忘记实现获取构造结果对象的方法。 你不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。

  1. 考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式。
  2. 客户端代码会同时创建生成器和主管对象。 构造开始前, 客户端必须将生成器对象传递给主管对象。 通常情况下, 客户端只需调用主管类构造函数一次即可。 主管类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给主管类的制造方法。
  3. 只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。

生成器模式优缺点

  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
  • 生成不同形式的产品时, 你可以复用相同的制造代码。
  • 。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
  • 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。

与其他模式的关系

  • ​ 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。 
  • 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。 
  • 你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。 
  • 你可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。 
  • 抽象工厂、 生成器和原型都可以用单例模式来实现。 

C++代码

此示例说明了 Builder 设计模式的结构。它侧重于回答以下问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?
⭕main.cc:概念示例
#include<iostream>
#include<vector>
#include<string>
/*
* 使用 Builder 模式,只有在 产品 相当复杂 且需要广泛的配置时 才有意义
* 
* 与其他 创建型 模式不同,不同的具体 构建器 可以生成不同的产品。
* 就像是构建小屋的构建器,用的是木头
* 构建城堡的构建器,用的是 砖头
* 但是不影响 一个房子 都是按照 一扇门,四面墙,一个屋顶来组成的
* 
* 换句话说,不同构建器可能并不总是遵循相同的接口
*/

class Product1 {
public:
	std::vector<std::string> parts_;//存储 产品组件

	//打印产品的 所有组件
	void ListParts() const {
		std::cout << "Product parts:";
		for (size_t i = 0; i < parts_.size(); i++)
		{
			if (parts_[i] == parts_.back())
			{
				std::cout << parts_[i];
			}
			else
			{
				std::cout << parts_[i] << ",";
			}
		}
		std::cout << "\n\n";
	}
};

/* 
* Builder 接口 指定了创建Product 对象 不同部分的方法
*/
class Builder
{
public:
	virtual ~Builder() {}
	virtual void ProducePartA() const = 0;//生产 门
	virtual void ProducePartB() const = 0;//生产 墙
	virtual void ProducePartC() const = 0;//生产 屋顶
};

/*
* 具体 构建器类 遵循Builder接口,并提供 构建步骤的具体实现
* 程序中 可能会有 多个不同实现的Builder变种
*/

class ConcreteBuilder1:public Builder
	//!
	//! 这个地方 因为:没切换为中文,报错了...
{
private:
	Product1* product;//指向 正在 构建的产品
	//例如 是一个 木屋

public:
	ConcreteBuilder1()
	{
		this->Reset();//初始化 一个 空白的 产品对象
		//用于 后续组装
	}

	~ConcreteBuilder1()
	{
		delete product;
	}

	void Reset()
	{
//!
		this->product = new Product1();
	//创建产品
	}

	void ProducePartA() const override
	{

	//安全 继承类
		this->product->parts_.push_back("PartA1");
	//调用木头
	}

	void ProducePartB() const override
	{
		this->product->parts_.push_back("PartB1");
	}

	void ProducePartC() const override
	{
		this->product->parts_.push_back("PartC1");
	}

	/**
		* 具体构建器应提供自己的方法来获取结果。因为不同类型的构建器可能创建完全不同且不遵循相同接口的产品,
		* 因此这样的方法不能在基Builder接口中声明(至少在静态类型编程语言中是这样)。
		* 注意,在调用GetProduct后,通常建议在该方法体内调用reset方法使构建器准备好生产下一个产品。
	*/

//准备好 生产  下一个产品
	Product1* GetProduct() {
		Product1* result = this->product;
		this->Reset();
		return result;
	}

};

/**
 * Director只负责以特定顺序执行构建步骤。这对于根据特定顺序或配置生产产品非常有用。
 * 严格来说,Director类不是必须的,因为客户端可以直接控制构建器。
 */

class Director {
private:
	Builder* builder;

public:
	void set_builder(Builder* builder) {
		this->builder = builder;
	}

//进行 组合
	void BuildMinimalViableProduct() {
		this->builder->ProducePartA();
	}

	void BuildFullFeaturedProduct() {
		this->builder->ProducePartA();
		this->builder->ProducePartB();
		this->builder->ProducePartC();
	}
};

/**
 * 客户端代码创建一个构建器对象,将其传递给Director,然后启动构建过程。最终结果是从构建器对象中检索的。
 * 注意:这里为了简单使用了原始指针,但在实际应用中推荐使用智能指针来避免内存泄漏。
 */

void ClientCode(Director& director)
{
	ConcreteBuilder1* builder = new ConcreteBuilder1();
	director.set_builder(builder);
	std::cout << "Standard basic product:\n";
	director.BuildMinimalViableProduct();

	Product1* p = builder->GetProduct();
	p->ListParts();
	delete p;

	std::cout << "Standard full featured product:\n";
	director.BuildFullFeaturedProduct();

	p = builder->GetProduct();
	p->ListParts();
	delete p;

	// Remember, the Builder pattern can be used without a Director class.
	std::cout << "Custom product:\n";
	builder->ProducePartA();
	builder->ProducePartC();
	p = builder->GetProduct();
	p->ListParts();
	delete p;

	delete builder;
}

int main() {
	Director* director = new Director();
	ClientCode(*director);
	delete director;
	return 0;
}
Output.txt:执行结果


http://www.niftyadmin.cn/n/5864656.html

相关文章

C语言【指针篇】(一)

前言 指针基础概念理解&#xff0c;从底层出发理解指针 C语言【指针篇】&#xff08;一&#xff09; 前言正文1. 内存和地址1.1 内存1.2 究竟该如何理解编址 2. 指针变量和地址2.1 取地址操作符(&)2.2 指针变量和解引用操作符(*)2.3 指针变量的大小 3. 指针变量类型的意义…

Qt 中集成mqtt协议

一&#xff0c;引入qmqtt 库 我是将整个头文件/源文件都添加到了工程中进行编译&#xff0c;这样 跨平台时 方便&#xff0c;直接编译就行了。 原始仓库路径&#xff1a;https://github.com/emqx/qmqtt/tree/master 二&#xff0c;使用 声明一个单例类&#xff0c;将订阅到…

from flask_session import Session 为什么是Session(app)这么用?

在 Flask 中&#xff0c;from flask_session import Session 和 Session(app) 的用法是为了配置和使用 Flask-Session 扩展&#xff0c;将用户的会话&#xff08;Session&#xff09;数据存储到服务器端&#xff08;如 Redis、数据库或文件系统&#xff09;&#xff0c;而不是默…

计算机视觉:经典数据格式(VOC、YOLO、COCO)解析与转换(附代码)

第一章&#xff1a;计算机视觉中图像的基础认知 第二章&#xff1a;计算机视觉&#xff1a;卷积神经网络(CNN)基本概念(一) 第三章&#xff1a;计算机视觉&#xff1a;卷积神经网络(CNN)基本概念(二) 第四章&#xff1a;搭建一个经典的LeNet5神经网络(附代码) 第五章&#xff1…

MySQL 单表访问方法详解

单表访问 MySQL 单表访问方法详解&#xff1a;高效查询之道**一、 查询执行基础****二、 访问方法 (Access Method) 概念****三、 具体访问方法 (从最优到最差)****四、 注意事项****五、 总结与优化建议****六、 电商网站数据存储应用示例****七、 数据备份与恢复模型 (补充)*…

Python安全之反序列化——pickle/cPickle

一&#xff0e; 概述 Python中有两个模块可以实现对象的序列化&#xff0c;pickle和cPickle&#xff0c;区别在于cPickle是用C语言实现的&#xff0c;pickle是用纯python语言实现的&#xff0c;用法类似&#xff0c;cPickle的读写效率高一些。使用时一般先尝试导入cPickle&…

基于AT89C52单片机的出租车计价器

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/90419909?spm1001.2014.3001.5501 C17 部分参考设计如下&#xff1a; 摘要 随着城市交通行业的迅速发展&#xff0c;出租车作为最主要的城市公共交通工具之一…

http 协议在互联网中扮演着怎样的角色?

互联网各领域资料分享专区(不定期更新): Sheet 正文 HTTP(超文本传输协议)在互联网中扮演着核心通信协议的角色,是万维网(World Wide Web)的基础技术之一。 1. 客户端-服务器交互的桥梁 浏览器与服务器的通信语言:HTTP定义了浏览器(客户端)如何向服务器请求资源(如…