基于动态配置值实例化接口实现

时间:2017-03-29 01:57:20

标签: go

新Gopher来自Java土地。

假设我有一个通用的存储接口:

package repositories

type Repository interface {
  Get(key string) string
  Save(key string) string
}

通过在单独的包中实现此接口,我支持多个不同的后端(Redis,Boltdb等)。但是,每个实现都有唯一的配置值需要传入。所以我在每个包中定义一个构造函数,如:

package redis 

type Config struct {
 ...
}

func New(config *Config) *RedisRepository {
  ...
}

package bolt

type Config struct {
  ...
}

func New(config *Config) *BoltRepository {
  ...
}

main.go读取类似于:

的json配置文件
type AppConfig struct {
  DatabaseName string,
  BoltConfig *bolt.Config,
  RedisConfig *redis.Config,
}

根据DatabaseName的值,应用程序将实例化所需的存储库。做这个的最好方式是什么?我在哪里做?现在我正在做一些可怕的工厂方法,看起来非常像Go反模式。

在我的main.go中,我有一个函数读取上面反映的配置值,根据BoltConfig的值选择正确的配置(RedisConfigDatabaseName):

func newRepo(c reflect.Value, repoName string) (repositories.Repository, error) {
    t := strings.Title(repoName)

    repoConfig := c.FieldByName(t).Interface()

    repoFactory, err := repositories.RepoFactory(t)
    if err != nil {
        return nil, err
    }

    return repoFactory(repoConfig)
}

在我的repositories包中,我有一个工厂,它查找存储库类型并返回一个生成实例化存储库的工厂函数:

func RepoFactory(provider string) (RepoProviderFunc, error) {
    r, ok := repositoryProviders[provider]
    if !ok {
        return nil, fmt.Errorf("repository does not exist for provider: %s", r)
    }
    return r, nil
}

type RepoProviderFunc func(config interface{}) (Repository, error)

var ErrBadConfigType = errors.New("wrong configuration type")

var repositoryProviders = map[string]RepoProviderFunc{
    redis: func(config interface{}) (Repository, error) {
        c, ok := config.(*redis.Config)
        if !ok {
            return nil, ErrBadConfigType
        }
        return redis.New(c)
    },
    bolt: func(config interface{}) (Repository, error) {
        c, ok := config.(*bolt.Config)
        if !ok {
            return nil, ErrBadConfigType
        }
        return bolt.New(c)
    },
}

将所有内容整合在一起,我的main.go看起来像是:

cfg := &AppConfig{}
err = json.Unmarshal(data, cfg)
if err != nil {
    log.Fatalln(err)
}

c := reflect.ValueOf(*cfg)

repo, err := newRepo(c, cfg.DatabaseName)
if err != nil {
    log.Fatalln(err)
}

是的,第二次我完成了键入这个代码我因为我带入这个世界的恐怖感而退缩。有人可以帮我逃避这个工厂吗?有什么更好的方法来做这种事情 - 即在运行时选择接口实现。

1 个答案:

答案 0 :(得分:2)

您需要动态注册吗?由于AppConfig类型,似乎已经将后端列表添加到您的服务器中,因此您可能最好只编写最简单的工厂代码:

func getRepo(cfg *AppConfig) (Repository, error) {
    switch cfg.DatabaseName {
    case "bolt":
        return bolt.New(cfg.BoltConfig), nil
    case "redis":
        return redis.New(cfg.RedisConfig), nil
    }
    return nil, fmt.Errorf("unknown database: %q", cfg.DatabaseName)
}

func main() {
    ...
    var cfg AppConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        log.Fatalf("failed to parse config: %s", err)
    }
    repo, err := getRepo(&cfg)
    if err != nil {
        log.Fatalln("repo construction failed: %s", err)
    }
   ...

}

当然,您可以使用基于反射的通用代码替换它。但是,虽然这样可以节省几行重复的代码,并且如果添加新的后端,则无需更新getRepo,但它会引入一堆混乱的抽象,并且您无论如何都必须编辑代码如果你引入了一个新的后端(例如,扩展你的AppConfig类型),那么在getRepo中保存几行就不算什么了。

如果此代码由多个程序使用,则将getRepoAppConfig移至repos包可能是有意义的。