• 使用工厂模式和策略模式实现动态数据源
  • 发布于 2个月前
  • 199 热度
    0 评论
现状分析
需求内容是获取多个不同项目的数据源,每个数据源的获取方式都不相同。例如,数据源1获取的方式是通过直接请求数据库的方式获取,数据源2获取的方式通过接口的方式获取。最初没有使用任何设计直接构建对应的函数,然后每次使用的时候直接调用对应的函数。
// thirdPart.go 文件
func GetSource1()
{
    // 堆代码 duidaima.com
    // do something
}
func GetSource2()
    {
        // do something
    }
    // 使用
func main()
{
    result1: = GetSource1()
    result2: = GetSource2();
}
直接使用的函数的函数也是可以的,只是没有设计,在后续的扩展时候就会发现很多问题:
1.对象没有归类,即没有封装,导致代码当前 package 会看起来像是一组随机功能
2.需要对对象进行继承、组合时无法实现,可能导致你需要不断增加函数来满足自己的功能
3.对象是具有属性的,如果有些行为时连续的操作,如果不断增加参数进行传递值,增加函数来满足功能就会显的臃肿。使用封装绑定 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()
}
可以从上面简单的代码例子可以看到:
1. IDataSource接口:定义了数据源的通用获取数据方法。主要是提供抽象层,统一了不同的数据源对象;
2. Source1和Source2结构体:实现了IDataSource接口,代表不同的数据源;
3. Factory结构体:工厂类,根据key创建不同的数据源对象。作用就是封装费了对象的创建细节;

但是具体的使用还是要根据自己的业务场景进行扩展,比如:数据源实体是复杂的。
1.对于创建实体的过程是复杂的情况下,可以使用工厂方法进行创建,否则就会导致 NewFactory(key string) 函数过于臃肿,这也是区分使用工厂模式还是工厂方法的标准
2.将工厂类抽象化,客户端就只依赖工厂接口,提高解耦和扩展性

不同数据源参数
由于每个数据源 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()来触发对应的策略行为,而不需要关心具体是哪种策略。使用策略模式,可以灵活的切换不同的算法,后续有不同的获取的方式,只需要增加策略即可。当然使用策略模式应该还需要考虑下是不是需要使用。


最后
详细的使用的主要还是要根据自己的实际业务场景进行细节的调整,没有一层不变的源码使用,只有不断调整适合自己才是正确的。

用户评论