建造者模式相比命名可选参数是否有额外价值?
不同资料对建造者模式的定位差异
不同技术资料对建造者模式的权重处理存在明显区别:
- 《Head First Design Patterns》仅在附录部分简要提及该模式,没有像其他核心设计模式一样做单独的展开讲解
- 经典GoF教材《Design Patterns: Elements of Reusable Object-Oriented Software》将建造者模式与其他创建型模式放在同等地位,做了完整的定义和场景说明
- Refactoring Guru同样将其归类为正式的创建型设计模式,没有归入补充类的附录内容
建造者模式的普遍共识作用
所有公开资料对建造者模式的基础价值认知是统一的:它支持客户端按需选择配置项构造对象,解决传统多参构造函数调用时必须传入大量无意义空值的问题。
比如下面这种参数列表很长的构造函数就是典型的反模式:
Ctor::Ctor(Param1* param1, Param2* param2, Param3* param3 /*, and many more */);
如果客户端不需要设置前两个参数,调用时就必须传入一堆空值占位,不仅可读性差,还很容易把参数位置传错:
auto obj = Ctor(null, null, some_param3 /*, null, null */);
用最基础的链式建造者实现,就可以只设置需要的参数,最后统一完成对象构造:
auto obj = Builder().setParam3(some_param3).build();
核心问题:命名可选参数能否完全替代建造者模式?
不少开发者都会有这个疑问:如果建造者模式只能解决上述多参传空的问题,那Python、C#、Kotlin等语言内置的命名可选参数,不就完全可以实现一样的效果?甚至部分语言靠自身特性根本不需要引入建造者模式:
- Clojure的解构能力可以完美支持按需向构造函数传参,不需要额外封装建造者逻辑
- 有曾任职Facebook的研究工程师提出,Haskell的类型类与智能构造器组合也可以替代建造者模式,这部分观点因理解成本较高,本次暂不展开讨论
直接给结论:建造者模式能提供的能力远不止命名可选参数覆盖的范围,这也是它在天生支持命名参数的语言中依然被广泛使用的核心原因,具体的额外能力包括这几点:
- 支持分阶段累积构造参数。很多场景下对象的构造参数不是一次性拿到的:可能先加载默认配置,再读取配置文件覆盖,再叠加用户传入的自定义参数,整个参数收集过程跨多个函数、甚至多个模块。这种场景下你不可能一开始就攒齐所有参数去调用构造函数,用建造者可以把builder实例在各个流程节点间传递,每个节点只负责填充自己管辖的参数,所有参数收集完成后统一build出成品。命名可选参数本质是一次性传参的语法糖,遇到这种场景你必须自己额外写一个暂存参数的结构,本质就是手写了一个不完整的builder。
- 支持根据参数组合返回不同的实现实例。同一个builder可以根据传入参数的差异,build出同一接口下的不同实现类:比如构造缓存实例时,传入本地磁盘路径就返回本地文件缓存实现,传入Redis连接参数就返回分布式缓存实现,客户端全程只和builder交互,不需要感知背后具体的实现类。命名可选参数是和具体类的构造函数强绑定的,要实现这种多实现的分发逻辑,你就得额外写工厂分支,远没有builder的封装性好。
- 支持更严格的编译期调用约束。很多场景下参数之间存在依赖关系:比如必须先设置文件存储路径,才能设置文件分片大小;必须先设置数据库连接地址,才能配置连接池参数。通过步骤式的builder设计,可以在编译期就强制参数的传入顺序,没填完必填参数的话根本调用不到build方法,从根源上避免非法构造。命名可选参数最多只能在构造函数运行时做校验,发现问题再抛异常,做不到编译阶段就拦截错误调用。
- 封装复杂对象的构造细节。如果要构造的对象内部结构非常复杂——比如拼接多条件的SQL语句、生成多层嵌套的配置文档、组装包含十多个内部依赖的服务实例,builder可以把所有内部组件的组装逻辑完全封装起来,客户端只需要调用语义清晰的高层方法传参即可,不需要了解对象内部的组件依赖关系,最后build出的还是完全不可变的成品对象,构造过程的临时状态不会外泄,这种封装度是直接调用构造函数+命名参数很难达到的。
当然也没必要神话建造者模式:如果只是构造只有两三个可选参数的简单对象,构造逻辑没有分支、没有复杂依赖,那直接用语言自带的命名可选参数就足够,硬套建造者模式平白增加代码量完全是多余的,设计模式从来不是必须遵守的教条,匹配实际场景才有价值。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

