因为一直在搞 node.js,对设计模式了解一直比较少,看了书不实践也记不住。最近在补充设计模式的知识。
工厂方法模式是一个比较常见的设计模式。
大部分资料将工厂方法模式分为三种:简单/静态工厂模式、工厂方法模式、抽象工厂模式。
简单工厂是对工厂方法的简化,而抽象工厂是对工厂方法的增强。
本文主要用 golang 来实现以下这三种工厂,然后分析一下优劣。
本文代码仓库地址
简单工厂 假设我们现在需要实现文件存储的功能:
我们开发的时候使用 native 的服务器本地存储
线上环境使用腾讯云oss 存储
先定义一个公共接口:
1 2 3 4 5 6 7 8 package ossimport "fmt" type OssImpl interface { GetObject() error PutObject() error }
目前我们有 本地 和 腾讯云oss两个途径,分别实现这个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type tencentOss struct { name string } func (t tencentOss) GetObject() error { fmt.Printf("Get object '%s' from tencentOSS\n" , t.name) return nil } func (t tencentOss) PutObject() error { fmt.Printf("Put object '%s' to tencentOSS\n" , t.name) return nil } type native struct { name string } func (n native) GetObject() error { fmt.Printf("Get object '%s' from native\n" , n.name) return nil } func (n native) PutObject() error { fmt.Printf("Put object '%s' to native\n" , n.name) return nil }
因为是简单工厂,所以只需要一个工厂方法,根据传入的参数,创建不同对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type ossType uint8 const ( TencentOss = iota Native ) func OssFactory (name string , t ossType) (OssImpl, error ) { var res OssImpl if t == TencentOss { res = tencentOss{name} } else if t == Native { res = native{name} } return res, nil }
创建main.go,模拟调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( oss "simple-factory/oss" ) func main () { tencentOss, err := oss.OssFactory("测试腾讯云OSS文件" , oss.TencentOss) if err != nil { panic (err) } _ = tencentOss.GetObject() _ = tencentOss.PutObject() native, err := oss.OssFactory("测试本地文件" , oss.Native) if err != nil { panic (err) } _ = native.GetObject() _ = native.PutObject() } Get object '测试腾讯云OSS文件' from tencentOSS Put object '测试腾讯云OSS文件' to tencentOSS Get object '测试本地文件' from native Put object '测试本地文件' to native
分析一下优缺点:
优点:
调用方无需关心内部实现,我只需要根据我想要的类型,获得一个对象进行文件操作,符合迪米特法则。
代码量相对 工厂方法和抽象工厂 少许多。。比较简单
缺点:
如果我想要再加一个 阿里云的oss,那就需要改动 OssFactory 方法,不符合开闭原则。
工厂方法 在上文简单工厂中说道,简单工厂有个缺点,如果我要新增一个 阿里云oss,那就要改动原来的代码。
我们增加阿里云oss,使用工厂方法模式实现一下:
增加 aliOss,实现 OssImpl接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 type aliOss struct { name string } func (a aliOss) GetObject() error { fmt.Printf("Get object '%s' from aliOss\n" , a.name) return nil } func (a aliOss) PutObject() error { fmt.Printf("Put object '%s' to aliOss\n" , a.name) return nil }
之前的一个工厂,变成了三个工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 package ossfunc TencentOssFactory (name string ) (OssImpl, error ) { return tencentOss{name: name}, nil } func NativeFactory (name string ) (OssImpl, error ) { return native{name: name}, nil } func AliOssFactory (name string ) (OssImpl, error ) { return aliOss{name: name}, nil }
实现main.go 模拟调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport "factory-method/oss" func main () { tencentOss, err := oss.TencentOssFactory("测试腾讯云OSS文件" ) if err != nil { panic (err) } _ = tencentOss.GetObject() _ = tencentOss.PutObject() native, err := oss.NativeFactory("测试本地文件" ) if err != nil { panic (err) } _ = native.GetObject() _ = native.PutObject() aliOss, err := oss.AliOssFactory("测试阿里云OSS文件" ) if err != nil { panic (err) } _ = aliOss.GetObject() _ = aliOss.PutObject() } Get object '测试腾讯云OSS文件' from tencentOSS Put object '测试腾讯云OSS文件' to tencentOSS Get object '测试本地文件' from native Put object '测试本地文件' to native Get object '测试阿里云OSS文件' from aliOss Put object '测试阿里云OSS文件' to aliOss
根据打印日志,我们确实实现了 alioss的工厂方法。
工厂方法的优点:
符合开闭原则,如果我们要再加一个 华为云oss, 只需要实现 OssImpl 接口,然后新增一个 hwOssFactory 方法就可以了,对历史代码没有任何改动。
同样对调用方隐藏了复杂的内部逻辑(虽然我们的demo很简单。),符合迪米特法则。
缺点:
抽象工厂 抽象工厂就比较复杂了,而且很少会用到。。
实在没想到上面的例子,用抽象工厂如何继续扩充需求。。(消息队列?但是和oss抽离不出来公共接口呀)
看这篇文章吧,虽然例子不够实用,但是也能说明问题:https://refactoringguru.cn/design-patterns/abstract-factory/go/example#example-0
优点:
缺点:
不符合开闭原则,如果需要增加产品族,那么就需要从头到尾改一遍。。。
总结 常用的就是简单工厂和工厂方法。
无论是哪种工厂,都隐藏了内部实现,耦合性低,符合迪米特法则。
简单工厂虽然不符合开闭原则,但是在比较简单的场景用的还是比较多。
抽象工厂比较复杂,而且扩展性也不强,一般用不到。。
疑问:
参考链接里有篇文章,使用工厂方法模式时,还多了一步注册工厂,没搞明白为什么要这么做。
参考链接: