Go-依赖注入实用指南(五)

86 阅读22分钟

Go 依赖注入实用指南(五)

原文:zh.annas-archive.org/md5/87633C3DBA89BFAAFD7E5238CC73EA73

译者:飞龙

协议:CC BY-NC-SA 4.0

第十三章:评估

许多章节末尾的问题都是故意引发思考的,就像编程中的许多事情一样,答案往往取决于程序员的情况或世界观。

因此,接下来的答案可能与你的不同,这没关系。这是我的答案,不一定是你的“正确”答案。

第一章,永远不要停止追求更好

  1. 什么是依赖注入?

在本章中,我将依赖注入定义为以这样的方式编码,即我们依赖的资源(即函数或结构)是抽象的。

我们接着说,因为这些依赖是抽象的,对它们的更改不需要对我们的代码进行更改。这个花哨的词是解耦。

对我来说,解耦实际上是这里的基本属性和目标。当对象解耦时,它们就更容易处理。更容易扩展、重构、重用和测试。虽然这些都非常重要,但我也试图保持务实。最终,如果软件没有解耦并且不使用依赖注入,它仍然会正常工作。但随着时间的推移,它将变得越来越难处理和扩展。

  1. 依赖注入的四个突出优势是什么?
  • 依赖注入通过以抽象或通用的方式表达依赖关系,减少了在处理代码时所需的知识。对我来说,这是关于速度。当我进入一段代码,特别是在一个大项目中,当其依赖关系是抽象的时,更容易理解特定部分(如结构)在做什么。通常,这是因为关系被很好地描述,交互干净(换句话说,没有对象嫉妒)。

  • 依赖注入使我们能够在与依赖项隔离的情况下测试我们的代码。与第一点类似,当依赖关系是抽象的且交互干净时,通过操纵其与依赖项的交互来测试当前代码片段是容易理解的,因此更快。

  • 依赖注入使我们能够快速而可靠地测试那些否则难以或不可能的情况。我知道,我非常注重测试。我并不是一个狂热者;这纯粹是自我保护和我对专业精神的理解。当我为别人编写代码时,我希望它尽可能地好(在资源限制内)。此外,我希望它继续按照我打算的方式工作。测试帮助我在构建过程中和将来澄清和记录我的意图。

  • 依赖注入减少了扩展或更改的影响。当一个方法签名发生变化时,它的用法也会发生变化。当我们依赖于我们自己的代码(如本地接口)时,我们至少可以选择如何应对变化。我们可以切换到其他依赖项;我们可以在中间添加一个适配器。无论我们如何处理,当我们的代码和测试依赖于未更改的部分时,我们可以确信任何出现的问题都在更改的部分或其提供的功能中。

  1. 它解决了什么样的问题?

这个答案本质上就是关于“代码异味”的整个部分,其中包括代码膨胀、难以改变、浪费的努力和紧密耦合。

  1. 为什么怀疑是重要的?

在我们的行业中,解决问题的方法几乎总是不止一种。同样,几乎总有很多人向你推销灵丹妙药来解决你所有的问题。就我个人而言,当被问及一个解决方案是否有效时,我的答案通常是这取决于。这可能会激怒那些寻求简单答案却收到一大堆问题的人,但实际上很少有确定的答案。事实上,这可能是让我不断回头的原因。总有新的东西要学习,新的想法要尝试,旧的概念要重新发现。因此,我恳求你,始终倾听,始终质疑,不要害怕尝试和失败。

5. 对你来说,惯用的 Go意味着什么?

这绝对没有正确的答案。请不要让任何人告诉你相反。如果你在团队中保持一致,那就足够了。如果你不喜欢这种风格,提出并辩论一个更好的风格。虽然很多人都抗拒改变,但更少的人反对更好的代码。

第二章,Go 的 SOLID 设计原则

1. 单一责任原则如何改进 Go 代码?

通过应用单一责任原则,我们的代码的复杂性得到了减少,因为它将代码分解为更小、更简洁的部分。

通过更小、更简洁的部分,我们增加了相同代码的潜在可用性。这些更小的部分更容易组合成更大的系统,因为它们的要求更轻,性质更通用。

单一责任原则还使得编写和维护测试变得更简单,因为当一段代码只有一个目的时,测试所需的范围(因此复杂性)就会大大减少。

2. 开闭原则如何改进 Go 代码?

开闭原则有助于通过鼓励我们不改变现有的代码,特别是公开的 API,来减少添加和扩展的风险。

开闭原则还有助于减少添加或删除功能所需的更改数量。当摆脱某些代码模式(如 switch 语句)时,这一点尤为突出。switch 语句很棒,但它们往往存在于多个地方,当添加新功能时很容易忽略其中一个实例。

此外,一旦出现问题,由于问题要么在新添加的代码中,要么在其与使用之间的交互中,因此更容易找到。

3. Liskov 替换原则如何改进 Go 代码?

通过遵循 Liskov 替换原则,我们的代码无论注入了什么样的依赖,都能保持一致的表现。另一方面,违反 Liskov 替换原则会导致违反开闭原则。这些违规行为会导致我们的代码对实现有过多的了解,从而破坏了注入依赖的抽象性。

在实现接口时,我们可以利用 Liskov 替换原则对一致行为的关注作为一种检测与不正确抽象相关的代码异味的方法。

4. 接口隔离原则如何改进 Go 代码?

接口隔离原则要求我们定义薄接口和明确的输入。这些特性使我们能够将我们的代码与实现我们的依赖的实现解耦。

所有这些都导致了简洁、易于理解和方便使用的依赖定义,特别是在测试过程中使用模拟和存根时。

5. 依赖反转原则如何改进 Go 代码?

依赖反转原则迫使我们关注抽象的所有权,并将其焦点从使用转移到需要

它还进一步将我们的依赖定义与其实现解耦。与接口隔离原则一样,结果是代码更加简单和独立,特别是与其用户分离。

第三章,用户体验编码

1. 代码的可用性为什么重要?

良好的用户体验并不像糟糕的用户体验那么明显。这是因为当用户体验良好时,它只是有效

通常,代码越复杂、难以理解、或者不寻常,就越难以理解。代码越难以跟踪,就越难以维护或扩展,出错的可能性就越大。

2. 谁最能从具有良好用户体验的代码中受益?

作为程序员,我们既是代码的创造者,也是最大的用户;因此,最受益的是我们的同事和我们自己。

3. 如何构建良好的用户体验?

最好的用户体验是直观和自然的。因此,关键是要尽量像你的用户一样思考。你写的代码可能对你来说是有意义的,希望对你来说也是自然的,但是你能说对你的团队其他成员也是这样吗?

在本章中,我们定义了一些需要牢记的方面:

  • 简单开始,只有在必要时才变得复杂。

  • 应用足够的抽象。

  • 遵循行业、团队和语言的惯例。

  • 只导出必要的内容。

  • 积极应用单一职责原则。

我们还介绍了UX 发现调查,作为深入了解你的用户的一种方式。调查包括四个问题:

  • 谁是用户?

  • 你的用户有什么能力?

  • 用户为什么想要使用你的代码?

  • 你的用户期望如何使用它?

4. 单元测试能为你做什么?

总之,很多事情。这因人而异。主要是,我使用测试来给我信心,要么快速前进,要么承担重任,这取决于需要什么。

我还发现测试在记录作者的意图方面做得很好,而且不太可能像注释那样过时。

5. 你应该考虑哪种测试场景?

你总是要考虑至少三种场景:

  • **快乐的路径:**你的函数是否做你期望它做的事情?

  • **输入错误:**在使用中可预测的错误(特别是输入)

  • **依赖问题:**当依赖关系失败时,你的代码是否能正常运行?

6. 表驱动测试(TDTs)如何帮助?

TDT 对减少由同一函数的多个测试场景引起的重复很有帮助。

它们通常比复制/粘贴大量测试更有效。

7. 测试如何损害你的软件设计?

这可能有很多种方式,有些是相当主观/个人的;但在本章中,我们概述了一些常见的原因:

  • 只有因为测试而存在的参数、配置选项或输出

  • 由测试引起或导致的参数泄漏抽象

  • 在生产代码中发布模拟

  • 过度的测试覆盖

第四章,ACME 注册服务简介

1. 对我们的服务定义的目标中,哪个对你个人来说最重要?

这是主观的,因此没有正确答案。就我个人而言,可能是可读性或可测试性。如果代码容易阅读,那么我可以更容易地理解它,可能也能记住更多。另一方面,如果它更容易测试,那么我可以利用这一点来编写更多的测试。有了更多的测试,我就不必记住那么多,可以让测试确保一切都按照我需要的方式执行。

2. 概述的问题中哪个似乎最紧急或最重要?

这也是主观的。你可能会感到惊讶,但我会说测试中缺乏隔离性。随着测试的进行,每个测试都有点类似于端到端测试。这意味着测试设置是冗长的,当出现问题时,找出问题所在将是耗时的。

第五章,使用 Monkey Patching 进行依赖注入

1. Monkey Patching 是如何工作的?

在其最基本的层面上,Go 中的 Monkey Patching 涉及在运行时交换一个变量为另一个变量。这个变量可以是依赖的实例(以结构体的形式)或者是一个包装对依赖的访问的函数。

在更高的层面上,猴子补丁是关于替换或拦截对依赖的访问,以将其替换为另一个实现,通常是存根或模拟,以使测试更简单。

2. 猴子补丁的理想用例是什么?

猴子补丁可以在各种情况下使用,但最显著的情况包括以下情况:

  • 使用依赖于单例的代码

  • 对于当前没有测试、没有依赖注入的代码,并且希望以最少的更改添加测试的情况

  • 在不更改依赖包的情况下解耦两个包

3. 如何使用猴子补丁来解耦两个包而不更改依赖包?

我们可以引入一个调用依赖包的函数类型的变量。然后,我们可以猴子补丁我们的本地变量,而不必更改依赖。在本章中,我们看到这对于与我们无法更改的代码(如标准库)解耦特别有用。

第六章,构造函数注入的依赖注入

1. 我们用来采用构造函数注入的步骤是什么?

  1. 我们确定了我们想要提取并最终注入的依赖关系。

  2. 我们删除了该依赖的创建并将其提升为成员变量。

  3. 然后,我们将依赖的抽象定义为本地接口,并将成员变量更改为使用该接口而不是真实的依赖。

  4. 然后,我们添加了一个构造函数,其中包含依赖的抽象作为参数,以便我们可以确保依赖始终可用。

2. 什么是守卫条款,何时使用它?

我们将守卫条款定义为一段代码,确保提供了依赖(换句话说,不是 nil)。在某些情况下,我们在构造函数中使用它们,以便我们可以百分之百确定依赖已提供。

3. 构造函数注入如何影响依赖的生命周期?

当依赖通过构造函数传入时,我们可以确保它们始终可用于其他方法。因此,与使用依赖相关的 nil 指针崩溃没有风险。

此外,我们不需要在方法中添加守卫条款或其他健全性检查,因为任何此类验证只需要存在于构造函数中。

4. 构造函数注入的理想用例是什么?

构造函数注入对许多情况都很有用,包括以下情况:

  • 所需的依赖

  • 被对象的大多数或所有方法使用的依赖

  • 当一个依赖有多个实现时

  • 依赖在请求之间不会改变的情况

第七章,方法注入的依赖注入

1. 方法注入的理想用例是什么?

方法注入非常适用于以下情况:

  • 函数、框架和共享库

  • 请求作用域依赖,比如上下文或用户凭据

  • 无状态对象

  • 提供请求中的上下文或数据的依赖,因此预计在调用之间会有所变化。

2. 为什么重要的是不保存使用方法注入注入的依赖?

因为依赖是函数或方法的参数,每次调用都会提供一个新的依赖。虽然在调用其他内部方法之前保存依赖可能比将参数传递为依赖更直接,但这样的做法会导致多个并发使用之间的数据竞争。

3. 如果我们过度使用方法注入会发生什么?

这个问题有点主观,取决于你对测试引起的损害和代码 UX 的看法。就我个人而言,我非常关心 UX。因此,通过减少参数使函数更易于使用始终在我脑海中(除了构造函数)。

从测试的角度来看,有一定形式的依赖注入要比没有更灵活。要务实;你会找到适合你的平衡点。

4. 停止短路对整个系统有什么用?

能够在没有人监听响应时停止处理请求是非常有用的。这不仅使系统更接近用户的期望,还减少了整个系统的负载。我们正在处理的许多资源是有限的,特别是数据库,我们可以做的任何事情来更快地完成请求的处理,即使最终以失败告终,也是有利的。

  1. 延迟预算如何改善用户体验?

诚然,延迟预算是一个我很少听到讨论的话题。鉴于我们行业中 API 的普遍存在,也许我们应该更多地讨论它们。它们的重要性是双重的——用于触发停止和为我们的用户设定一些界限或期望。

当我们在 API 文档中发布我们的最大执行时间时,用户将清楚地了解我们的最坏情况性能期望。此外,我们可以利用延迟预算生成的错误返回更具信息性的错误消息,进一步使用户能够做出更明智的决定。

第八章,通过配置进行依赖注入

  1. 配置注入与方法或构造函数注入有何不同?

配置注入是方法和构造函数注入的扩展形式。它旨在通过隐藏常见和环境问题来改善代码的用户体验。减少参数使方法更易于理解、扩展和维护。

  1. 我们如何决定将哪些参数移动到配置注入中?

需要考虑的关键点是参数与方法或构造函数的关系。如果依赖关系微不足道但又是必要的,比如记录器和仪器,那么将其隐藏在配置中会提高函数签名的清晰度,而不是削弱它。同样,来自配置文件的配置通常是必要的但不具信息性。

  1. 为什么我们不通过配置注入来注入所有的依赖关系?

将所有依赖项合并为一个存在两个重要问题。第一个是可读性。方法/函数的用户必须每次都打开配置定义,才能了解可用的参数。其次,作为接口,用户将被迫创建和维护一个可以提供所有参数的接口实现。虽然所有配置可能来自同一位置,但其他依赖关系可能不是。包括环境依赖有点狡猾,但它们的存在几乎是无处不在的,它们在每个构造函数中的重复将会非常恼人。

  1. 为什么我们想要注入环境依赖(如记录器)而不是使用全局公共变量?

作为程序员,我们喜欢不要重复自己DRY)原则。在所有地方注入环境依赖是很多重复的。

  1. 为什么边界测试很重要?

我希望我们都能同意测试很重要。测试的价值部分来自重复运行测试并尽快检测到回归。为了最小化频繁运行测试的成本,我们需要测试速度相当快且绝对可靠。当测试依赖于外部系统,特别是我们不负责的系统时,我们就会把测试的价值置于风险之中。

外部系统可能发生任何事情。所有者可能会破坏它;互联网/网络可能会中断。面向内部的边界测试类似于我们的单元测试。它们保护我们的代码免受回归的影响。面向外部的边界测试是我们自动化记录和确保外部系统执行我们需要它执行的方式。

  1. 配置注入的理想用例是什么?

配置注入可以在与构造函数或方法注入相同的情况下使用。关键的决定因素是依赖项本身是否应该通过配置注入进行组合并在一定程度上隐藏,并且这如何改进或减少代码的用户体验。

第九章,即时依赖注入

1. JIT(即时)依赖注入与构造函数注入有何不同?

这在很大程度上取决于构造函数注入的使用方式;特别是依赖项有多少不同的实现。如果只有一个依赖项的生产实现,那么它们在功能上是等效的。唯一的区别是用户体验(即,是否有一个更少的依赖项注入到构造函数中)。

然而,如果有多个生产实现,那么就不能使用 JIT 依赖注入。

2. 在处理可选依赖项时,为什么使用 NO-OP 实现很重要?

当成员变量没有被构造函数设置时,它实际上是可选的。因此,我们无法确定该值是否已设置且不为 nil。通过添加可选依赖项的 NO-OP 实现并自动将其设置为成员变量,我们可以假定该依赖项始终不为 nil,因此我们可以放弃对守卫子句的需求。

3. JIT 注入的理想用例是什么?

JIT 注入非常适合以下情况:

  • 替换本应注入构造函数的依赖项,且只有一个生产实现

  • 在对象和全局单例之间提供一层间接或抽象,特别是当我们想在测试期间替换全局单例时

  • 允许用户选择性地提供依赖项

第十章,现成的注入

1. 采用依赖注入框架时,可以期待获得什么?

当然,这在不同的框架之间有很大的不同,但通常,你可以期待看到以下内容:

  • 减少样板代码

  • 减少设置和维护依赖项创建顺序的复杂性

2. 在评估依赖注入框架时,应该注意哪些问题?

除了之前提到的收益之外,我的主要标准是它对代码的影响;换句话说,我是否喜欢在采用框架后代码的外观。

我还会考虑框架本身的可配置性。一些配置是可以预期的,但太多可能会导致复杂的用户体验。

最后要考虑的是框架项目的健康状况。它是否在积极维护?报告的错误是否得到了回应?在不同框架之间切换可能不会很便宜;花点时间确保你选择的框架长期来看是合适的是个好主意。

3. 采用现成的注入的理想用例是什么?

通常,框架只支持构造函数注入。因此,已经使用构造函数注入的项目可以使用现成的注入。

4. 为什么重要保护服务免受意外 API 更改的影响?

服务的 API 有时被描述为一个合同。 合同 这个词被精心选择,因为它意在传达 API 与其用户之间的关系是多么重要和有约束力。

当我们发布 API 时,我们无法控制用户如何使用我们的 API,也许更重要的是,我们无法控制他们的软件对我们 API 的更改做出反应。为了履行我们的合同,我们必须尽一切努力确保我们不会通过对 API 的计划外更改来破坏他们的软件。

第十一章,控制你的热情

1. 你最常见到的依赖注入引起的损害形式是什么?

对我来说,这绝对是过多参数。学习了依赖注入并对此感到兴奋后,很容易想要抽象和注入所有东西。这往往会使测试变得更容易,因为每个对象的责任都减少了。缺点是有很多对象和太多的注入。

如果我发现自己有太多的依赖关系,我会尝试退后一步,检查我的对象设计,特别是寻找单一责任原则方面的问题。

2. 为什么不应该一味地应用依赖注入?

仅仅因为某些东西很“酷”或者新颖,并不意味着它就是最适合这项工作的工具。我们应该始终努力解决问题的解决方案,并在可以的时候避免“模仿”编程。

3. 采用 Google Wire 等框架是否消除了依赖注入引起的所有问题?

很遗憾,不是。鉴于它只支持构造函数注入,它甚至不能在所有情况下应用。除此之外,它可以显著减少过多参数的管理痛苦。

虽然这是一件好事,但它减轻了痛苦,这使我们不太可能感到有必要解决潜在的问题。

第十二章,回顾我们的进展

1. 对我们的示例服务进行的最重要的改进是什么?

这是主观的,因此没有正确答案。对我来说,要么是解耦,要么是去除全局变量。当代码解耦时,测试变得更容易,每个部分都变成了一小块,这意味着很容易处理。基本上,我不必费太多心思或记住太多上下文。

就全局变量而言,我过去曾受到过这方面的影响,特别是在测试过程中发生的数据竞争。我无法忍受我的测试不可靠。

2. 在我们的依赖图中,为什么数据包不在主包下?

我们可以重构成这种方式,但目前我们正在模型和数据层之间使用 JIT 注入。这意味着代码的用户体验得到了改善,但依赖图并不像它本应该的那样平坦。数据层还输出 DTOs 而不是基本数据类型,因此任何用户也将使用数据包。

如果我们决定也要移除这个,我们可以为 DTO 制作一个特殊的包,然后将该包从依赖图中排除,但这是额外的工作,目前并没有太多好处。

3. 如果您要启动一个新的服务,您会做些什么不同?

这是主观的,因此没有正确答案。在进行用户体验调查后,我会首先编写足够的代码启动一个 Web 服务器,即使这时还没有使用依赖。然后我会设计所有的端点,并使用硬编码的响应来实现它们。这将使我能够用示例来与用户讨论我的可交付成果。我还可以进行一些端到端的测试,以防止任何 API 回归。

然后我的用户就可以放心地继续,对我的 API 有清晰的认识,我也可以填写细节。