根据动态配置值实例化接口实现

huangapple go评论68阅读模式
英文:

Instantiating an interface implementation based on a dynamic configuration value

问题

新来的地鼠,从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 中,我有一个函数,它读取上述反射配置值,根据 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)
}

是的,我一打完这段代码,我就对我带入这个世界的恐怖感到厌恶。有人能帮我摆脱这个工厂地狱吗?有没有更好的方法来实现这种类型的事情 - 即在运行时选择接口实现?

英文:

New Gopher here, coming from Java land.

Let's say I have a some generic storage interface:

package repositories

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

I support multiple different backends (Redis, Boltdb, etc) by implementing this interface in separate packages. However, each implementation has unique configuration values that need to be passed in. So I define a constructor in each package, something like:

package redis 

type Config struct {
 ...
}

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

and

package bolt

type Config struct {
  ...
}

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

main.go reads a json configuration file that looks something like:

type AppConfig struct {
  DatabaseName string,
  BoltConfig *bolt.Config,
  RedisConfig *redis.Config,
}

Based on the value of DatabaseName, the app will instantiate the desired repository. What is the best way to do this? Where do I do it? Right now I'm doing some kind of horrible factoryfactory method which seems very much like a Go anti-pattern.

in my main.go, I have a function that reads the above reflected configuration values, selecting the proper configuration (either BoltConfig or RedisConfig) based on the value of DatabaseName:

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

and in my repositories package, I have a factory that looks for the repository type and returns a factory function that produces an instantiated repository:

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

bringing it all together, my main.go looks like:

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

And yes, the second I was done typing this code I recoiled at the horror I had brought into this world. Can someone please help me escape this factory hell? What's a better way to do this type of thing -i.e selecting an interface implementation at runtime.

答案1

得分: 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包中可能是有意义的。

英文:

Do you need dynamic registration? It seems like the list of backends is already baked into your server because of the AppConfig type, so you may be better just writing the simplest possible factory code:

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

}

Sure, you can replace this with generic reflection-based code. But while that saves a few lines of duplicated code and removes the need to update getRepo if you add a new backend, it introduces a whole mess of confusing abstraction, and you're going to have to edit code anyway if you introduce a new backend (for example, extending your AppConfig type), so saving a couple of lines in getRepo is hardly a saving.

It might make sense to move getRepo and AppConfig into a repos package if this code is used by more than one program.

huangapple
  • 本文由 发表于 2017年3月29日 09:57:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/43082936.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定