Go工厂模式:简单工厂与抽象工厂

Go工厂模式:简单工厂与抽象工厂 Go工厂模式:简单工厂与抽象工厂摘要: 本篇讲解Go语言工厂模式用map注册和switch实现简单工厂定义工厂方法接口解耦创建逻辑抽象工厂创建系列对象结合依赖注入管理对象生命周期分享map并发读写导致工厂panic的踩坑经验。开篇故事去年做支付网关支持微信、支付宝、银联三种渠道。第一版用switch-case根据渠道名创建对应的支付客户端代码写了三个分支。后来新增京东支付改了工厂函数测试时发现回归测试没覆盖到上线后京东支付调用流程里有个参数没传对线上失败了。更麻烦的是新增渠道要改工厂函数的代码违反开闭原则。每次加渠道都要改switch一不小心改错分支影响已有渠道。后来重构成map注册的简单工厂新渠道只需要实现接口并注册工厂代码完全不改。但map注册有个坑启动时注册和运行时查找并发了偶尔panic。这篇把几种工厂模式讲清楚顺便说说怎么避开这个坑。一、简单工厂:map注册加switch简单工厂的核心思路是: 定义一个接口不同实现注册到map里运行时按类型名查找对应的构造函数。packagepaymentimporterrors// PaymentChannel 支付渠道接口// 所有支付渠道实现这个接口typePaymentChannelinterface{// Pay 发起支付Pay(amountint,orderIDstring)(string,error)// Query 查询支付状态Query(orderIDstring)(string,error)// Name 渠道名称Name()string}// Constructor 构造函数类型// 每个渠道注册一个构造函数typeConstructorfunc()PaymentChannel// factory 简单工厂typefactorystruct{// 用map保存类型名到构造函数的映射creatorsmap[string]Constructor}// NewFactory 创建工厂实例funcNewFactory()*factory{returnfactory{creators:make(map[string]Constructor),}}// Register 注册渠道构造函数// type是渠道标识如wechat、alipayfunc(f*factory)Register(typstring,c Constructor){f.creators[typ]c}// Create 根据类型创建支付渠道实例func(f*factory)Create(typstring)(PaymentChannel,error){constructor,ok:f.creators[typ]if!ok{returnnil,errors.New(unsupported payment type: typ)}returnconstructor(),nil}// --- 微信支付实现 ---// WeChatPay 微信支付渠道导出类型供外部包使用typeWeChatPaystruct{}func(w*WeChatPay)Pay(amountint,orderIDstring)(string,error){returnwechat_pay_orderID,nil}func(w*WeChatPay)Query(orderIDstring)(string,error){returnpaid,nil}func(w*WeChatPay)Name()string{returnwechat}// --- 支付宝支付实现 ---// Alipay 支付宝支付渠道导出类型供外部包使用typeAlipaystruct{}func(a*Alipay)Pay(amountint,orderIDstring)(string,error){returnalipay_orderID,nil}func(a*Alipay)Query(orderIDstring)(string,error){returnpaid,nil}func(a*Alipay)Name()string{returnalipay}使用时先注册所有渠道再按需创建。packagemainimportpaymentfuncmain(){f:payment.NewFactory()// 启动时注册所有渠道f.Register(wechat,func()payment.PaymentChannel{returnpayment.WeChatPay{}})f.Register(alipay,func()payment.PaymentChannel{returnpayment.Alipay{}})// 运行时按类型创建channel,err:f.Create(wechat)iferr!nil{panic(err)}channel.Pay(100,order_123)}map注册的好处是新增渠道不用改工厂代码。实现接口写个构造函数调用Register注册工厂的Create逻辑完全不变。二、工厂方法接口简单工厂把所有创建逻辑集中在一个地方。工厂方法模式把创建逻辑分散到子工厂每个产品类型有自己的工厂。packagestorageimporterrors// Storage 存储接口typeStorageinterface{Save(keystring,data[]byte)errorLoad(keystring)([]byte,error)Delete(keystring)error}// StorageFactory 工厂方法接口// 每种存储类型实现自己的工厂typeStorageFactoryinterface{// Create 创建存储实例Create(configmap[string]string)(Storage,error)// Type 工厂支持的存储类型Type()string}// --- 本地文件存储 ---typefileStoragestruct{dirstring}func(f*fileStorage)Save(keystring,data[]byte)error{returnnil}func(f*fileStorage)Load(keystring)([]byte,error){returnnil,nil}func(f*fileStorage)Delete(keystring)error{returnnil}typefileStorageFactorystruct{}func(f*fileStorageFactory)Create(configmap[string]string)(Storage,error){dir,ok:config[dir]if!ok{returnnil,errors.New(file storage需要dir配置)}returnfileStorage{dir:dir},nil}func(f*fileStorageFactory)Type()string{returnfile}// --- S3存储 ---types3Storagestruct{bucketstringregionstring}func(s*s3Storage)Save(keystring,data[]byte)error{returnnil}func(s*s3Storage)Load(keystring)([]byte,error){returnnil,nil}func(s*s3Storage)Delete(keystring)error{returnnil}types3StorageFactorystruct{}func(f*s3StorageFactory)Create(configmap[string]string)(Storage,error){bucket:config[bucket]region:config[region]ifbucket{returnnil,errors.New(s3 storage需要bucket配置)}returns3Storage{bucket:bucket,region:region},nil}func(f*s3StorageFactory)Type()string{returns3}// --- 工厂注册中心 ---typefactoryRegistrystruct{factoriesmap[string]StorageFactory}funcNewFactoryRegistry()*factoryRegistry{returnfactoryRegistry{factories:make(map[string]StorageFactory)}}func(r*factoryRegistry)Register(f StorageFactory){r.factories[f.Type()]f}func(r*factoryRegistry)Create(typstring,configmap[string]string)(Storage,error){f,ok:r.factories[typ]if!ok{returnnil,errors.New(unsupported storage type: typ)}returnf.Create(config)}工厂方法和简单工厂的区别在于: 简单工厂的工厂类知道所有产品的创建逻辑工厂方法把创建逻辑下放到具体工厂类。新增产品时工厂方法模式只需要加一个工厂类并注册注册中心的代码不变。Go没有继承工厂方法的子工厂用接口实现代替。三、踩坑经验:map并发读写导致工厂panic这个坑我踩过。支付工厂在启动时注册渠道运行时查找渠道。某次加了热加载功能运行时动态注册新渠道同时业务请求在查找渠道。偶尔panic: concurrent map read and map write。Go的map不是并发安全的。同时读写map会触发runtime fatal error整个goroutine直接panicrecover都接不住。packagepaymentimport(errorssync)// SafeFactory 并发安全的工厂// 用sync.RWMutex保护maptypeSafeFactorystruct{mu sync.RWMutex creatorsmap[string]Constructor}funcNewSafeFactory()*SafeFactory{returnSafeFactory{creators:make(map[string]Constructor),}}// Register 注册渠道加写锁func(f*SafeFactory)Register(typstring,c Constructor){f.mu.Lock()deferf.mu.Unlock()f.creators[typ]c}// Create 创建渠道加读锁func(f*SafeFactory)Create(typstring)(PaymentChannel,error){// 读锁: 允许多个goroutine同时读f.mu.RLock()constructor,ok:f.creators[typ]f.mu.RUnlock()if!ok{returnnil,errors.New(unsupported type: typ)}returnconstructor(),nil}// Unregister 注销渠道(热卸载场景)func(f*SafeFactory)Unregister(typstring){f.mu.Lock()deferf.mu.Unlock()delete(f.creators,typ)}// List 列出所有已注册的类型func(f*SafeFactory)List()[]string{f.mu.RLock()deferf.mu.RUnlock()types:make([]string,0,len(f.creators))fortyp:rangef.creators{typesappend(types,typ)}returntypes}// PaymentChannel 支付渠道接口typePaymentChannelinterface{Pay(amountint,orderIDstring)(string,error)Query(orderIDstring)(string,error)Name()string}// Constructor 构造函数类型typeConstructorfunc()PaymentChannel关键点是用sync.RWMutex替代普通map。读多写少场景用RWMutex读操作加读锁不阻塞其他读操作写操作加写锁独占。如果工厂在启动后不再注册新渠道可以用sync.Map读操作完全无锁性能更好。四、抽象工厂创建系列对象抽象工厂创建一系列相关对象。比如一个UI框架Windows风格创建WindowsButton和WindowsDialogMac风格创建MacButton和MacDialog。抽象工厂保证这组对象风格一致。packageui// Button 按钮接口typeButtoninterface{Render()stringOnClick(callbackfunc())}// Dialog 对话框接口typeDialoginterface{Show(titlestring)Close()}// UIFactory 抽象工厂接口// 创建一组相关的UI组件保证风格一致typeUIFactoryinterface{CreateButton()ButtonCreateDialog()DialogTheme()string}// --- Windows风格实现 ---typewinButtonstruct{}func(b*winButton)Render()string{return[Win Button]}func(b*winButton)OnClick(ffunc()){f()}typewinDialogstruct{}func(d*winDialog)Show(titlestring){}func(d*winDialog)Close(){}typewinFactorystruct{}func(f*winFactory)CreateButton()Button{returnwinButton{}}func(f*winFactory)CreateDialog()Dialog{returnwinDialog{}}func(f*winFactory)Theme()string{returnwindows}// --- Mac风格实现 ---typemacButtonstruct{}func(b*macButton)Render()string{return[Mac Button]}func(b*macButton)OnClick(ffunc()){f()}typemacDialogstruct{}func(d*macDialog)Show(titlestring){}func(d*macDialog)Close(){}typemacFactorystruct{}func(f*macFactory)CreateButton()Button{returnmacButton{}}func(f*macFactory)CreateDialog()Dialog{returnmacDialog{}}func(f*macFactory)Theme()string{returnmac}// NewUIFactory 根据主题创建抽象工厂// 工厂的工厂按需选择具体工厂funcNewUIFactory(themestring)UIFactory{switchtheme{casewindows:returnwinFactory{}casemac:returnmacFactory{}default:returnwinFactory{}}}抽象工厂和DI容器结合时工厂从容器拿依赖。比如S3StorageFactory创建S3Storage时需要AWS client从DI容器注入。工厂负责创建对象DI容器负责管理依赖工厂从容器拿依赖注入到产品中解耦了对象间的依赖关系。五、对比分析工厂方案扩展性创建逻辑分布并发安全适用场景简单工厂好(map注册)集中需加锁产品类型固定工厂方法好分散到子工厂需加锁创建逻辑复杂抽象工厂差(加系列改接口)集中需加锁创建对象系列switch工厂差集中无需产品极少简单工厂用map注册新增产品不改工厂代码扩展性最好。工厂方法把创建逻辑分散到各子工厂每个子工厂独立变化。抽象工厂保证一组产品风格一致但加新产品系列要改接口。最原始的switch-case工厂适合产品极少且不会增长的场景。总结工厂模式用接口隔离创建逻辑和业务代码。简单工厂用map注册构造函数新增产品不改工厂代码扩展性好。Go的map非并发安全运行时增删产品要加锁用sync.RWMutex或sync.Map。抽象工厂创建一系列相关对象保证风格一致。工厂和DI结合时工厂从容器拿依赖注入到产品中。下一篇我们聊观察者模式看看怎么用channel实现事件总线。