最佳实践
以下是我们推荐使用 Wire 的最佳实践。这个列表会不断更新。
区分类型
如果你需要注入一个常见的类型如 string
,那么为了避免与其他提供者产生冲突,请创建一个新的字符串类型。例如:
type MySQLConnectionString string
选项结构体
一个包含很多依赖的提供者函数可以与一个选项结构体配对。
type Options struct {
// Messages 是推荐的问候语集合。
Messages []Message
// Writer 是问候消息发送的位置,nil 表示标准输出。
Writer io.Writer
}
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
// ...
}
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
库中的提供者集
当为一个库创建提供者集时,唯一可以在不破坏兼容性的情况下进行的更改是:
- 更改提供者集使用来提供特定输出的提供者,只要不引入新的输入到提供者集中即可。它可以删除输入。但是要注意,现有的注入器将使用旧的提供者,直到它们被重新生成。
- 将新的输出类型引入到提供者集中,但前提是该类型本身是新添加的。如果该类型不是新类型,则可能某些注入器已经包括了该输出类型,这将导致冲突。
所有其他更改都是不安全的。包括:
- 在提供者集中要求一个新的输入。
- 从提供者集中删除一个输出类型。
- 将现有的输出类型添加到提供者集中。
与其进行这些破坏性的更改,可以考虑添加一个新的提供者集。
例如,如果你有这样一个提供者集:
var GreeterSet = wire.NewSet(NewStdoutGreeter)
func DefaultGreeter(ctx context.Context) *Greeter {
// ...
}
func NewStdoutGreeter(ctx context.Context, msgs []Message) *Greeter {
// ...
}
func NewGreeter(ctx context.Context, w io.Writer, msgs []Message) (*Greeter, error) {
// ...
}
你可以:
- 在
GreeterSet
中使用DefaultGreeter
而不是NewStdoutGreeter
。 - 创建一个新类型
T
,并将T
的提供者添加到GreeterSet
中,前提是与提供者一起引入了T
。
你不能:
- 在
GreeterSet
中使用NewGreeter
而不是NewStdoutGreeter
。这将添加一个输入类型(io.Writer
),并要求注入器在*Greeter
的提供者之前返回一个error
,这要求不同于之前。 - 从
GreeterSet
中删除NewStdoutGreeter
。依赖于*Greeter
的注入器将被破坏。 - 在
GreeterSet
中添加一个io.Writer
的提供者。注入器可能已经为io.Writer
有一个提供者,这可能会造成冲突。
因此,你应该仔细选择库提供者集中的输出类型。通常情况下,应该优先选择较小的库提供者集。例如,一个库提供者集通常只包含一个单独的提供者函数和 wire.Bind
语句,绑定返回类型所实现的接口。避免使用较大的提供者集可以减少应用程序遇到冲突的可能性。比如,假设你的库提供了一个用于 Web 服务的客户端,尽管在库的提供者集中绑定 *http.Client
看似很有用,但是如果每个库都做同样的事情,会造成冲突。相反,库提供者集应该只包括 API 客户端的提供者,并让 *http.Client
成为提供者集的输入。
模拟
有两种方法可以创建具有模拟依赖项的注入应用程序。这些方法的示例详见 此处。
方法 A:向注入器传递模拟对象
创建一个仅用于测试的注入器,将所有模拟对象作为参数传入。参数类型必须是模拟对象所模拟的接口类型。无法在 wire.Build
中包含用于模拟依赖项的提供者而不会产生冲突,因此,如果你使用的是提供者集,则需要定义一个不包括模拟类型的提供者集。
方法 B:从注入器返回模拟对象
创建一个新的结构,其中包含应用程序以及希望模拟的所有依赖项。创建一个仅用于测试的注入器,返回此结构,在其中为具体类型的模拟对象提供提供者,并使用 wire.Bind
告诉 Wire 应该使用具体类型的模拟对象来实现相应的接口。