背景
我们需要一个工厂,这个工厂里生产针对各种不同领域的策略。比如说节点的相关操作,创建节点,节点迁移就是一个领域,实例的相关操作,创建库,创建表也是一个领域。
而且这个工厂会被很多地方调用,所以我们希望把它做成一个单例类。
遇到的问题
当我想要支持节点领域的时候,我就需要写节点领域工厂的代码,里面通常有两个方法,一个是产品注册到工厂,一个是从工厂获取产品,示意代码如下。
// INodeFactory 策略工厂
type INodeFactory interface {
GetStrategy(key string) INodeProduct
RegisterStrategy(key string, product INodeProduct)
}
var (
localFactory INodeFactory
onceFactory sync.Once
)
// NodeFactory 获取策略工厂
func NodeFactory() INodeFactory {
onceFactory.Do(func() {
localFactory = &nodeFactory{
localNodeMap: map[string]INodeProduct{},
}
})
return localFactory
}
type nodeFactory struct {
localNodeMap map[string]INodeProduct
}
// GetStrategy 获取策略
func (n *nodeFactory) GetStrategy(key string) INodeProduct {
return nil
}
// RegisterStrategy 注册策略
func (n *nodeFactory) RegisterStrategy(key string, product INodeProduct) {
}
这么写是没问题的,但是当我们领域非常多的时候,问题就出现了。我们需要为每一个领域书写工厂代码,而且每个领域的工厂其实都长的一样,都只需要两个方法,注册产品和获取产品,这样代码就会显得非常的冗余。
思考
想着既然代码都一样,那能不能合成一个代码呢。
一个大工厂
第一个版本想着能不能把所有领域的工厂都合并起来,弄一个大工厂,一个工厂里面负责生产所有的产品。
// IFactory 策略工厂
type IFactory interface {
GetStrategy(domain, key string) INodeProduct
RegisterStrategy(domain, key string, product INodeProduct)
}
var (
localFactory IFactory
onceFactory sync.Once
)
// Factory 获取策略工厂
func Factory() IFactory {
onceFactory.Do(func() {
localFactory = &factory{
localNodeMap: map[string]map[string]INodeProduct{},
localInstanceMap: map[string]map[string]IInstanceProduct{},
}
})
return localFactory
}
type factory struct {
localNodeMap map[string]map[string]INodeProduct
localInstanceMap map[string]map[string]IInstanceProduct
}
// GetStrategy 获取策略
func (n *factory) GetStrategy(domain, key string) INodeProduct {
return nil
}
// RegisterStrategy 注册策略
func (n *factory) RegisterStrategy(domain, key string, product INodeProduct) {
}
这么写是可以满足需求的,采用一个大工厂也解决了刚刚的问题,这样每次新加一个领域的时候只需要添加一个 localXXXMap 就可以了。
但是这种写法会比较奇怪,因为明明是不同的产品,为什么要放在同一个工厂里面生产呢。而且这个工厂也会随着领域的增加,代码量逐步增长。
有没有什么方法可以解决这个问题呢。
一个大工厂-改版
既然不想工厂随着领域的增加而代码膨胀,那能不能将所有的领域Map都合成一个呢。答案是可以。
type factory struct {
localMap map[string]map[string]interface{}
}
只需要将map里面的value由具体类型换成interface{}不就可以了,这样就可以使用一个map存下所有的产品。
但是这样只是将代码复杂度由工厂内转移到了使用方,因为存储的是interface{},那么GetStrategy()方法就会变成下面这样
// GetStrategy 获取策略
func (n *factory) GetStrategy(domain, key string) interface{} {
return nil
}
返回值变为了interface{},这样就需要调用方在自己的方法里面做强制类型转换,并判断是否转换成功。
这种方式虽然工厂代码不会膨胀,但是写起来每个调用的地方都需要转换+判断一次,不够优雅。
泛型工厂
考虑到 golang 在 1.18 的版本中已经实现了泛型技术,想着能不能使用泛型技术来让我们工厂的GetStrategy()方法返回具体的类型,这样就不需要在外面进行多余的判断。
查阅过资料过后发现golang只支持泛型函数,并不支持泛型方法。也就是说这种是不支持的
type A struct {
}
// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
return a + b
}
但是因为receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参
type A[T int | float32 | float64] struct {
}
// 方法可以使用类型定义中的形参 T
func (receiver A[T]) Add(a T, b T) T {
return a + b
}
// 用法:
var a A[int]
a.Add(1, 2)
var aa A[float32]
aa.Add(1.0, 2.0)
那我们的工厂就可以写成下面这样
type Factory[T any] struct {
localMap map[string]map[string]T
}
// GetStrategy 获取策略
func (n *Factory[T]) GetStrategy(domain, key string) T {
var t T
return t
}
// RegisterStrategy 注册策略
func (n *Factory[T]) RegisterStrategy(domain, key string, product T) {
}
看起来很美好,可以将多个产品通过泛型技术用同一份代码实现,在外面使用的时候也不需要进行类型转换。
但是这种实现方案与我们的需求不符,我们需要的是单例工厂,每次被调用的时候都拿到同一个工厂,如果使用泛型技术的话是没办法使用 sync.Once来明确每一个具体的泛型工厂的。
泛型工厂-改进
在搜素的时候发现rakyll写了一篇[文章](Generics facilitators in Go · rakyll.org),里面写了她遇到的问题及解决方案。里面提到的思路也比较适合我们的场景。
解决方案简单来说就是使用一个泛型的查询器将工厂包装起来,保证工厂的唯一性。
// Querier 查询器
type Querier[T any] struct {
factory IFactory
}
func NewQuerier[T any]() *Querier[T] {
return &Querier[T]{
factory: GetFactory(),
}
}
func (q *Querier[T]) GetStrategy(domain, key string) T {
strategy := q.factory.GetStrategy(domain, key)
return strategy.(T)
}
// IFactory 策略工厂
type IFactory interface {
GetStrategy(domain, key string) interface{}
RegisterStrategy(domain, key string, product interface{})
}
var (
localFactory IFactory
onceFactory sync.Once
)
// GetFactory 获取策略工厂
func GetFactory() IFactory {
onceFactory.Do(func() {
localFactory = &factory{
localMap: map[string]map[string]interface{}{},
}
})
return localFactory
}
type factory struct {
localMap map[string]map[string]interface{}
}
// GetStrategy 获取策略
func (n *factory) GetStrategy(domain, key string) interface{} {
return nil
}
// RegisterStrategy 注册策略
func (n *factory) RegisterStrategy(domain, key string, product interface{}) {
}
这样就由工厂保证了自身的唯一性,查询器可以被创建很多个,但是使用的都是同一个工厂,保证了单例工厂,也不需要每次使用的时候进行强制类型转换了。
总结
没有什么事是增加一层解决不了了,如果有,那就再增加一层。