Golang建造者模式与工厂模式如何选择_模式对比说明

建造者模式适用于多可选参数、需校验与分步配置的场景,如HTTPClient;工厂模式仅适合参数固定、变体极少的情况;二者组合时应通过私有字段、链式方法和Build()校验实现可控扩展。

工厂模式管“造什么”,建造者模式管“怎么造”——选错就容易写一堆冗余代码或失去配置灵活性。

什么时候该用 NewHTTPClientBuilder() 而不是 NewHTTPClient()

当对象初始化需要多个可选参数、依赖注入、校验逻辑或分步设置时,直接构造函数会迅速失控。比如 HTTPClient 至少涉及 TimeoutRetryMiddlewaresTransport 等 5+ 配置项,且不同业务场景组合差异大。

  • 用工厂函数(如 NewHTTPClient())只适合参数固定、变体极少的场景(例如只有一种默认客户端)
  • 用建造者(如 NewHTTPClientBuilder().WithTimeout(30 * time.Second).WithRetry(3).Build())才能支持链式、可读、可复用的配置流
  • 若还需区分“测试用客户端”“生产用客户端”“Mock 客户端”,那就该让工厂返回建造者:NewClientBuilder("prod") → 返回带默认超时/重试/日志中间件的 *HTTPClientBuilder

为什么不能只用工厂模式支持多类型 + 多配置?

工厂模式本身不处理“配置过程”,它只负责“分发”。一旦你试图在工厂里塞进所有配置逻辑(比如 NewHTTPClient(timeout, retry, middleware...)),就会立刻掉进两个坑:

  • 参数爆炸:新增一个 MaxIdleConns 就得改函数签名,所有调用点都要动
  • 必填/可选边界模糊:哪些字段必须设?哪些有默认值?工厂函数无法表达这种约束
  • 类型安全丢失:用 map[string]interface{} 或结构体传参,编译期无法检查字段合法性

而建造者把校验和默认值封装在 Build() 里,比如未设 Timeout 时返回 error,或自动设为 30s,这才是可控的扩展方式。

工厂 + 建造者组合的最小可行结构长什么样?

核心就三样:统一接口、具体建造者、工厂函数。不需要 Director,也不需要抽象 Builder 接口——Go 里靠值语义和函数链式调用更轻量。

type LoggerBuilder interface {
    WithLevel(level string) LoggerBuilder
    WithFormat(format string) LoggerBuilder
    Build() (Logger, error)
}

type fileLoggerBuilder struct { level string format string path string }

func (b *fileLoggerBuilder) WithLevel(level string) LoggerBuilder { b.level = level return b } // ... 其他方法

func NewLoggerBuilder(kind string) LoggerBuilder { switch kind { case "file": return &fileLoggerBuilder{level: "info"} // 默认值在此 case "console": return &consoleLoggerBuilder{level: "debug"} default: panic("unknown logger kind") } }

  • 工厂函数名要体现意图,比如 NewLoggerBuilder()NewBuilder() 更明确
  • 建造者内部字段应私有,避免外部直接修改;Build() 是唯一出口,也是校验入口
  • 不要返回指针给建造者(如 *fileLoggerBuilder),除非你确认并发安全;值接收器 + 每次返回新实例更稳妥

最容易被忽略的是:建造者不是为“炫技”而存在,而是为“防止配置遗漏”和“降低调用方认知负担”。如果一个对象创建后 90% 的调用都用同一组参数,那它大概率不该用建造者;但如果每次初始化都要查文档确认字段含义,那就该立刻重构出建造者——哪怕只封装三个字段。