项目启动时,依赖越来越多,依赖之间还有先后顺序,main函数的代码会慢慢膨胀起来变得不容易维护,这种情况下可以引入依赖注入框架。今天我们来分享 Google wire 库。

依赖注入介绍

依赖注入(dependency injection,缩写为 DI)是一种设计模式,这种模式能让一个物件接收它所依赖的其他物件。所谓“依赖”是指接收方所需的对象,“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”。该设计的目的是为了分离关注点,分离接收方和依赖,从而提供松耦合以及代码重用性。传统编程方式,客户对象自己创建一个服务实例并使用它。这带来的缺点和问题是:

  • 如果使用不同类型的服务对象,就需要修改、重新编译客户类。
  • 客户类需要通过配置来适配服务类及服务类的依赖。如果程序有多个类都使用同一个服务类,这些配置就会变得复杂并分散在程序各处。
  • 难以单元测试。本来需要使用服务类的 mock 或 stub,在这种方式下不太可行。

依赖注入可以解决上述问题:

  • 使用接口或抽象基类,来抽象化依赖实现。
  • 依赖在一个服务容器中注册。客户类构造函数被注入服务实例。框架负责创建依赖实例并在没有使用者时销毁它。依赖注入可以解决上述问题依赖注入涉及四个概念:
  • 服务:任何类,提供了有用功能。
  • 客户:使用服务的类。
  • 接口:客户不应该知道服务实现的细节,只需要知道服务的名称和 API。
  • 注入器:Injector,也称 assembler、container、provider 或 factory。负责把服务引入给客户。依赖注入把对象构建与对象注入分开。因此创建对象的 new 关键字也可消失了。

让我们创建一个使用依赖注入设计原则的小程序,模拟一个带有特定消息的迎宾员问候客人的活动。

type Message stringtype Greeter struct {    // ... TBD }  type Event struct {         // ... TBD }

该Message类型只是包装一个字符串。现在,我们将创建一个总是返回硬编码消息的简单初始化程序:

func NewMessage() Message{         return Message("Hi there!") }

我们Greeter将需要参考Message,因此,让Greeter也为我们创建一个初始化程序。

func NewGreeter(m Message) Greeter {    return Greeter{Message: m} }  type Greeter struct {    Message Message // <- adding a Message field }

在初始化程序中,我们将一个Message字段分配给Greeter. 现在,我们可以 Message在创建Greet方法时使用Greeter:

func (g Greeter) Greet() Message {    return g.Message }

接下来,需要Event有一个Greeter,因此我们也将为它创建一个初始化程序。

func (g Greeter) Greet() Message {    return g.Message }

然后我们添加一个方法来启动Event:

func (e Event) Start() {    msg := e.Greeter.Greet()         fmt.Println(msg) }

该Start方法是该小应用程序的核心:它告诉问候者发出问候语,然后将该消息打印到屏幕上。现在已经准备好应用程序的所有组件,如何在不使用 Wire 的情况下初始化所有组件。主要功能如下所示:

func main() {        message := NewMessage()         greeter := NewGreeter(message)         event := NewEvent(greeter)          event.Start() }

首先创建一条消息,然后用该消息创建一个问候语,最后我们用该问候语创建一个事件。完成所有初始化后,我们就可以开始我们的活动了。依赖注入的一个缺点是需要太多的初始化步骤。让我们看看如何使用 Wire 使初始化组件的过程更加顺畅。

Google wire库使用

Wire是一种代码生成工具,它使用依赖注入自动连接组件。组件之间的依赖关系在 Wire 中表示为函数参数,鼓励显式初始化而不是全局变量。我们把以上小程序引入wire库来管理:让我们首先将main函数更改为如下所示:

func main() {     e := InitializeEvent()          e.Start() }

接下来,在一个单独的文件中,wire.go我们将定义InitializeEvent. 这是事情变得有趣的地方:

// wire.gofunc InitializeEvent() Event {    wire.Build(NewEvent, NewGreeter, NewMessage)         return Event{} }

不必经历依次初始化每个组件并将其传递给下一个组件的麻烦,而是通过一次调用来wire.Build 传递我们想要使用的初始化器。在 Wire 中,初始化器被称为“提供者”,即提供特定类型的函数。添加一个零值 Event作为返回值以满足编译器。请注意,即使我们向Event中添加值,Wire 也会忽略它们。事实上,注入器的目的是提供有关使用哪些提供程序来构建一个的信息Event,因此我们将在文件顶部使用构建约束将其从最终二进制文件中排除:

//+build wireinject

请注意,需要一个空白的尾随行。
用 Wire 的话说,InitializeEvent就是一个“注入器”。现在已经完成了注入器,可以使用wire命令行工具了。使用以下工具安装该工具:

go install github.com/google/wire/cmd/wire@latest

然后在与上述代码相同的目录中,只需运行wire. Wire 将找到InitializeEvent注入器并生成一个函数,函数体中填充了所有必要的初始化步骤。结果将写入名为wire_gen.go.让我们看看Wire做了什么:

// wire_gen.gofunc InitializeEvent() Event {    message := NewMessage()         greeter := NewGreeter(message)         event := NewEvent(greeter)         return event }

它看起来就像我们上面写的一样!现在这是一个只有三个组件的简单示例,因此手动编写初始化程序并不太痛苦。想象一下 Wire 对于复杂得多的组件有多大用处。使用 Wire 时,我们会将wire.go和都提交wire_gen.go给源代码管理。

总结一下:首先,编写了一些具有相应初始化程序或提供程序的组件。接下来,创建了一个注入器函数,指定它接收哪些参数以及返回哪些类型。wire.Build后,通过调用提供所有必要的提供程序来填充注入器函数。最后,运行wire命令生成连接所有不同初始化程序的代码。注入器添加一个参数和一个错误返回值时,wire再次运行会对我们生成的代码进行所有必要的更新。这里的示例很小,但它展示了 Wire 的一些强大功能,以及它如何减轻使用依赖项注入初始化代码的痛苦。此外,使用 Wire 生成的代码看起来很像我们以其他方式编写的代码。没有将用户提交给 Wire 的定制类型。相反,它只是生成的代码。大家可以随心所欲地使用它。