英文:
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
的值选择正确的配置(BoltConfig
或 RedisConfig
):
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
函数中节省几行代码并不是真正的节省。
如果这段代码被多个程序使用,将getRepo
和AppConfig
移动到一个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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论