本文是该系列的第四篇,采用问答式的方式展开,问题由我提出,答案由 Chat AI 作出,灰色背景的文字则主要是我的旁白与思考。
问题列表
- Q.1 说说建造者模式
- A.1
- Q.2 “将一个复杂对象的构建过程和其表示分离”,这里不是很清楚,给我讲解一下吧
- A.2
- Q.3 给我举一个建造者模式的例子吧
- A.3
- Q.4 你的代码里,客户是通过 builderA 和 builderB 来获取汽车,似乎不太好呢
- A.4
- Q.5 客户端是应该通过建造者获取产品还是通过指挥者类来获取产品呢?
- A.5
- Q.6 我是想问 “getCar这个方法应该由客户端调用,还是由指挥者类来调用”
- A.6
- Q.7 复杂的产品,必须要继承自同一个产品基类吗?
- A.7
- Q.8 画类图
- A.7
Q.1 说说建造者模式
A.1
建造者模式是一种创建型设计模式,它将一个复杂对象的构建过程和其表示分离,使得同样的构建过程可以创建不同的表示。这个模式的核心思想是将一个复杂的对象构建过程拆分成多个步骤,在这些步骤之间建立一定的依赖关系,最终生成一个完整的对象。
建造者模式通常包含以下几个角色:
-
Builder(建造者):定义用于构建复杂对象的接口,包含多个抽象方法,每个方法对应一个构建步骤。
-
ConcreteBuilder(具体建造者):实现 Builder 接口,完成复杂对象的各个构建步骤,最终生成一个合成对象。同时具有访问和获取构建结果的方法。
-
Director(指挥者):负责组合 Builder 中的各个构建步骤,最终得到复杂对象。它不直接创建对象,而是通过调用Builder的方法得到一个构建过程的完整对象。
-
Product(产品):产品是由具体建造者生产的复杂对象,它包含了多个部件,具有一定的复杂性和功能性。
使用建造者模式可以有效地将对象的创建过程模块化,降低了复杂对象的构建难度,提高了可维护性和可扩展性。
Q.2 “将一个复杂对象的构建过程和其表示分离”,这里不是很清楚,给我讲解一下吧
A.2
当我们需要构建一个复杂对象时,可能需要通过多个步骤来完成这个过程,包括创建多个部分、添加属性、设置关联等等,这些步骤的执行顺序和具体实现可能因对象类型、使用场景等因素而不同。此时,如果将这个复杂的构建过程和最终对象的表示紧密耦合在一起,将会导致代码复杂、难以维护。
建造者模式的核心思想就是将这个复杂的构建过程和最终对象的表示分离开来,把构建复杂对象的过程拆分成一些简单的步骤,这些步骤之间不直接关联,而是在指挥者的统筹下依次调用各个步骤,最终生成构建完成的对象。
通过这种方式,建造者模式实现了构建过程和表示之间的松耦合,使得我们可以根据需要构建不同的对象,而且能够更加灵活地对构建过程进行扩展或者重构,同时也提高了代码的可读性和可维护性。
表示:其实就是我们想要产生的最终对象,也就是 Product 类对象;
构建:Builder 类就是对象的构建;
构建过程:是指 Director 类中产品构建步骤的装配顺序,该过程是稳定不会变化的。只要传递给指挥者类不同的具体建造者(ConcreteBuilder)子类,它就能构建出不同的产品(Product)。
下面举一个例子来说明。
Q.3 给我举一个建造者模式的例子吧
A.3
假设我们要创建一个汽车类,这个汽车类包含多个部件(例如车架、发动机、轮胎、油箱等等),并且每个部件的属性、数量和顺序都有所不同。
我们可以使用建造者模式来构建这个复杂对象。以下是一个简单的示例代码:
#include <iostream>
#include <string>
using namespace std;//定义汽车类
class Car {
public://车架string frame;//发动机string engine;//轮胎string tire;//油箱string tank;//车辆信息打印函数void printCarInfo() {cout << "Car Info: " << endl;cout << "Frame: " << frame << endl;cout << "Engine: " << engine << endl;cout << "Tire: " << tire << endl;cout << "Tank: " << tank << endl;}
};//定义汽车建造者类
class CarBuilder {
public://构造函数CarBuilder() : m_car(new Car) {}//设置车架virtual void setFrame() {}//设置发动机virtual void setEngine() {}//设置轮胎virtual void setTire() {}//设置油箱virtual void setTank() {}//获取构造完成的车辆Car* getCar() {return m_car;}protected://保存构造完成的车辆Car* m_car;
};//定义汽车建造者A
class CarBuilderA : public CarBuilder {
public://设置车架void setFrame() {m_car->frame = "Frame A";}//设置发动机void setEngine() {m_car->engine = "Engine A";}//设置轮胎void setTire() {m_car->tire = "Tire A";}//设置油箱void setTank() {m_car->tank = "Tank A";}
};//定义汽车建造者B
class CarBuilderB : public CarBuilder {
public://设置车架void setFrame() {m_car->frame = "Frame B";}//设置发动机void setEngine() {m_car->engine = "Engine B";}//设置轮胎void setTire() {m_car->tire = "Tire B";}//设置油箱void setTank() {m_car->tank = "Tank B";}
};//定义指挥者类
class Director {
public:void constructCar(CarBuilder* builder) {builder->setFrame();builder->setEngine();builder->setTire();builder->setTank();}
};//主函数
int main() {CarBuilderA builderA;CarBuilderB builderB;Director director;//指挥者指定汽车建造者A来构建汽车director.constructCar(&builderA);Car* carA = builderA.getCar();carA->printCarInfo();//指挥者指定汽车建造者B来构建汽车director.constructCar(&builderB);Car* carB = builderB.getCar();carB->printCarInfo();return 0;
}
在代码中,Car
类是我们所要构建的复杂对象。CarBuilder
是汽车建造者的接口类,包含了构建汽车的各种方法。不同的汽车建造者继承这个接口类,并完成各自的构建过程,即不同的车架、发动机、轮胎和油箱。Director
类是指挥者,负责组合不同步骤的构建者来生成最终的复杂对象加粗样式。
最后,我们可以使用CarBuilderA
和CarBuilderB
来构建不同的汽车对象。
这个例子还是比较好理解的,对各个类的作用也进行了讲解。
一般来说,指挥者类组合一个 builder 类的对象,同时提供一个接口让客户端来设置不同的builder具体对象。但像例子中这样不直接持有也是可以的,在使用的时候直接传入即可。
我们注意到,在main()
函数(客户端)中,是通过 buildA、buildB 这个两个具体建造者类对象来获取产品的。这点我并不认可,因为这个例子明显是一个标准的建造者模式示例,拥有指挥者类,不应该让客户端继续与建造者类沟通。
Q.4 你的代码里,客户是通过 builderA 和 builderB 来获取汽车,似乎不太好呢
A.4
非常抱歉,我的代码确实有误,我在获取汽车对象那一段没有更新。在正确的代码中,我们应该将指挥者和具体的汽车建造者进行配合,完成具体的构建过程,最终返回构建完成的汽车对象。
以下是代码的修改版本:
#include <iostream>
#include <string>
using namespace std;//定义汽车类
class Car {
public://车架string frame;//发动机string engine;//轮胎string tire;//油箱string tank;//车辆信息打印函数void printCarInfo() {cout << "Car Info: " << endl;cout << "Frame: " << frame << endl;cout << "Engine: " << engine << endl;cout << "Tire: " << tire << endl;cout << "Tank: " << tank << endl;}
};//定义汽车建造者类
class CarBuilder {
public://构造函数CarBuilder() : m_car(new Car) {}//设置车架virtual void setFrame() {}//设置发动机virtual void setEngine() {}//设置轮胎virtual void setTire() {}//设置油箱virtual void setTank() {}//获取构造完成的车辆Car* getCar() {return m_car;}protected://保存构造完成的车辆Car* m_car;
};//定义汽车建造者A
class CarBuilderA : public CarBuilder {
public://设置车架void setFrame() {m_car->frame = "Frame A";}//设置发动机void setEngine() {m_car->engine = "Engine A";}//设置轮胎void setTire() {m_car->tire = "Tire A";}//设置油箱void setTank() {m_car->tank = "Tank A";}
};//定义汽车建造者B
class CarBuilderB : public CarBuilder {
public://设置车架void setFrame() {m_car->frame = "Frame B";}//设置发动机void setEngine() {m_car->engine = "Engine B";}//设置轮胎void setTire() {m_car->tire = "Tire B";}//设置油箱void setTank() {m_car->tank = "Tank B";}
};//定义指挥者类
class Director {
public:Car* constructCar(CarBuilder* builder) {builder->setFrame();builder->setEngine();builder->setTire();builder->setTank();return builder->getCar();}
};//主函数
int main() {CarBuilderA builderA;CarBuilderB builderB;Director director;//指挥者指定汽车建造者A来构建汽车Car* carA = director.constructCar(&builderA);carA->printCarInfo();//指挥者指定汽车建造者B来构建汽车Car* carB = director.constructCar(&builderB);carB->printCarInfo();return 0;
}
在新的代码中,Director
类的constructCar
函数接受一个CarBuilder
指针参数,用于在执行构建过程并返回构建成功的汽车对象。客户端代码现在可以通过Director
来调用不同的CarBuilder
来获得不同类型的汽车对象了。
既然分离出了指挥者类,就应该通过指挥者类来获取最终表示,而不应该让客户端继续调用建造者类。当然,如果,没有分离出指挥者类,那自然是没任何问题的,此处不再展开。
为了确定,AI 不是屈服于我作出的违心答案,我还将继续追问这一点。
Q.5 客户端是应该通过建造者获取产品还是通过指挥者类来获取产品呢?
A.5
客户端应该通过指挥者类来获取产品,而不是直接通过建造者获取产品。
指挥者类的作用是协调建造者中的各个构建步骤,确保整个构建过程的正确执行。客户端如果直接通过建造者来获取产品,那么就需要了解建造者的具体实现细节,带来了一定的耦合性,同时也不利于后期的维护和扩展。
通过指挥者类来获取产品,客户端只需要与指挥者类进行交互,即使建造者的实现发生了改变,也不会对客户端造成影响。同时,指挥者类也可以封装建造者构建对象的过程,提供更加方便的接口给客户端使用。
我感觉它这里回答的有问题,尤其是斜体字部分,或许它没有理解我的问题。客户端即使通过建造者类来获取产品,也不需要了解具体实现细节,只是调用 getCar() 接口而已。因此,建造者的实现发生了改变,对客户端的调用也不会有任何影响。
这就是 AI 一本正经的胡说八道吧。
Q.6 我是想问 “getCar这个方法应该由客户端调用,还是由指挥者类来调用”
A.6
getCar
方法应该由指挥者类来调用,这也是指挥者类的作用的一部分。
由于指挥者类负责协调建造者的构建步骤,最终将构建完成的产品组装起来,因此在 constructCar
方法中应该调用建造者的各个构建方法,并在完成构建之后,从建造者对象中获取构建完成的产品。指挥者类可以把获得的产品返回给客户端,或者进一步处理之后返回。因此,在 constructCar
方法中的返回值就是通过建造者构建好的产品。
对于客户端调用建造者的 getCar
方法来获得产品,会暴露建造的过程细节,这样会导致建造者与客户端之间的紧耦合。
现在它算是明确回答了我的问题,应该让指挥者类来返回成品,别让客户端再调用 getCar() 方法了。但“会暴露建造的过程细节”我是不完全认同的,也就是多暴露一个 getCar() 方法罢了,还称不上实现细节,耦合确实加重了。
Q.7 复杂的产品,必须要继承自同一个产品基类吗?
A.7
在建造者模式中,复杂的产品并不一定必须继承自同一个产品基类。建造者模式关注的是构建过程和组装复杂对象,而不是具体的产品类型。
建造者模式允许构建不同类型的产品,这些产品可以是无关的、不具备共同特征的。如果产品之间没有共同的属性或行为,那么它们可以是独立的类,而不需要继承自同一个基类。
然而,如果多个产品类型具有一些共同的属性或行为,使用继承来实现一个产品基类是合理的。通过继承,可以提取和共享共同的代码,简化代码结构,同时也方便对产品进行统一管理和扩展。
继承产品基类有利于实现多态,使得客户端可以以统一的方式操作不同类型的产品。此外,基类也可以定义一些虚拟方法,供子类进行自定义实现。
所以,是否需要继承自同一个产品基类取决于具体的设计需求和产品结构,根据实际情况来决定。
关注的不是产品类型,而是产品的构建步骤,只要有相同的构建步骤,就可以使用建造者模式。
Q.8 画类图
A.7
要记住:并不要求创建的产品继承自同一个基类,只是我们这个例子恰好如此。