英文:
Dependency Injection & Testing
问题
我正在开发一个小型的Go应用程序,基本上是各种密码存储(Ansible Vault、Hashicorp Vault、Chef Vault等)的包装器。我的想法是:在我的各种配置脚本中,我可以使用我的Go包装器来获取密码,如果我们决定在后台切换密码存储,所有接口都不需要在我的项目中进行更新。
我正在尝试为这个应用程序设置适当的测试,并在此过程中,我试图找出最佳的方法来注入我的依赖关系。
例如,假设项目名为secrets
。其中一个实现是ansible
。Ansible实现需要自己的parser
并且需要打开自己的connection
到ansible vault来检索数据。
所以我可能有以下代码:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
type Connection interface {
open() (string, error)
}
type Ansible struct {
connection Connection
contents map[string]string
}
func New(c map[string]interface{}) (*Ansible, error) {
conn, err := NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// 打开连接,解析等...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
这样看起来很好,因为secrets
包不需要了解ansible
包的依赖关系(connection),工厂只需使用一些配置数据创建新的实例。然而,如果我需要模拟Ansible接收到的connection
,似乎没有很好的方法可以做到这一点(除非该配置映射有一个名为mock
的连接选项)。
另一种选择是放弃工厂,只需从secrets
包中组装所有依赖项,例如:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(AnsibleConnection{}, config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
// 在此文件中与之前相同,但注入了依赖项...
func New(connect Connection, c map[string]interface{}) (*Ansible, error) {
conn, err := connect.NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// 打开连接,解析等...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
现在依赖项被注入了,但似乎secrets
需要了解每个实现的每个依赖项。
是否有一种更合理的方式来构建这个结构,使得secrets
知道得更少?或者顶层包是否通常负责协调一切?
英文:
I'm working on a small Go application that's basically a wrapper for various password stores (Ansible Vault, Hashicorp Vault, Chef Vault, etc). The idea is: In my various provisioning scripts, I can use my Go wrapper to grab secrets and if we decide to switch password stores behind the scenes, all of the interfaces don't need to be updated across my projects.
I'm trying to setup proper tests for this application, and in doing so, am trying to figure out the best way to to inject my dependencies.
For example, lets say the project is called secrets
. And one of my implementations is ansible
. And the ansible implementation needs its own parser
and needs to open its own connection
to the ansible vault, to retrieve the data.
So I might have the following:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
type Connection interface {
open() (string, error)
}
type Ansible struct {
connection Connection
contents map[string]string
}
func New(c map[string]interface{}) (*Ansible, error) {
conn, err := NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// open connection, parse, etc...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
So this seems nice because the secrets
package doesn't need knowledge of the ansible
package dependencies (connection), and the factory just new's up the instance with some config data. However, if I need to mock the connection
that Ansible receives, there doesn't seem to be a good way to do this (unless that config map had a connection option called mock
)
The other option is to abandon the factory, and just assemble all the dependencies from the secrets
package, like:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(AnsibleConnection{}, config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
// same as before in this file, but with injected dependency ...
func New(connect Connection, c map[string]interface{}) (*Ansible, error) {
conn, err := connect.NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// open connection, parse, etc...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
Now the dependency is injected, but it seems like secrets
needs to have knowledge of every dependency for every implementation.
Is there a more logical way to structure this so that secrets
knows less? Or is it typical for the top level package to be orchestrating everything?
答案1
得分: 3
决定后端是什么的是什么?这应该能帮助你指导。我在一个项目中做过类似的事情,支持多个数据库,我所做的基本上是:
config
包读取配置文件,确定使用的后端是什么store
包提供通用接口,并有一个函数接受配置,并返回一个实现server
包只引用接口main
包读取配置,将其传递给store
中的工厂函数,然后在创建服务器时将结果注入到服务器中
所以当我创建我的服务器(实际上使用数据存储)时,我将配置传递给 store
中的工厂函数,该函数返回一个接口,然后将其注入到服务器中。唯一需要了解不同具体实现的是暴露接口和工厂的同一个包;server
、config
和 main
包将其视为黑盒。
英文:
What decides what the backend is? That should help guide you. I've done something similar with support for multiple databases on a project, and what I did was basically:
config
package reads in config file, which determines what backend is being usedstore
package offers the generic interface and has a function that takes a config, and returns an implementationserver
package references only the interfacemain
package reads the config, passes it to the factory function instore
, then injects the result into the server on creation
So when I create my server (which actually uses the data store), I pass the config to the factory function in store
, which returns an interface, and then inject that into the server. The only thing that has to know about the different concrete implementations is the same package that exposes the interface and factory; the server
, config
, and main
packages see it as a black box.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论