曾几何时我们并不那么关心工程的良好实践,它可能源于微服务架构的发展,我们认为我们总是可以轻松地重写一切。这可能是真的,但在实践中,我们不想花时间重写代码,只想编写新代码、新产品,特别是在高速增长的公司工作。这就是为什么良好实践很重要的原因,它们可以使您的软件随着时间的推移保持可维护性。至此我们看下【依赖注入】是如何帮助我们的。
1. 什么是依赖注入
依赖注入是一种设计模式,可帮助您解耦实现的外部逻辑。实现需要外部 API 或数据库等是很常见的。知道这些东西不是实现的责任,它应该接收其依赖项并根据需要使用它们。假设您有一个具有以下依赖项的实现。
你可以创建依赖项并将它们设置到您的服务/用例中。但这对于测试尤其不利,如果想更改依赖服务的实现比如数据库,比如如果您唯一的选择是编写集成测试而不是单元测试。实现方式如下
依赖注入的关键:不要注入实现(structs)应该注入interfaces. 这是 S.O.L.I.D 的字母 D:依赖倒置原则。它允许您轻松切换某些依赖项的实现,并且您可以更改模拟实现的真实实现。它是单元测试的基础。
手动构造注入:
2 依赖注入的种类
有几种依赖注入,每种都有自己的使用场景。我们主要看下其中的三个:构造函数、属性和方法(或 Setter)。
构造注入:最常见的一种是构造函数注入。它允许您使您的实现不可变,没有任何东西可以改变依赖关系(如果您的属性是私有的)。此外,它要求所有依赖项都准备好创建某些东西。如果不是,通常会产生错误。
属性和方法注入非常相似,我认为它们的采用是语言特性的问题。在 Java 中更常见的是方法注入,而在 C# 中更常见的是属性注入。在 Go 中,我们将看到两者的用法。这些类型允许您在运行时更改依赖关系,因此按照设计,它们不是不可变的。但是如果您需要更改某些依赖项的实现,则不需要重新创建所有内容。您可以覆盖您需要的内容。如果您有一个功能标志可以更改服务中的实现,这可能会很有用。
方法注入:
3. 如何实现注入
- 手动注入
- 容器注入
3.1 手动注入
手动构建是一种客观的方法。您逐步声明、创建和注入您的依赖项。我认为它很干净,幕后没有任何魔法发生。问题是随着依赖关系变得复杂,需要自己处理复杂性。可能会看到您func main() 包含数百行代码并且更难维护。
3.2 容器
在容器模式下,需要告诉容器如何创建依赖项,然后它会创建您的依赖关系图以发现如何创建依赖项。一旦你请求依赖,它就会按照图表创建与之相关的所有内容。让我们想象一个场景,我们需要创建两个服务/用例,以及它们之间存在依赖关系:
一旦你教会了你的容器如何创建每个依赖项,它就会创建一个依赖关系图,就像这样:
如果您选择了手动样式,想象一下这个场景越来越大。想象一下您的所有服务都在执行相同的启动过程。从长远来看,它会变得复杂。
uber-go/dig 是 Uber 为解决此类问题而开发的依赖注入工具包,带有反射。它非常强大,可以帮助您减少为设置服务而编写的代码。重要的是要说它专注于应用程序启动,还有其他容器,例如 sarulabs/di,它们也可能处理依赖生命周期(例如,每个请求一个实例)。如果你对这种依赖注入感兴趣,可以看看。按照设计,uber-go/dig 把一切都是理解为单例,它只会创建你的依赖项一次。
uber-go/dig的所有参数都是interface{},但是不能传你想要的。它必须是一个接收N个参数的函数,它也可能返回N个结果,也是一个错误。通过反射,它读取你的 func 接收和返回的每个参数的类型,然后它可以构建我们上面讨论的依赖关系图。如果某些函数返回错误,当您尝试调用它时它也会返回错误。如果您尝试连接到某个数据库但它没有启动,这很有用。
容器构造注入:(构造参数--手动指定 缺点....)
具体实现:
属性注入:
uber-go/dig 也支持属性注入,这个策略允许你创建你的基础依赖的模块。每个服务都需要一些日志配置、一些数据库连接、消息代理、云提供商客户端等。你可以将这些依赖抽象到你公司的内部库中,让你的服务只提供它们自己的依赖。它将从您的微服务中删除大量重复的代码。为此,您需要创建一个带有 dig.In 的结构,它会告诉容器它必须使用您提供的值填充所有属性。在下面的示例中,当容器尝试创建 MyArgs 时,它会看到 dig.In 嵌入其中,因此它将设置您在 Service 和 Logger 属性中提供的值。
param struct 结构体 : dig.In 自动属性注入
4 小节
如果您正在寻找一种更简单的方法来管理代码中的依赖项,我相信 uber-go/dig 对您来说可能是一个不错的选择。但是请注意,它是一个工具包,替代我们做了很多肮脏的工作,所以当另一个工程师第一次查看您的代码时,她/他会觉得它正在发生很多神奇的事情,而且确实如此。此外,我认为它可以改善您的工程团队的经验。它消除了无聊的重构。假设您需要在服务/用例中添加新的依赖项,您需要重构调用其构造函数的所有内容(单元测试、main 等)。一个容器可以帮助你处理这些东西。因此,团队可以停止重构不必要的代码并专注于真正重要的事情。 uber-go/dig 是一个不错选择。它使您的服务的设置过程更容易,它使它变得干净。手动样式始终是一种选择,但是当您具有复杂的依赖关系时,它会变得更难处理,并且当您有多个具有相似依赖关系的服务时,您可能会在它们之间复制/粘贴代码。对于一个成长中的平台来说,最好有一些围绕它的模式来保持事情以同样的方式进行。