Go实践|使用工厂模式和策略模式获取数据源
记录一次自己在处理获取不同数据源的优化过程!
工厂模式 + 策略模式
现状分析
需求内容是获取多个不同项目的数据源,每个数据源的获取方式都不相同。例如,数据源1获取的方式是通过直接请求数据库的方式获取,数据源2获取的方式通过接口的方式获取。最初没有使用任何设计直接构建对应的函数,然后每次使用的时候直接调用对应的函数。
// thirdPart.go 文件
func GetSource1() {
// do something
}
func GetSource2() {
// do something
}
// 使用
func main() {
result1 := GetSource1()
result2 := GetSource2();
}
直接使用的函数的函数也是可以的,只是没有设计,在后续的扩展时候就会发现很多问题:
- 对象没有归类,即没有封装,导致代码当前 package 会看起来像是一组随机功能
- 需要对对象进行继承、组合时无法实现,可能导致你需要不断增加函数来满足自己的功能
- 对象是具有属性的,如果有些行为时连续的操作,如果不断增加参数进行传递值,增加函数来满足功能就会显的臃肿。使用封装绑定 struct,并对 struct设置一定的属性,后续在一些连续操作时可以保持实例的属性值。即将数据和行为进行封装。
使用工厂模式
对于上面数据源可以绑定每个数据源结构体,然后再使用工厂模式,就能实现不同的场景,创建不同的数据源是对象。
// 定义数据源获取接口,
type IDataSource interface {
FetchData()([]byte, error)
}
// 数据源实体1
type Source1 struct{}
func (s *Source1)FetchData()([]byte, error){
// do somnething
return result,err
}
// 数据源实体2
type Source2 struct{}
func (s *Source1)FetchData()([]byte, error){
// do somnething
return result,err
}
// 工厂类
type Factory struct{}
func (F factory)NewFactory(key string) *IDataSource {
switch key {
case "source1":
return &Source1{}
case "source2":
return &Source2{}
}
}
// main.go
func main(){
s := NewFactory("source1")
s.FetchData()
}
可以从上面简单的代码例子可以看到:
- IDataSource接口:定义了数据源的通用获取数据方法。主要是提供抽象层,统一了不同的数据源对象;
- Source1和Source2结构体:实现了IDataSource接口,代表不同的数据源;
- Factory结构体:工厂类,根据key创建不同的数据源对象。作用就是封装费了对象的创建细节;
但是具体的使用还是要根据自己的业务场景进行扩展,比如:数据源实体是复杂的。
- 对于创建实体的过程是复杂的情况下,可以使用工厂方法进行创建,否则就会导致 NewFactory(key string) 函数过于臃肿,这也是区分使用工厂模式还是工厂方法的标准
- 将工厂类抽象化,客户端就只依赖工厂接口,提高解耦和扩展性
不同数据源参数
由于每个数据源 FetchData 时它们的参数都是不一样的。可以看上面源码,将参数都转换为 []byte 类型。然后在具体的实现中转为其对应的类型数据。
type SourceParams struct {
Start string
UserId string
}
type Source1 struct {
}
func (*Source1) FetchData(params interface{}) ([]byte, error) {
p := params.(SourceParams)
// do something ...
}
相同数据源不同获取方式
当需求升级,对于相同的数据源但是可能参数不一样了,或者是提供新版本的获取方式但又要保持第一版本的方式,这个时候怎么扩展。
第一版本:构建新的数据源Struct 当做新的数据源,比如 SourceV1。
type Factory struct{}
func (F factory)NewFactory(key string) *IDataSource {
switch key {
case "source1":
return &Source1{}
case "source1V1":
return &source1V1{}
case "source2":
return &Source2{}
}
}
第二版本:使用策略模式
type FetchStrategy interface{
Fetch()([]byte,error)
}
// 获取数据方式1
type FetchV1 struct{}
func (s *FetchV1) Fetch() {
// do something ...
}
// 获取数据方式2
type FetchV2 struct{}
func (s *FetchV2) Fetch() {
// do something ...
}
type Source1 struct {
strategy FetchStrategy // 数据源增加strategy 属性
}
// 获取时就可以根据对应策略获取数据
func (s *Source1) FetchData(params interface{}) ([]byte, error) {
p := params.(SourceParams)
// do something ...
s.strategy.Fetch()
}
// 外层的工厂模式中返回对应的实体时可以增加对应策略key参数
func (F factory)NewFactory(key string, strategy FetchStrategy) *IDataSource {
switch key {
case "source1":
return &Source1{strategy:strategy}
case "source2":
return &Source2{}
}
}
从上面的源码可以看到,在Souce中增加strategy 属性,在工厂中创建DataSource时,传入不同的策略对象到strategy字段中,这样就可以动态改变所使用的策略。在DataSource的FetchData方法内,它只需要调用strategy.Fetch()来触发对应的策略行为,而不需要关心具体是哪种策略。
使用策略模式,可以灵活的切换不同的算法,后续有不同的获取的方式,只需要增加策略即可。当然使用策略模式应该还需要考虑下是不是需要使用。
最后
详细的使用的主要还是要根据自己的实际业务场景进行细节的调整,没有一层不变的源码使用,只有不断调整适合自己才是正确的。
转载自:https://juejin.cn/post/7268607295017844773