当前位置: 首页 > news >正文

【设计模式】建造者模式

三、建造者模式

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头文件sstreamoss << (*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头文件sstreamoss << (*iter)->getSpendTime();strtmp += "        <Content>" + (*iter)->getContent() + "</Content>\n";strtmp += "        <SpendTime>花费的时间:" + oss.str() + "小时" + "</SpendTime>\n";} //end forstrtmp += "    </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 中构建产品的代码由普 通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是 由组长来完成,显然这项工作要复杂得多。
模板方法模式与建造者模式有类似之处,但模板方法模式主要用来定义算法的骨架,把 算法中的某些步骤延迟到子类中去实现,模板方法模式采用继承的方式来体现。在建造者 模式中,构建算法由指挥者来定义,具体部件的构建和装配工作由构建器实现,也就是说,该 模式采用的是委托(指挥者委托给构建器)的方式来体现的。

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

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

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

相关文章:

【设计模式】建造者模式

三、建造者模式 3.3 建造者模式 建造者(Builder) 模式也称构建器模式、构建者模式或生成器模式&#xff0c;同工厂模式或原型 模式一样&#xff0c;也是一种创建型模式。建造者模式比较复杂&#xff0c;不太常用&#xff0c;但这并不表示不需要了 解和掌握该模式。建造者模式…...

一场由 ES 分片 routing 引发的问题

一场由 ES 分片 routing 引发的问题 ES 结构 {"poroperties": {"joinType": {"type": "join","eager_global_ordinals": true,"relations": {"spu": "sku"}},"id":{"type&q…...

搭建Python量化开发环境:从零开始的完整指南

搭建Python量化开发环境&#xff1a;从零开始的完整指南 在量化投资领域&#xff0c;一个稳定且高效的开发环境是成功的关键。本文将引导你一步步搭建起自己的Python量化开发环境&#xff0c;确保你能够顺利开始编写和运行量化策略。 &#x1f680;量化软件开通 &#x1f68…...

JavaScript日期区间计算:精准解析年月日差异

一、应用场景与功能概述 在日常的制作项目或者是练习&#xff0c;我们经常需要计算两个日期之间的精确时间差。本文将通过一个JavaScript日期计算函数&#xff0c;详细解析如何实现精准的年/月/日差异计算&#xff0c;并探讨实际开发中的常见问题和解决方案。 二、核心功能解…...

大数据学习(71)-三范式构成

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…...

el-table 插槽踩过的坑 :slot-scope 和#default的区别

slot-scope和#default是Vue中用于定义插槽的两种不同语法&#xff0c;它们在Vue 2和Vue 3中有不同的应用场景和语法规则。‌ slot-scope 在‌Vue 2.x‌中&#xff0c;slot-scope是用于声明具名插槽并获取父组件传递过来的数据的主要方式。通过slot-scope可以定义一个变量scop…...

Linux一键安装node.js【脚本】

node.js一般不用系统的apt安装&#xff0c;而是用nvm这个前端的应用商店安装 node.js是js环境&#xff0c;nvm是安装nodejs管理器。npm是nodejs里的包管理器&#xff0c;安装模块的&#xff0c;类似于python的pip 把以下代码复制保存在一个文件里 比如nano install_nodejs.sh …...

vue3:pinia安装及其使用

​一、安装 Pinia 的步骤 ​1、安装 Pinia npm install pinia 2、​在 Vue 应用中引入 Pinia 在 main.js 中引入并注册 Pinia&#xff1a; import { createApp } from vue; import { createPinia } from pinia; import App from ./App.vue;const app createApp(App); app…...

vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性

native取消 在 Vue 3 中&#xff0c;v-on 的 .native 修饰符已经被移除。在 Vue 2 中&#xff0c;.native 修饰符用于在组件的根元素上监听原生 DOM 事件&#xff0c;但在 Vue 3 中&#xff0c;这一行为发生了变化。 在 Vue 3 中&#xff0c;所有未在子组件的 emits 选项中定…...

【漫话机器学习系列】146.Softmax 激活函数(Softmax Activation Function)

Softmax 激活函数详解 1. Softmax 函数概述 Softmax 函数&#xff08;Softmax Activation Function&#xff09;是一种常用于多分类任务的激活函数&#xff0c;广泛应用于机器学习和深度学习模型&#xff0c;特别是在神经网络的输出层。它的主要作用是将输入的多个实数值转换…...

解决:ModuleNotFoundError: No module named ‘_sqlite3‘

报错&#xff1a; from _sqlite3 import * ModuleNotFoundError: No module named _sqlite3安装sqlite3支持组件: sudo apt-get install libsqlite3-dev进入之前下载的python包下&#xff0c;重新编译和安装Python ./configure --enable-loadable-sqlite-extensions make &a…...

C++差分风暴:区间修改终极模板

目录 &#x1f525; 差分核心价值 &#x1f31f; 一维差分模板 1. 核心思想 2. 代码实现 3. 动态图示 &#x1f4e6; 二维差分模板 1. 核心公式 2. 代码实现 3. 二维修改示意图 &#x1f6a8; 六大避坑指南 &#x1f4a1; 复杂度对比 &#x1f308; LeetCode实战 &…...

easypoi导入Excel兼容日期和字符串格式的日期和时间

问题场景 在使用easypoi导入Excel时&#xff0c;涉及到的常用日期会有yyyy-MM-dd HH:mm:ss、yyyy-MM-dd和HH:mm:ss&#xff0c;但是Excel上面的格式可不止这些&#xff0c;用户总会输入一些其他格式&#xff0c;如 如果在定义verify时用下面这种格式定义&#xff0c;那么总会…...

《保险科技》

自己在保险行业工作很多年&#xff0c;只是接触了一些数据的内容&#xff0c;对于保险业务的知识了解的很少&#xff0c;想通过这本书补充一下&#xff0c;但是发现这本书就是一些知识的拼接。 先将保险的历史&#xff0c;后讲保险的定义&#xff0c;然后就是吹嘘保险行业和互联…...

QT编程之HTTP服务端与客户端技术

一、HTTP 服务器实现方案 ‌QtWebApp 集成‌ 将QtWebApp源码的 httpserver 目录导入项目&#xff0c;并在 .pro 文件中添加 include ($$PWD/httpserver/httpserver.pri)‌。配置 WebApp.ini 文件定义服务参数&#xff08;IP、端口、线程池等&#xff09;&#xff0c;通过 HttpL…...

每日一题--计算机网络

一、基础概念类问题 1. TCP 和 UDP 的区别是什么&#xff1f; 回答示例&#xff1a; TCP&#xff1a;面向连接、可靠传输&#xff08;通过三次握手建立连接&#xff0c;丢包重传&#xff09;、保证数据顺序&#xff08;如文件传输、网页访问&#xff09;。 UDP&#xff1a;无…...

IIS 服务器日志和性能监控

Internet Information Services &#xff08;IIS&#xff09; 是 Microsoft 提供的一款功能强大、灵活且可扩展的 Web 服务器&#xff0c;用于托管网站、服务和应用程序。IIS 支持 HTTP、HTTPS、FTP、SMTP 和更多用于提供网页的协议&#xff0c;因此广泛用于企业环境。 IIS 的…...

Unity学习之Shader总结(一)

一、Lesson1 1、渲染流水线 &#xff08;1&#xff09;应用阶段 模型->模型处理&#xff08;应用阶段&#xff09;–>输入结构 应用阶段主要操作&#xff1a;粗粒度剔除、进行渲染设置、准备基本数据、输出到几何阶段 &#xff08;2&#xff09;几何阶段 输入结构-&…...

java,poi,提取ppt文件中的文字内容

注意&#xff0c;不涉及图片处理。 先上pom依赖&#xff1a; <!-- 处理PPTX文件 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency><!--…...

matlab R2024b下载教程及安装教程(附安装包)

文章目录 前言一、matlab R2024b 安装包下载二、matlab R2024b安装教程 前言 为帮助大家顺利安装该版本软件&#xff0c;特准备matlab R2024b下载教程及安装教程&#xff0c;它将以简洁明了的步骤&#xff0c;指导你轻松完成安装&#xff0c;开启 MATLAB R2024 的强大功能之旅…...

设计模式之外观模式:原理、实现与应用

引言 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它通过提供一个统一的接口来简化复杂系统的使用。外观模式隐藏了系统的复杂性&#xff0c;使得客户端可以通过一个简单的接口与系统交互。本文将深入探讨外观模式的原理、实现方式以及实…...

移远QuecPython模组中的看门狗技术:如何提升设备可靠性

概述 对蜂窝通信模组而言&#xff0c;看门狗&#xff08;Watchdog&#xff09;是一种硬件或软件的监控机制&#xff0c;用于监测模组的运行状态。当模组因为外界干扰或程序错误陷入死循环时&#xff0c;看门狗会自动触发模组重启&#xff0c;从而恢复模组的运行状态。 对看门…...

汽车感性负载-智能高边钳位能量计算

随着汽车电子技术的发展&#xff0c;新的电子电气架构下&#xff0c;越来越多的执行部件在车身出现&#xff0c;比如电磁阀、风机、水泵、油泵、雨刮继电器等常用的执行器&#xff0c; 它们一般都表现为感性特点。驱动这些负载的最简单和最常见的方法是将它们连接到高边侧开关(…...

Kubernetes之ETCD

ETCD 是 Kubernetes 的核心组件之一&#xff0c;它是一个分布式键值对存储数据库&#xff0c;专为高可用性和一致性设计。它在 Kubernetes 中的主要作用是存储所有集群数据&#xff0c;包括配置数据和状态信息。ETCD 是 Kubernetes 的“大脑”&#xff0c;其稳定性对整个集群至…...

通过Geopandas进行地理空间数据可视化

目录 引言 安装与导入 数据加载与探索 数据预处理 基本地图可视化 添加其他数据到地图上 空间分析与查询 地图叠加与分组 空间缓冲区 交互式地图可视化 实际应用案例 城市规划 环境监测 结论 引言 在数据科学领域,地理空间数据可视化扮演着至关重要的角色。它不…...

堆(heap)

堆&#xff1f;对于初学者来说&#xff0c;或许是一个陌生十足的概念。 但&#xff01;堆不可怕。 什么是堆&#xff1f; 学术上&#xff0c;常常是这样说的&#xff08;一个完全二叉树&#xff09;。 没毛病&#xff0c;要想更好的理解堆(heap)&#xff0c;确实需要好好掌…...

Leetcode-回溯-组合型

22. 括号生成 - 力扣&#xff08;LeetCode&#xff09; 这题并没有才有恢复现场的做法 而是直接覆盖 题目核心是看穿本质 在代码执行过程中左括号必须大于等于右括号 以及回溯最底层的递归条件是递归长度已经达到2n 还有做括号要小于n 又因为i左右 因此右等于i-左 ope…...

Modbus通信协议基础知识总结

1. 数据类型与存储区分类 Modbus协议将数据分为四类存储区&#xff0c;通过存储区代号区分&#xff1a; 输出线圈&#xff08;0x&#xff09;&#xff1a;可读写&#xff0c;对应二进制开关量&#xff08;如继电器状态&#xff09;&#xff0c;地址范围000001-065536&#xff…...

python pip 最最开始新手教程/pip安装jupyter

pip 安装 直接安排了python即安装了pip,一般找pip的是安装了python之后用不了的&#xff0c;出现问题的&#xff0c;我也是因为这个所以写了这篇文章。 一、找不到pip,在C盘的cmd 输入pip -- version显示没有的是因为pip不能直接使用&#xff0c;需要额外配置环境变量 在系统…...

MATLAB 控制系统设计与仿真 - 28

MATLAB状态空间控制系统分析 - 极点配置 就受控系统的控制律的设计而言,由状态反馈极点配置和输出反馈极点配置。 状态反馈极点配置问题就是:通过状态反馈矩阵K的选取,使闭环系统的极点,即(A-BK)的特征值恰好处于所希望的一组给定闭环极点的位置。 另外,线性定常系统可…...

[leetcode] 面试经典 150 题——篇3:滑动窗口

[leetcode] 面试经典 150 题——篇3&#xff1a;滑动窗口 方法概述基本原理适用场景示例说明 1. [中等] 长度最小的子数组(leetcode 209题)题目描述解题思路python代码 2. [中等] 无重复字符的最长子串(leetcode 5题)题目描述解题思路python代码 方法概述 滑动窗口是一种常用的…...

华为云虚拟化技术

‌华为云底层的虚拟化技术‌是一种将物理资源&#xff08;如服务器、存储设备和网络&#xff09;抽象成虚拟资源的技术。通过这种技术&#xff0c;用户可以将物理资源划分为多个虚拟资源&#xff0c;从而提高资源利用率和灵活性‌1。 华为云底层虚拟化的定义和作用 华为云底层…...

在Pycharm配置conda虚拟环境的Python解释器

〇、前言 今天在配置python解释器时遇到了这样的问题 经过一下午自行摸索、上网搜寻后&#xff0c;终于找到的解决的方案&#xff0c;遂将该方法简要的记录下来&#xff0c;以备后用&#xff0c;并希望能帮助到有同样问题或需求的朋友:) 我所使用的软件的版本如下&#xff0c;假…...

【AI】在AWS AI芯片服务上部署运行Qwen 2.5模型

推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 准备选项 1:在 Amazon EC2 Inf2 上部署 TGI选项 2:在 SageMaker 上部署 TGI清理Qwen 2.5 多语言大型语言模型(LLMs) 是一系列预先…...

如何用日事清做研发目标、需求、规划、迭代、Bug、效能、复盘、绩效一站式管理

近年来&#xff0c;受监管环境趋严、盈利模式转型、市场竞争加剧等影响&#xff0c;互联网行业逐步进入深度变革与持续创新阶段。在此过程中&#xff0c;项目进度延期、需求频繁变更、开发流程混乱等痛点问题频发&#xff0c;导致部分互联网企业创新升级的步伐停滞不前。 企业…...

手搓智能音箱——语音识别及调用大模型回应

一、代码概述 此 Python 代码实现了一个语音交互系统&#xff0c;主要功能为监听唤醒词&#xff0c;在唤醒后接收用户语音问题&#xff0c;利用百度语音识别将语音转换为文本&#xff0c;再调用 DeepSeek API 获取智能回复&#xff0c;最后使用文本转语音功能将回复朗读出来。 …...

Docker安装mysql——Linux系统

拉取mysql镜像 docker pull mysql 查看镜像 docker images 运行镜像&#xff08;这一步的作用&#xff1a;数据持久化&#xff0c;通过挂载卷将日志、数据和配置文件存储在主机上&#xff0c;避免容器删除导致数据丢失&#xff09; docker run -p 3306:3306 --name mysql …...

Redis 面试思路

分布式redis面试思路俩点 高性能 高并发 高性能 1.存储在内存 所以速度快 2. 线程模型 io多路复用 监控多个客户端socket 放入队列里面 只是文件分发机制是单线程的 处理队列中的数据 根据不同类型 分发给不同处理器 后面处理的过程 也是多线程的 3. 内存回收机制 定期懒惰 …...

Oracle GoldenGate 全面解析

Oracle GoldenGate 全面解析 Oracle GoldenGate 是一种实时数据集成和复制解决方案,广泛应用于数据同步、数据库迁移、高可用性和灾难恢复等场景。以下将详细解答您提出的关于 Oracle GoldenGate 的一系列问题。 1. Oracle GoldenGate 的架构组成及其核心组件的作用 架构组成…...

【QT:控件】

目录 控件状态&#xff1a;​编辑 geometry : window frame windowlcon: qrc机制 qrc的使用方式&#xff1a; window opacity cursor font: ToolTip focusPolicy: styleSheet: 按钮类控件&#xff1a; PushButton: 给按钮添加图标&#xff1a; 给按钮添加快捷键…...

掌握xtquant:实时行情订阅与数据处理的实战指南

掌握xtquant&#xff1a;实时行情订阅与数据处理的实战指南 &#x1f680;量化软件开通 &#x1f680;量化实战教程 在量化交易领域&#xff0c;实时行情的获取和处理是构建有效交易策略的关键。本文将深入探讨如何使用xtquant库进行实时行情的订阅与数据处理&#xff0c;帮…...

解决 Nginx 访问 /root/下 403 Forbidden 问题

文章目录 解决 Nginx 访问 /root/test/1.html 403 Forbidden 问题问题复现Nginx 配置 可能的原因/root 目录权限问题SELinux 限制 解决方案方案 1&#xff1a;移动文件到 /data/或 /var/www/(推荐方案)方案 2&#xff1a;修改 /root 目录权限&#xff08;不推荐&#xff09;方案…...

免费实用工具,wps/office/永中通吃!

很多小伙伴在使用办公套件的过程中&#xff0c;往往会选择安装一些插件来提高工作效率。在众多的插件中&#xff0c;Excel和Word的插件非常常见&#xff0c;能够帮助用户进行数据分析、文档处理、格式美化等操作。然而&#xff0c;支持PPT&#xff08;PowerPoint&#xff09;的…...

Bash环境定制git分支提示符暨JDK版本切换脚本

一、在Bash提示符中显示Git分支 实现方法 编辑Bash配置文件 打开 ~/.bashrc 文件&#xff0c;添加以下代码&#xff1a; # 显示当前 Git 分支 parse_git_branch() {git branch 2>/dev/null | sed -e /^[^*]/d -e s/* \(.*\)/(\1)/ } export PS1\[\033[01;35m\]$PPX\[\033[0…...

使用fasterwhisper加速

背景 最近申请到一张8卡的gpu 机器用来验证音频转文本&#xff0c;发现whisper的处理速度很慢&#xff0c;于是有了下面的几种验证方法 配置信息 8张A30 第一次尝试 使用whisper对一个95M 的mp4文件进行解码 import time import whisperdef transcribe_audio(audio_path…...

IntelliJ IDEA 调试技巧指南

在日常开发中&#xff0c;调试是不可或缺的一部分。掌握调试工具的使用可以让我们更高效地定位和解决问题。本文将介绍一些在 IntelliJ IDEA 中常用的调试技巧&#xff0c;希望能帮助你在开发过程中更顺畅地解决问题。 1. 方法断点&#xff1a;快速定位实现类 方法断点可以帮…...

蓝桥杯Python赛道备赛——Day5:算术(一)(数学问题)

笔者计划用两期博客对蓝桥杯中所涉及的算术&#xff08;数学问题&#xff09;进行解释&#xff0c;本期博客包括&#xff1a;GCD&#xff08;最大公约数&#xff09;、LCM&#xff08;最小公倍数&#xff09;、质数判断、埃氏筛法、线性筛法&#xff08;欧拉筛&#xff09;和质…...

Android的消息机制

Android的消息机制-从入门到精通 前言Android消息机制概述Android 的消息机制分析ThreadLocal 的工作原理消息队列的工作原理Looper的工作原理Handler的工作原理 主线程的消息循环 前言 作为开发者&#xff0c;提及Android的消息机制&#xff0c;必然绕不开Handler&#xff0c;…...

Three.js 阴影 (Shadow) 知识点整理

阴影主要由 castShadow 和 receiveShadow 控制&#xff0c;并通过不同类型的光源 (DirectionalLight、SpotLight、PointLight) 生成。我们将系统地整理与阴影相关的知识点。 1️⃣ 基础概念 castShadow &#x1f3ad;&#xff1a;物体是否投射阴影。receiveShadow &#x1f3d…...

从C语言开始的C++编程生活(1)

前言 本系列文章承接C语言的学习&#xff0c;需要有C语言的基础才能学会哦。 第1篇主要讲的是有关于C的命名空间、输入和输出。 C才起步&#xff0c;都很简单呢&#xff01; 目录 前言 命名空间namespace 基本语法 作用 使用命名空间 域作用限定符 :: 基本语法 using n…...