⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️设计模式系列专栏设计模式系列系列上期内容【设计模式系列 (三) 】单例模式系列下期内容暂无目录1. 概述2. 结构3. 实现一带指挥者的经典版人种3.1 产品3.2 抽象建造者3.3 具体建造者3.4 指挥者3.5 客户端4. 实现二链式 Builder英雄4.1 产品与建造者4.2 客户端5. 两种实现怎么选6. 优缺点与适用环境7. 面试专题7.1 开场题说说建造者模式吧7.2 建造者 vs 工厂常问7.3 高频追问清单7.4 什么时候不该用建造者结语建造者模式Builder Pattern要解决的是一个非常具体的坏味道构造器参数失控。本文代码取自design-pattern-cpp 仓库的design-pattern/builder/使用 C11。为阅读方便片段省略了头文件保护宏。1. 概述WikipediaThe builder pattern is an object creation software design pattern with the intentions of finding a solution to thetelescoping constructor anti-pattern.建造者模式是一种对象创建软件设计模式旨在找到伸缩构造器反模式的解决方案。伸缩性构造的反模式指通过构造器实现对象构建参数初始化如果对象属性比较多导致构造器的参数个数不可控GoFSeparate the construction of a complex object from its representation so that the same construction process can create different representations.将一个复杂对象的构建与它的表示分离使得同样的构建过程可以创建不同的表示。两个关键词① 伸缩构造器反模式telescoping constructor anti-pattern一个对象属性一多构造函数就变成这样Hero(std::string name, HairType* hairType, HairColor* hairColor, Armor armor, Profession* profession, Weapon* weapon);想只传前三个只能再补一堆重载Hero(std::string name); Hero(std::string name, HairType* hairType); Hero(std::string name, HairType* hairType, HairColor* hairColor); // ... 组合爆炸调用处则是new Hero(张飞, nullptr, color, Armor(), nullptr, nullptr)这种靠数位置才能看懂的代码。参数一多谁是谁全靠猜。② 同构不同果将一个复杂对象的构建与它的表示分离使得同样的构建过程可以创建不同的表示。2. 结构建造者模式把造对象拆成四步分工共四个角色角色职责Builder抽象建造者声明两类方法一类是buildPartXxx()负责逐个构造部件另一类是build()负责返回成品。既可以是抽象类也可以是接口ConcreteBuilder具体建造者实现各部件具体的构造与装配方法明确自己要造的是哪个产品Product产品被构建的复杂对象包含多个部件Director指挥者/导演负责安排部件的建造次序与抽象建造者关联在construct()中调用建造者的装配方法。客户端通常只与指挥者交互3. 实现一带指挥者的经典版人种3.1 产品class Human { // 人种类型名称 CC_SYNTHESIZE(std::string, type, Type); // 人种分布区域 CC_SYNTHESIZE(std::string, region, Region); };CC_SYNTHESIZE是仓库的宏展开后是受保护的成员 公开的 getter/setter后面几个类都在用它等价于手写getType()/setType()。3.2 抽象建造者class HumanBuilder { // 关联一个人种实例 CC_SYNTHESIZE(Human, m_human, Human); public: virtual HumanBuilder* buildType() 0; virtual HumanBuilder* buildRegion() 0; // 将成员变量实例创建出一个新的实例对象 virtual Human* build() final; }; ​ // cpp builder::Human* builder::HumanBuilder::build() { // 通过拷贝创建新的实例 return new Human(m_human); }三个细节值得注意buildType()/buildRegion()返回HumanBuilder*而不是void—— 每个建造步骤都return this于是可以链式调用。装配顺序由调用方Director决定。build()声明为final—— 产品怎么生成是基类定死的子类只负责填什么部件不能改产品怎么产出。build()是拷贝出一份新产品—— 建造者内部的m_human是草稿每次build()都产出一个独立的成品建造者本身可以复用。3.3 具体建造者class YellowManBuilder : public HumanBuilder { DECLARE_CLASS(builder::YellowManBuilder); public: HumanBuilder* buildType() override; HumanBuilder* buildRegion() override; }; ​ // cpp IMPLEMENT_CLASS(builder::YellowManBuilder); ​ builder::HumanBuilder* builder::YellowManBuilder::buildType() { m_human.setType(黄种人); return this; // ← 链式的关键 } ​ builder::HumanBuilder* builder::YellowManBuilder::buildRegion() { m_human.setRegion(华夏); return this; }DECLARE_CLASS/IMPLEMENT_CLASS是仓库的自注册反射宏让这个类可以在运行时按类名字符串被创建。这里用它是为了配合下面的配置化客户端。另外两个具体建造者只有字面量不同WhiteManBuilder造白种人 / 金星BlackManBuilder造黑种人 / 火星。3.4 指挥者class HumanDirector { CC_SYNTHESIZE_SET(HumanBuilder*, m_builder, Builder); public: Human* construct() { return m_builder-buildType()-buildRegion()-build(); } };装配顺序先 type 后 region被固化在这里。换一个具体建造者同一套顺序就能造出不同的产品——这正是同样的构建过程创建不同的表示。3.5 客户端int main() { CREATE_PROPERTIES(prop, conf); // 读取配置 string bcname prop.getProperty(bp); // bpbuilder::YellowManBuilder GET_INSTANCE_BY_NAME(HumanBuilder*, builder, bcname); // 反射创建建造者 HumanDirector director; director.setBuilder(builder); Human* man director.construct(); std::cout man-getType() : man-getRegion() std::endl; delete builder; delete man; return 0; }客户端里没有出现任何一个具体建造者的类名全部来自config/conf.propertiesbpbuilder::YellowManBuilder改一行配置就能换成白种人或黑种人代码零改动。4. 实现二链式 Builder英雄经典版有 Director 显得重。如果只是想让构造成员变量这件事可读可以省掉 Director用链式 Builder——也就是现代语言里最常见的写法。4.1 产品与建造者class Hero { CC_SYNTHESIZE_GET(std::string, name, Name); CC_SYNTHESIZE_GET(HairType*, type, Type); CC_SYNTHESIZE_GET(HairColor*, color, Color); public: class Builder { friend class Hero; // 只有 Hero 能访问 Builder 的私有成员 private: std::string name; HairType* type; HairColor* color; public: Builder(); Hero* build(); Builder* withName(std::string name); Builder* withHairType(HairType* type); Builder* withHairColor(HairColor* color); }; private: Hero(Builder* builder); // ← 构造私有外部只能通过 Builder 造 public: ~Hero(); void show(); static Hero::Builder build(); // ← 链式入口 }; builder::Hero::Builder* builder::Hero::Builder::withName(std::string name) { this-name name; return this; } ​ builder::Hero* builder::Hero::Builder::build() { return new Hero(this); // 一次性把攒好的状态交给产品 } ​ builder::Hero::Hero(Builder* builder) { this-name builder-name; this-type builder-type; this-color builder-color; } ​ builder::Hero::~Hero() { if (type) delete type; // 所有权随指针转移给了产品 if (color) delete color; } ​ builder::Hero::Builder builder::Hero::build() { return Hero::Builder(); }三个设计要点Builder是Hero的静态内部类Hero的构造函数私有两者用friend打通。结果是除了 Builder没人能造出 Hero——产品构造的合法性被类型系统兜住。状态先攒在 Builder 身上build()时一次交付。中间任何时刻对象都不是半成品不存在构造到一半被别人看到的可能。withXxx()返回Builder*this天然支持链式name是值、type/color是指针指针的所有权在build()时转移给 Hero由~Hero()负责释放。4.2 客户端int main() { Hero* h1 Hero::build() .withName(张飞) -withHairType(new HairType(双马尾)) -withHairColor(new HairColor(金黄色)) -build(); h1-show(); ​ Hero::Builder b; Hero* h2 b.withName(赵云)-build(); h2-show(); ​ delete h1; delete h2; return 0; }调用处的可读性就是建造者模式最大的收益方法名即字段名参数位置不再需要记忆。⚠️ 一个小陷阱Hero::build()返回的是Builder 临时对象withName()返回指向它的指针。整条链写在一个表达式里是安全的临时对象活到表达式结束但一旦拆成多行赋值就会悬空。要稳就把Hero::Builder b;显式声明成变量再链上面h2的写法。5. 两种实现怎么选带 Director人种链式 Builder英雄装配顺序由谁定Director 固化客户端不关心客户端按需自由组合产品构造权限建造者内部持有草稿build()拷贝产出产品构造私有只有 Builder 能造是否必须按固定流程是同一构建过程造不同表示否想给几个字段就给几个字段典型场景参数化的流水线生产产品族差异大参数很多的单个对象字段可选主要收益隔离构建过程扩展新产品不改客户端消灭伸缩构造器调用处可读一句话要同一套流程造不同的东西就用 Director 版只是参数太多写不下了就用链式版。6. 优缺点与适用环境主要优点客户端不必知道产品内部组成细节产品本身与创建过程解耦同样的创建过程能造出不同产品各具体建造者彼此独立替换或新增建造者很方便。指挥者面向抽象建造者编程符合开闭原则可以把复杂产品的创建步骤分解到不同方法中更精细地控制创建过程。主要缺点产品需要有较多共同点、组成部件相似。如果产品之间差异极大不适合使用适用范围受限如果产品内部变化复杂会导致需要定义大量具体建造者类系统变得庞大增加理解与运行成本。适用环境需要生成的产品对象有复杂的内部结构通常包含多个成员属性需要生成的产品对象的属性之间相互依赖需要指定生成顺序对象的创建过程独立于创建该对象的类——引入指挥者后创建过程被封装在指挥者中而不在建造者或客户端里需要隔离复杂对象的创建和使用并使得同样的创建过程可以创建不同的产品。7. 面试专题7.1 开场题说说建造者模式吧考察点是概念、角色、应用场景概念建造者模式将一个复杂对象的构建与它的表示分离使得同样的构建过程可以创建不同的表示。它主要用于解决构造器参数失控的伸缩构造器反模式。角色抽象建造者 Builder 声明部件的构造方法与build()具体建造者 ConcreteBuilder 实现各部件的具体装配产品 Product 是被构建的复杂对象指挥者 Director 负责安排建造次序。使用场景需要生成的对象内部结构复杂、包含多个成员属性时对象的属性之间相互依赖、有生成顺序要求时。可扩展把与工厂模式的区别、优缺点、项目中的用法接上见下。7.2 建造者 vs 工厂常问建造者模式工厂模式关注点装配顺序——一步步把部件组装起来创建哪个对象——把实例化过程封装起来返回时机所有部件都装完才返回成品一次调用直接返回对象产品复杂度复杂对象多部件、多属性相对简单通常是单个产品或一个产品族客户端可以参与指定装配过程链式版只给参数或选工厂不关心过程典型问法怎么造造哪个一句话版本建造者关注装配顺序工厂模式关注如何使用对象。7.3 高频追问清单追问答法buildXxx()为什么返回this实现链式调用也让 Director 能把多个装配步骤串成一个表达式build()为什么声明成final产品如何产出是基类的契约子类只该决定填什么部件不该改怎么产出成品为什么要把产品构造函数私有化强制所有创建都走 Builder杜绝参数不全或顺序错误的半成品对象Builder 和产品之间为什么要friend产品构造私有后只有 Builder 能访问它的私有构造用友元打通比开放 public 构造更安全Director 到底有什么用把装配顺序这层知识从客户端移走。换建造者不改顺序改顺序不改建造者两边都符合开闭原则建造者能不能省掉 Director能。链式 Builder 就是省掉 Director 的形态代价是装配顺序交回客户端指针成员的所有权怎么管理仓库里是Builder 只暂存指针build()时把所有权交给产品由产品的析构函数统一delete。更稳的做法是换成std::unique_ptr建造者模式有什么缺点产品差异大时不适用具体建造者类数量容易膨胀比直接new多了不少类有过度设计的风险和可选参数默认值有什么区别默认值解决参数省略解决不了参数顺序和构建过程两个问题建造者两者都能解决7.4 什么时候不该用建造者判断标准还是那一条这个对象真的有那么多参数吗成员只有两三个 → 老老实实写构造函数建造者是纯粹的成本属性之间没有先后依赖、也不需要校验 → 用聚合初始化或默认参数就够了产品之间差异极大、几乎没有共同部件 → 建造者模式会让具体建造者类爆炸得不偿失。建造者的价值在于隔离复杂对象的装配过程对象不复杂的时候隔离它就是过度设计。结语我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见