【问题标题】:Instantiating an interface implementation based on a dynamic configuration value根据动态配置值实例化接口实现
【发布时间】:2017-08-22 07:36:21
【问题描述】:

这里是新的 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 的值,应用程序将实例化所需的存储库。做这个的最好方式是什么?我在哪里做?现在我正在做某种可怕的 factoryfactory 方法,它看起来很像 Go 的反模式。

在我的 main.go 中,我有一个函数可以读取上述反映的配置值,根据 DatabaseName 的值选择正确的配置(BoltConfigRedisConfig):

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)
}

是的,在我输入完这段代码的那一刻,我为自己带入这个世界的恐怖而退缩了。有人可以帮我逃离这个工厂地狱吗?做这种事情的更好方法是什么 - 即在运行时选择接口实现。

【问题讨论】:

  • 概括地说,问题中的方法类似于 database/sql 和 image 包如何处理这个问题。参见database/sql中Open/Register的实现。
  • 这个问题最初出现是因为我有一个周期性依赖 - 非常有用的链接伙计们,谢谢。

标签: go


【解决方案1】:

您需要动态注册吗?由于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 包中可能有意义。

【讨论】:

  • 我认为你是对的,我只是把事情复杂化了。如果这是一个库,那么拥有一个注册表/工厂和动态配置存储库是有意义的,但是由于所有支持的后端都已经像你所说的那样“嵌入”到服务器中,所以真的没有意义。
猜你喜欢
  • 1970-01-01
  • 2017-08-21
  • 2022-10-06
  • 2016-10-04
  • 2014-03-12
  • 2011-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多