大家好,我是十三!欢迎来到十三Tech。

接手过一个支付模块,调用方代码长这样:if 走支付宝就 new 一个 AlipayClient,if 走微信就 new 一个 WechatClient,if 走银联就 new 一个 UnionPayClient……每接一个新渠道,调用方代码就得改一遍,5 个渠道下来已经没法看了。

更麻烦的是单元测试——所有调用方都得 mock 三套不同的 client,写一个测试用例要准备三份 fake。这种代码不是"没用设计模式",而是把创建逻辑漏到了业务逻辑里

工厂方法(Factory Method)就是为这种场景准备的——但绝大多数文章只告诉你"定义一个接口创建对象",这是描述不是理由。这一篇聊聊工厂方法在 Go 工程里真正的价值,以及 database/sql 是怎么把它用成教科书的。

工厂方法的现实类比:调用方不关心具体类型

一、工厂方法在解决什么——不是"封装 new"

把教科书定义翻一下:定义一个创建对象的接口,让子类决定实例化哪个类。这话听起来很玄,但翻译成工程语言就一句话——让调用方不关心具体类型,只关心接口

很多人误把工厂方法当成"把 new 包装一层",结果写出 func NewAlipay() *Alipay { return &Alipay{} } 这种无意义包装。这不是工厂方法,这是语法糖。真正的工厂方法解决的是类型决策的归属问题——具体类型的选择应该由工厂负责,不应该由调用方负责。

回头看那个支付模块。调用方判断 if channel == "alipay" 本身就是问题——这个判断属于"创建逻辑",应该被工厂吃掉。调用方只需要:给我一个 Payment 接口,我调它 Pay() 就完了。

判断口诀:创建逻辑比 new 复杂(涉及类型选择/配置加载/依赖组装)→ 工厂;就是 &T{} 一行 → 不需要工厂

二、Go 工程里工厂方法最地道的写法

教科书工厂方法长这样:一个 Creator 抽象类,每个 ConcreteCreator 负责创建一种 Product。Java 写多了会原样搬到 Go,写出一堆 interface + struct 的样板代码。

type Payment interface { Pay(amount int) error }

type Alipay struct{ AppID string }
func (a *Alipay) Pay(amount int) error { /* ... */ return nil }

type Wechat struct{ MchID string }
func (w *Wechat) Pay(amount int) error { /* ... */ return nil }

这段是 Product 和具体实现,没问题。问题在创建逻辑放哪里。如果像 Java 那样写 AlipayFactory / WechatFactory 各一个 struct,Go 工程师会觉得别扭——为什么创建个对象要两层间接?

Go 社区更地道的写法是一个工厂函数 + 一个 map 注册表,把所有创建逻辑集中到一处:

工厂方法 UML 结构:Creator 返回 Product 接口

var factories = map[string]func(cfg Config) Payment{
    "alipay": func(c Config) Payment { return &Alipay{AppID: c.AppID} },
    "wechat": func(c Config) Payment { return &Wechat{MchID: c.MchID} },
}

func NewPayment(channel string, cfg Config) (Payment, error) {
    if f, ok := factories[channel]; ok { return f(cfg), nil }
    return nil, fmt.Errorf("unsupported channel: %s", channel)
}

factories 这个 map——它就是工厂方法的"创建逻辑集中地"。加新渠道只要在 map 里添一行,调用方零改动。对比那个 if-else 灾难现场:5 个渠道的 if 消失了,调用方变成 NewPayment(channel, cfg) 一行。

这个写法的关键不是"用 map 替代 if",而是让类型决策从调用方迁移到工厂。调用方只知道 Payment 接口,不感知具体类型的存在。这是工厂方法的工程价值。

工厂方法调用流程:客户端传 channel 给工厂,工厂返回接口

工厂模式其实有三个孪生兄弟容易混淆——简单工厂、工厂方法、抽象工厂。简单工厂就是上面这个 map 注册,工厂方法是"每个具体工厂一个类",抽象工厂是"创建一族相关对象"。Go 工程里 90% 的场景用简单工厂就够,工厂方法和抽象工厂的额外复杂度通常不值得。

工厂方法 vs 简单工厂 vs 抽象工厂的边界

三、什么时候别用——database/sql 教你怎么用对

工厂方法不是所有创建场景都适用,至少有三个反模式要知道。

反模式一:把 &T{} 包装成工厂。如果创建逻辑就是 &Order{ID: id},写一个 NewOrder(id) *Order 反而增加了间接层。工厂的价值是"封装复杂创建",简单场景硬上工厂是过度设计。

反模式二:工厂返回具体类型而不是接口NewAlipay() *Alipay 这种写法让调用方依然耦合到具体类型,工厂的意义全废。工厂应该返回 Payment 这种接口,让具体类型对调用方不可见。

反模式三:一个工厂创建太多不相关对象。如果工厂既能创建 Payment,又能创建 Logger,还能创建 Order——这不是工厂,这是上帝类。一个工厂专注于一种对象的创建。

工厂方法适用决策:创建逻辑复杂度 + 类型多样性

Go 标准库里工厂方法的教科书是 database/sql——sql.Register("mysql", &MySQLDriver{}) 把驱动注册到一个全局 map,调用方 sql.Open("mysql", dsn) 拿到的是统一的 *sql.DB,完全不知道底层是 MySQL 还是 PostgreSQL。这是工厂方法 + 接口隔离 + 开闭原则的完美组合,看懂它比记住工厂模式定义有用得多。

回头看那个支付模块——值得用工厂吗?值得,因为渠道确实在增加,每个渠道的初始化逻辑(密钥加载、签名计算、HTTP client 配置)都比较复杂。但如果只是 if status == "ok" 这种简单分支,老老实实 if-else 反而读起来更快。

工厂方法真正教会我的,不是"用接口代替 new",而是让调用方和具体实现解耦——调用方依赖接口,创建方依赖具体实现,工厂是两者之间的边界。这个边界一旦建立,加新类型就只改工厂,调用方零修改。

下一篇讲建造者模式,它解决的是"参数太多怎么创建对象"的问题。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI