【设计模式】建造者模式

发布于:2025-03-16 ⋅ 阅读:(20) ⋅ 点赞:(0)

三、建造者模式

3.3 建造者模式

建造者(Builder) 模式也称构建器模式、构建者模式或生成器模式,同工厂模式或原型 模式一样,也是一种创建型模式。建造者模式比较复杂,不太常用,但这并不表示不需要了 解和掌握该模式。建造者模式通常用来创建一个比较复杂的对象(这也是建造者模式本身 比较复杂的主要原因),该对象的构建一般是需要按一定顺序分步骤进行的。例如,建造一 座房子(无论是平房、别墅还是高楼),通常都需要按顺序建造地基、建筑体、建筑顶等步骤, 建造一辆汽车通常会包含发动机、方向盘、轮胎等部件,创建一份报表也通常会包含表头、表 身、表尾等部分。

3.3.1 一个具体实现范例的逐步重构

这里还是以游戏中的怪物类来讲解。怪物同样分为亡灵类怪物、元素类怪物、机械类怪 物 。

在创建怪物对象的过程中,有一个创建步骤非常烦琐——把怪物模型创建出来用于显 示给玩家。策划规定,任何一种怪物都由头 部、躯干(包括颈部、尾巴等)、肢体 3个部位组 成,在制作怪物模型时,头部、躯干、肢体模型分开制作。每个部位模型都会有一些位置和方 向信息,用于挂接在其他部位模型上,比如将头部挂接到躯干部,再将肢体挂接到躯干部就 可以构成一个完整的怪物模型。当然, 一些在水中的怪物可能不包含四肢,那么将肢体挂接 到躯干部这个步骤什么都不做即可。
之所以在制作怪物模型时将头部、躯干、肢体模型分开制作,是便于同类型怪物的3个 组成部位进行互换。试想一下,如果针对亡灵类怪物制作了3个头部、3个躯干以及3个肢 体,则最多可以组合出27个外观不同的亡灵类怪物(当然,有些组合看起来会比较丑陋,不 适合用在游戏中),这既节省了游戏制作成本,又节省了游戏运行时对内存的消耗。
程序需要先把怪物模型载入内存并进行装配以保证正确地显示给玩家看。所以程序需 要进行如下编码步骤:

  • (1)将怪物的躯干模型信息读入内存并提取其中的位置和方向信息;
  • (2)将怪物的头部和四肢模型信息读入内存并提取其中的位置和方向信息;
  • (3)将头部和四肢模型以正确的位置和方向挂接(Mount) 到躯干部位,从而装配出完 整的怪物模型。
    因为讲解的侧重点不同,所以在这里重新实现 Monster 怪物类,在该类中引入 Assemble 成员函数,用于装配一个怪物,代码大概如下:
class Monster
{
public:
	virtual ~Monster();
	
	void Assemble(std::string strmodelno);

	virtual void LoadTrunckModel(std::string strno) = 0;
	virtual void LoadHeadModel(std::string strno) = 0;
	virtual void LoadLimbsModel(std::string strno) = 0;

};

Monster::~Monster()
{
}

void Monster::Assemble(std::string strmodelno) { //9位数
	LoadTrunckModel(strmodelno.substr(0, 3));
	LoadHeadModel(strmodelno.substr(3, 3));
	LoadLimbsModel(strmodelno.substr(6, 3));
}

上述代码只是大致的实现代码,在Assemble 成员函数中实现了载入一个怪物模型的 固定流程——分别载入了躯干、头部、四肢模型并将它们装配到一起,游戏中所有怪物的载 入都遵循该流程(其中的代码是稳定的,不发生变化),所以这里的 Assemble 成员函数很像 模板方法模式中的模板方法。

笔者在上述代码中做了很多简化,例如 LoadTrunkModel 载入躯干模型时可能要返回 一个与载入结果相关的结构(模型结构)以传递到后续即将调用的 LoadHeadModel 和 LoadLimbsModel 成员函数中,这样这两个成员函数就可以在载入头部和四肢模型时完善 (继续填充)该结构等,因为这些内容与设计模式无关,所以全部省略。

因为亡灵类怪物、元素类怪物、机械类怪物的外观差别巨大,所以虽然这3类怪物的载入 流程相同,但不同种类怪物的细节载入差别很大,所以,将LoadTrunkModel、LoadHeadModel、
LoadLimbsModel (构建模型的子步骤)成员函数写为虚函数以方便在Monster 的子类中重 新实现。

有些读者可能会希望将 Assemble 成员函数的内容放到 Monster 类构造函数中以达到 怪物对象创建时就载入模型数据的目的,但在本书附录A 中将重点强调,不要在类的构造 函数与析构函数中调用虚函数以防止出现问题,而Assemble 调用的都是虚函数,所以,切 不 可 将 Assemble 成员函数的内容放到 Monster 类构造函数中实现。

接下来分别实现继承自父类Monster 的亡灵类怪物、元素类怪物、机械类怪物相关类 M_Undead、M_Element、M_Mechanic, 代码如下:

class M_Undead : public Monster {
public:
	virtual void LoadTrunckModel(std::string strno) override;
	virtual void LoadHeadModel(std::string strno) override;
	virtual void LoadLimbsModel(std::string strno) override;

};
class M_Element : public Monster {
public:
	virtual void LoadTrunckModel(std::string strno) override;
	virtual void LoadHeadModel(std::string strno) override;
	virtual void LoadLimbsModel(std::string strno) override;

};


class M_Mechanic : public Monster {
public:
	virtual void LoadTrunckModel(std::string strno) override;
	virtual void LoadHeadModel(std::string strno) override;
	virtual void LoadLimbsModel(std::string strno) override;
};


void M_Mechanic::LoadTrunckModel(std::string strno) {
	std::cout << "载入机械类怪物的躯干" << std::endl;
}

void M_Mechanic::LoadHeadModel(std::string strno) {
	std::cout << "载入机械类怪物的头部" << std::endl;
}

void M_Mechanic::LoadLimbsModel(std::string strno) {
	std::cout << "载入机械类怪物的四肢" << std::endl;
}

void M_Element::LoadTrunckModel(std::string strno) {
	std::cout << "载入元素类怪物的躯干" << std::endl;
}

void M_Element::LoadHeadModel(std::string strno) {
	std::cout << "载入元素类怪物的头部" << std::endl;
}

void M_Element::LoadLimbsModel(std::string strno) {
	std::cout << "载入元素类怪物的四肢" << std::endl;
}

void M_Undead::LoadTrunckModel(std::string strno) {
	std::cout << "载入亡灵类怪物的躯干" << std::endl;
}

void M_Undead::LoadHeadModel(std::string strno) {
	std::cout << "载入亡灵类怪物的头部" << std::endl;
}

void M_Undead::LoadLimbsModel(std::string strno) {
	std::cout << "载入亡灵类怪物的四肢" << std::endl;
}

可以看到,在代码中,创建了一只元素类怪物对象,然后调用Assemble 成员函数对怪 物模型进行装配以用于后续的怪物显示。

上述代码看起来更像是模板方法模式。但阅读代码应该更侧重代码的实现目的而非代码的实现结构,这些代码用于创建怪物对象以显示给玩家看,但怪物的创建比较复杂,严格 地说,应该是怪物模型的载入过程比较复杂,需要按顺序分别载入躯干、头部、四肢模型并实 现不同部位模型之间的挂接。至此,可以说所需的功能(指模型载入功能)已经完成,如果程 序员不再继续开发,也是可以的。但是,目前的代码实现结构还不能称为建造者模式,通过 对程序进一步拆分还可以进一步提升灵活性。

这里将 Assemble 、LoadTrunkModel 、LoadHeadModel 、LoadLimbsModel 这些与模型 载入与挂接步骤相关的成员函数称为构建过程相关函数。考虑到Monster 类中要实现的 逻辑功能可能较多,如果把构建过程相关函数提取出来(分离)放到一个单独的类中,不但可 以减少Monster 类中的代码量,还可以增加构建过程相关代码的独立性,日后游戏中任何 由头部、躯干、肢体 3个部位组成并需要将头部挂接到躯干部,再将肢体挂接到躯干部的生 物,都可以通过这个单独的类实现模型的构建。

引入与怪物类同层次的相关构建器类,把怪物类中的代码搬到相关的构建器类中,代码 如 下 :

class MonsterBuilder
{
public:
	virtual ~MonsterBuilder();
	void Assemble(std::string strmodelno);

	virtual void LoadTrunckModel(std::string strno) = 0;
	virtual void LoadHeadModel(std::string strno) = 0;
	virtual void LoadLimbsModel(std::string strno) = 0;

	Monster* GetResult();

protected:
	Monster* m_pMonster = nullptr;
};

class M_Undead;
class M_UndeadBuilder : public MonsterBuilder
{
public:
	M_UndeadBuilder();

	virtual ~M_UndeadBuilder();

	virtual void LoadTrunckModel(std::string strno) override;
	virtual void LoadHeadModel(std::string strno) override;
	virtual void LoadLimbsModel(std::string strno) override;

};

class M_ElementBuilder:MonsterBuilder
{
public:
	virtual ~M_ElementBuilder();
	
	virtual void LoadTrunckModel(std::string strno) = 0;
	virtual void LoadHeadModel(std::string strno) = 0;
	virtual void LoadLimbsModel(std::string strno) = 0;
};

class M_MechanicBuilder :MonsterBuilder
{
public:
	virtual ~M_MechanicBuilder();

	virtual void LoadTrunckModel(std::string strno) = 0;
	virtual void LoadHeadModel(std::string strno) = 0;
	virtual void LoadLimbsModel(std::string strno) = 0;
};

在上述代码中,可以注意到,在MonsterBuilder 类中放置了一个指向 Monster 类的成 员变量指针 m_pMonster, 同时引入 GetResult 成员函数用于返回这个m pMonster 指 针 , 也就是说,当一个复杂的对象通过构建器构建完成后,可以通过GetResult 返回。分别为 Monster 的子类M_Undead 、M _Element 、M_Mechanic 创建对应的父类为 MonsterBuilder 的构建子类M_UndeadBuilder 、M_ElementBuilder 、M_MechanicBuilder, 因为工厂方法模 式是创建一个与产品等级结构相同的工厂等级结构,所以这部分看起来似乎与工厂方法模 式有些相似之处。

重点观察 MonsterBuilder 类中的Assemble 成员函数,前面曾经提过,该成员函数中的 代码是稳定的,不会发生变化。所以可以继续把Assemble 成员函数的功能拆出到一个新 类中(这步拆分也不是必需的)。创建新类 MonsterDirector (扮演一个指挥者角色),将 MonsterBuilder 类中的 Assemble 成员函数整个迁移到 MonsterDirector 类中并按照惯例 重新命名为Construct, 同时,在 MonsterDirector 类中放置一个指向 MonsterBuilder 类的 成员变量指针m_pMonsterBuilder, 同时对Construct 成员函数的代码进行调整(注意也增 加了返回值)。完整的MonsterDirector 类代码如下:

class MonsterDirector
{
public:
	MonsterDirector(MonsterBuilder* ptmpBuilder);
	void SetBuilder(MonsterBuilder* ptmpBuilder);
	Monster* Construct(std::string strmodelno);

private:
	MonsterBuilder* m_pMonsterBuilder = nullptr;
};

3.3.2 引入建造者模式

从前面的代码可以看到,建造者模式的实现代码相对比较复杂。
引入“建造者”模式的定义:将一个复杂对象的构建与它的表示分离,使得同样的构建 过程可以创建不同的表示。

在上述范例中,MonsterBuilder 类是对象的构建,而Monster 类是对象的表示,这两个 类是相互分离的。构建过程是指MonsterDirector 类中的 Construct 成员函数所代表的怪 物模型的载入和装配(挂接)过程,该过程稳定不会发生变化(稳定的算法),所以只要传递给 MonsterDirector 不同的构建器子类(M_UndeadBuilder 、M_ElementBuilder 、M_MechanicBuilder),就会构建出不同的怪物,可以随时调用 MonsterDirector 类 的 SetBuilder 成员函数为 MonsterDirector(指挥者)指定一个新的构建器以创建不同种类的怪物对象。

针对前面的范例绘制建造者模式的UML 图,如图3.8所示。

在图3 .8中,重点观看除抽象产品父类和具体产品类(Monster 、M_Undead 、M_Element、M_Mechanic) 之外的其他类。图3.8中的空心菱形在哪个类这边,就表示哪个类 中包含另外一个类的对象指针(这里表示 MonsterDirector 类中包含指向MonsterBuilder 类对象的指针m_pMonsterBuilder) 作为成员变量。

创建
创建
创建
MonsterDirector
-m_pMonsterBuilder:MonsterBuilder
+Construct()
MonsterBuilder
+LoadTrunkModel()
+LoadHeadModel()
+LoadLimbsModel()
+GetResult()
M_MechanicBuilder
+LoadTrunkModel()
+LoadHeadModel()
+LoadLimbsModel()
M_ElementBuilder
+LoadTrunkModel()
+LoadHeadModel()
+LoadLimbsModel()
M_UndeadBuilder
+LoadTrunkModel()
+LoadHeadModel()
+LoadLimbsModel()
M_Mechanic
M_Element
M_Undead
Monster

建造者模式的 UML 图包含4种角色。

  • (1)Builder (抽象构建器):为创建 一 个产品对象的各个部件指定抽象接口 (LoadTrunkModel、LoadHeadModel、LoadLimbsModel), 同时,也会指定一个接口(GetResult) 用于返回所创建的复杂对象。这里指MonsterBuilder类。

  • 2)ConcreteBuilder(具体构建器):实现了Builder接口以创建(构造和装配)该产品的 各个部件,定义并明确其所创建的复杂对象,有时也可以提供一个方法用于返回创建好的复 杂对象。这里指M_UndeadBuilder 、M_ElementBuilder 、M_MechanicBuilder 类。

  • (3)Product (产品):指的是被构建的复杂对象,其包含多个部件,由具体构建器创建该 产品的内部表示并定义它的装配过程。这里指M_Undead 、M_Element 、M_Mechanic 类 。

  • (4)Director (指挥者):又称导演类,这里指 MonsterDirector 类。该类有一个指向抽 象构建器的指针(m_pMonsterBuilder), 利用该指针可以在Construct 成员函数中调用构建 器对象中“构建和装配产品部件”的方法来完成复杂对象的构建,只要指定不同的具体构建 器,用相同的构建过程就会构建出不同的产品。同时,Construct 成员函数还控制复杂对象 的构建次序(例如,在 Construct 成 员 函 数 中 对 LoadTrunkModel、LoadHeadModel、

LoadLimbsModel 的调用是有先后次序的)。在客户端(指main 主函数中的调用代码)只需 要生成一个具体的构建器对象,并利用该构建器对象创建指挥者对象并调用指挥者类的 Construct 成员函数,就可以构建一个复杂的对象。

前面已经说过,从MonsterBuilder 分拆出MonsterDirector 这步不是必需的,不做分拆 可以看作建造者模式的一种退化情形,当然,此时客户端就需要直接针对构建器进行编码 了。一般的建议是:如果MonsterBuilder类本身非常庞大、非常复杂,则进行分拆,否则可 以不进行分拆,总之——复杂的东西就考虑做拆解,简单的东西就考虑做合并。

3.3.3 另一个建造者模式的范例

为了进一步加深读者对建造者模式的理解,再来讲述一个比较常见的应用建造者模式 的 范 例 。
某公司各部门的员工工作日报中包含标题、内容主体、结尾3部分。 ·标题部分包含部门名称、日报生成日期等信息。

  • 内容主体部分就是具体的描述数据(包括该项工作内容描述和完成该项工作花费的 时间),具体描述数据可能会有多条(该员工一天可能做了多项工作)。
  • 结尾部分包含日报所属员工姓名。
    现在要将工作日报导出成多种格式的文件,例如导出成纯文本格式、XML 格式、JSON 格式等,工作日报中内容主体部分的描述数据可能会有多条,导出到文件时每条数据占用 一 行 。
  1. 不用设计模式时程序应该如何书写
    针对上面的需求,看一看不采用设计模式时应该如何编写程序代码。可以把工作日报 中所包含的3部分内容分别定义3个类来实现,首先定义一个类来表达日报中的标题部分:

class DailyHeaderData
{
public:
	DailyHeaderData(std::string strDepName, std::string strGenDate);
	std::string getDepName();
	std::string getExportDate();

private:
	std::string m_strDepName;
	std::string m_strGenDate;
};

class DailyContentData {

public:
	DailyContentData(std::string strContent, double dspendTime);
	std::string getContent();
	double getSpendTime();

private:
	std::string m_strContent;
	double m_dspendTime; 

};

class DailyFooterData {
public:
	DailyFooterData(std::string strUserName);
	std::string getUserName();

private:
	std::string m_strUserName;

};


//将日报导出到纯文本格式文件 相关的类
class ExportToTxtFile
{
public:
	//实现导出动作
	void doExport(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj);
};


DailyHeaderData::DailyHeaderData(std::string strDepName, std::string strGenDate)
    :m_strDepName(strDepName),m_strGenDate(strGenDate)
{
}

std::string DailyHeaderData::getDepName()
{
    return m_strDepName;
}

std::string DailyHeaderData::getExportDate()
{
    return m_strGenDate;
}


DailyContentData::DailyContentData(std::string strContent, double dspendTime)
    :m_strContent(strContent), m_dspendTime(dspendTime)
{   
}

std::string DailyContentData::getContent() {
    return m_strContent;
}
double DailyContentData::getSpendTime() {
    return m_dspendTime;
}

DailyFooterData::DailyFooterData(std::string strUserName)
    :m_strUserName(strUserName)
{}


std::string DailyFooterData::getUserName() {
    return m_strUserName;
}

//实现导出动作

void ExportToTxtFile::doExport(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj) //记得#include头文件vector,因为日报的内容主体部分中的描述数据可能会有多条,所以用vector容器保存
{
	std::string strtmp = "";

	//(1)拼接标题
	strtmp += dailyheaderobj.getDepName() + "," + dailyheaderobj.getExportDate() + "\n";

	//(2)拼接内容主体,内容主体中的描述数据会有多条,因此需要迭代
	for (auto iter = vec_dailycontobj.begin(); iter != vec_dailycontobj.end(); ++iter)
	{
		std::stringstream oss; //记得#include头文件sstream
		oss << (*iter)->getSpendTime();
		strtmp += (*iter)->getContent() + ":(花费的时间:" + oss.str() + "小时)" + "\n";
	} //end for

	  //(3)拼接结尾
	strtmp += "报告人:" + dailyfooterobj.getUserName() + "\n";

	//(4)导出到真实文件的代码略,只展示在屏幕上文件的内容
	std::cout << strtmp;
}


在 main 主函数中加入代码,来展示一下导出到纯文本格式文件中的内容:

	DailyHeaderData* pdhd = new DailyHeaderData("研发一部", "11月1日");
	DailyContentData* pdcd1 = new DailyContentData("完成A项目的需求分析工作", 3.5);
	DailyContentData* pdcd2 = new DailyContentData("确定A项目开发所使用的工具", 4.5);
	
	std::vector<DailyContentData*> vec_dcd;
	vec_dcd.push_back(pdcd1);
	vec_dcd.push_back(pdcd2);

	DailyFooterData* pdfd = new DailyFooterData("小李");

	ExportToTxtFile file_ettxt;
	file_ettxt.doExport(*pdhd, vec_dcd, *pdfd);

	for (auto iter = vec_dcd.begin(); iter != vec_dcd.end(); ++iter) {
		delete (*iter);
	}

	delete pdfd;
	delete pdhd;

如 果 想 将 员 工 工 作 日 报 数 据 导 出 到 XML 格 式 的 文 件 中 , 可 以 编 写 另 一 个 类
ExportToXmlFile, 代码如下:


//将日报导出到XML格式文件 相关的类
class ExportToXmlFile
{
public:
	//实现导出动作
	void doExport(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj);
};
//实现导出动作

void ExportToXmlFile::doExport(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj) //记得#include头文件std::vector,因为日报的内容主体部分中的描述数据可能会有多条,所以用std::vector容器保存
{
	std::string strtmp = "";

	//(1)拼接标题
	strtmp += "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
	strtmp += "<DailyReport>\n";
	strtmp += "    <Header>\n";
	strtmp += "        <DepName>" + dailyheaderobj.getDepName() + "</DepName>\n";
	strtmp += "        <GenDate>" + dailyheaderobj.getExportDate() + "</GenDate>\n";
	strtmp += "    </Header>\n";

	//(2)拼接内容主体,内容主体中的描述数据会有多条,因此需要迭代
	strtmp += "    <Body>\n";
	for (auto iter = vec_dailycontobj.begin(); iter != vec_dailycontobj.end(); ++iter)
	{
		std::stringstream oss; //记得#include头文件sstream
		oss << (*iter)->getSpendTime();
		strtmp += "        <Content>" + (*iter)->getContent() + "</Content>\n";
		strtmp += "        <SpendTime>花费的时间:" + oss.str() + "小时" + "</SpendTime>\n";
	} //end for
	strtmp += "    </Body>\n";

	//(3)拼接结尾
	strtmp += "    <Footer>\n";
	strtmp += "        <UserName>报告人:" + dailyfooterobj.getUserName() + "</UserName>\n";
	strtmp += "    </Footer>\n";

	strtmp += "</DailyReport>\n";

	//(4)导出到真实文件的代码略,只展示在屏幕上文件的内容
	std::cout << strtmp;
}

从上述范例中可以看到,无论是将工作日报导出到纯文本格式文件中还是导出到XML 格式文件中,如下3个步骤始终是稳定不会发生变化的:

  • 拼接标题;
  • 拼接内容主体;
  • 拼接结尾。

虽然导出到的文件格式不同,上述3个步骤每一步的具体实现代码不同,但对于不同格 式的文件,这3个步骤是重复的,所以考虑把这3个步骤(复杂对象的构建过程)提炼(抽象) 出来,形成一个通用的处理过程,这样以后只要给这个处理过程传递不同的参数,就可以控 制该过程导出不同格式的文件。这也就是建造者模式的初衷——将构建不同格式数据的细节实现代码与具体的构建步骤分离,以达到复用构建步骤的目的。

  1. 采用设计模式时程序应该如何改写
    可以参考前面采用建造者设计模式的范例来书写本范例。先实现抽象构建器 FileBuilder 类(文件构建器父类),用于为上述3个步骤指定抽象接口,代码如下:
class FileBuilder{

public:
	virtual ~FileBuilder();

public:
	virtual void buildHeader(DailyHeaderData& dailyheaderobj) = 0;
	virtual void buildBody(std::vector<DailyContentData*>& vec_dailycontobj) = 0;
	virtual void buildFooter(DailyFooterData& dailyfooterobj) = 0; 
	
	std::string GetResult();

protected:
	std::string m_strResult;
};

FileBuilder::~FileBuilder()
{
}
std::string FileBuilder::GetResult()
{
	return m_strResult;
}

紧接着,构建两个FileBuilder 的子类——纯文本文件构建器类TxtFileBuilder 和 XML 文件构建器类XmlFileBuilder, 以实现FileBuilder 类中定义的接口。 TxtFileBuilder 中接口 的实现代码与前述 ExportToTxtFile 类 中 doExport 成员函数的实现代码非常类似, XmlFileBuilder 中接口的实现代码与前述 ExportToXmlFile 类 中doExport 成员函数的实 现代码非常类似。


class TextFileBuilder : public FileBuilder {
public:
	virtual ~TextFileBuilder();

	virtual void buildHeader(DailyHeaderData& dailyheaderobj) override;
	virtual void buildBody(std::vector<DailyContentData*>& vec_dailycontobj) override;
	virtual void buildFooter(DailyFooterData& dailyfooterobj) override;

};

class XmlFileBuilder : public FileBuilder {
public:
	virtual ~XmlFileBuilder();

	virtual void buildHeader(DailyHeaderData& dailyheaderobj) override;
	virtual void buildBody(std::vector<DailyContentData*>& vec_dailycontobj) override;
	virtual void buildFooter(DailyFooterData& dailyfooterobj) override;

};


TextFileBuilder::~TextFileBuilder()
{
}

void TextFileBuilder::buildHeader(DailyHeaderData& dailyheaderobj)
{
	m_strResult += dailyheaderobj.getDepName() + "," + dailyheaderobj.getExportDate() + "\n";
}

void TextFileBuilder::buildBody(std::vector<DailyContentData*>& vec_dailycontobj)
{
	for (auto iter = vec_dailycontobj.begin(); iter != vec_dailycontobj.end(); ++iter)
	{
		std::ostringstream oss;
		oss << (*iter)->getSpendTime();
		m_strResult += (*iter)->getContent() + ":(花费的时间:" + oss.str() + "小时)" + "\n";
	}
}

void TextFileBuilder::buildFooter(DailyFooterData& dailyfooterobj)
{
	m_strResult += "报告人:" + dailyfooterobj.getUserName() + "\n";
}

XmlFileBuilder ::~XmlFileBuilder()
{
}

void XmlFileBuilder::buildHeader(DailyHeaderData& dailyheaderobj)
{
	m_strResult += "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
	m_strResult += "<DailyReport>\n";

	m_strResult += "    <Header>\n";
	m_strResult += "        <DepName>" + dailyheaderobj.getDepName() + "</DepName>\n";
	m_strResult += "        <GenDate>" + dailyheaderobj.getExportDate() + "</GenDate>\n";
	m_strResult += "    </Header>\n";
}

void XmlFileBuilder::buildBody(std::vector<DailyContentData*>& vec_dailycontobj)
{
	m_strResult += "    <Body>\n";
	for (auto iter = vec_dailycontobj.begin(); iter != vec_dailycontobj.end(); ++iter)
	{
		std::ostringstream oss;
		oss << (*iter)->getSpendTime();
		m_strResult += "        <Content>" + (*iter)->getContent() + "</Content>\n";
		m_strResult += "        <SpendTime>花费的时间:" + oss.str() + "小时" + "</SpendTime>\n";
	} 
	m_strResult += "    </Body>\n";
}

void XmlFileBuilder::buildFooter(DailyFooterData& dailyfooterobj)
{
	m_strResult += "    <Footer>\n";
	m_strResult += "        <UserName>报告人:" + dailyfooterobj.getUserName() + "</UserName>\n";
	m_strResult += "    </Footer>\n";
	m_strResult += "</DailyReport>\n";
}


当然,如果愿意,也可以继续实现JSON 格式文件甚至是各种其他格式文件的导出,例 如创建一个JsonFileBuilder 类来实现JSON 格式文件的导出工作,相关代码可仿照上面的 代码自行扩展。
然后,实现一个文件指挥者类FileDirector, 代码如下:

class FileDirector
{
public:
	FileDirector(FileBuilder* ptmpBuilder);
	std::string Construct(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj);

private:
	FileBuilder* m_pFileBuilder;

};
FileDirector::FileDirector(FileBuilder* ptmpBuilder)
{
	m_pFileBuilder = ptmpBuilder;
}

std::string FileDirector::Construct(DailyHeaderData& dailyheaderobj, std::vector<DailyContentData*>& vec_dailycontobj, DailyFooterData& dailyfooterobj)
{
	m_pFileBuilder->buildHeader(dailyheaderobj);
	m_pFileBuilder->buildBody(vec_dailycontobj);
	m_pFileBuilder->buildFooter(dailyfooterobj);
	return m_pFileBuilder->GetResult();
}

请注意,在上个(创建怪物)范例中,复杂的对象或产品是指具体的怪物,这些具体的怪 物都继承自同一个父类(Monster 类),这不是必需的,即便是构建器子类创建彼此之间没什 么关联关系的产品也完全可以。
在这个范例中,所导出的纯文本文件内容或 XML 文件内容就被看作一个复杂的对象 或者说成是产品(当然,在这个范例中并没有为这些产品创建单独的类),构建步骤就是按照 拼接标题、拼接内容主体、拼接结尾的顺序进行,这个拼接步骤是稳定的。看一看本范例的 建造者模式UML 图,如图3.9所示。

创建
创建
FileDirector
- m_pFileBuilder: FileBuilder
+Construct()
FileBuilder
+GetResult()
+buildHeader()
+buildBody()
+buildFooter()
TxtFileBuilder
+buildHeader()
+buildBody()
+buildFooter()
XmlFileBuilder
+buildHeader()
+buildBody()
+buildFooter()
TextFileContent
+ String content
XmlFileContent
+ XMLDocument content

3.3.4 建造者模式的总结

通过上述两个范例,不难看到,建造者设计模式主要用于分步骤构建一个复杂的对象,其中构建步骤是一个稳定的算法(构建算法),而复杂对象各个部分的创建则会有不同的变化。 在如下情形时,可以考虑使用建造者模式:

  • 需要创建的产品对象内部结构复杂,产品往往由多个零部件组成。
  • 需要创建的产品对象内部属性相互依赖,需要指定创建次序。
  • 当创建复杂对象的步骤(过程)应该独立于该对象的组成部分(通过引入指挥者类, 将创建步骤封装在其中)。
  • 将复杂对象的创建和使用分离,使相同的创建过程可以创建不同的产品。

建造者模式的核心要点在于将构建算法和具体的构建相互分离,这样构建算法就可以 被重用,通过编写不同的代码又可以很方便地对构建实现进行功能扩展。引入指挥者类后, 只要使用不同的生成器,利用相同的构建过程就可以构建出不同的产品。

构建器接口定义的是如何构建各个部件,也就是说,当需要创建具体部件的时候,交给 构建器来做。而指挥者有两个作用:

  • 负责通过部件以指定的顺序来构建整个产品(控制了构建的过程)。

  • 指挥者通过提供Construct 接 口隔离了客户端(指main 主函数中的代码)与具体构 建过程必须要调用的类的成员函数之间的关联。

对于客户端,只需要知道各种具体的构建器以及指挥者的Construct 接口即可,并不需 要知道如何构建具体的产品。想象一个项目开发小组,如果main 中构建产品的代码由普 通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是 由组长来完成,显然这项工作要复杂得多。
模板方法模式与建造者模式有类似之处,但模板方法模式主要用来定义算法的骨架,把 算法中的某些步骤延迟到子类中去实现,模板方法模式采用继承的方式来体现。在建造者 模式中,构建算法由指挥者来定义,具体部件的构建和装配工作由构建器实现,也就是说,该 模式采用的是委托(指挥者委托给构建器)的方式来体现的。

工厂方法模式与建造者模式也有类似之处,但建造者模式侧重于一步步构建一个复杂 的产品对象,构建完成后返回所构建的产品,工厂方法模式侧重于多个产品对象(且对象所 属的类继承自同一个父类)的构建而无论产品本身是否复杂。

建造者模式具有如下优点:

  • 将一个复杂对象的创建过程封装起来。用同一个构建算法可以构建出表现上完全 不同的产品,实现产品构建和产品表现(表示)上的分离。 建造者模式也正是通过把 产品构建过程独立出来,从而才使构建算法可以被复用。这样的程序结构更容易扩 展和复用。
  • 向客户端隐藏了产品内部的表现。
  • 产品的实现可以随时被替换(将不同的构建器提供给指挥者)。 建造者模式具有如下缺点:
  • 要求所创建的产品有比较多的共同点,创建步骤(组成部分)要大致相同,如果产品 很不相同,创建步骤差异极大,则不适合使用建造者模式,这是该模式使用范围受限 的地方。
  • 建造者模式涉及很多的类,例如需要组合指挥者和构建器对象,然后才能开始对象 的构建工作,这对于理解和学习是有一定门槛的。

网站公告

今日签到

点亮在社区的每一天
去签到