
在本文中,我将用一种简单的语言讨论依赖项注入的基础知识(Eng。Dependency Injection,DI ),以及使用这种方法的原因。 本文适用于那些不知道什么是依赖注入或怀疑使用此技术的需要的人员。 因此,让我们开始吧。
什么是上瘾?
让我们先来看一个例子。 我们有ClassA
, ClassB
和ClassC
,如下所示:
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
您可以看到ClassA
类包含ClassB
类的实例,因此我们可以说ClassA
类取决于ClassB
类。 怎么了 因为ClassA
需要ClassB
才能正常工作。 我们也可以说ClassB
类是ClassA
类的依赖项。
在继续之前,我想澄清一下这种关系是好的,因为我们不需要一个类来完成应用程序中的所有工作。 我们需要将逻辑划分为不同的类,每个类将负责某个功能。 在这种情况下,这些类将能够有效地进行交互。
如何使用依赖项?
让我们看一下用于执行依赖项注入任务的三种方法:
第一种方法:在依赖类中创建依赖项
简而言之,我们可以在需要时创建对象。 看下面的例子:
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
这很容易! 我们在需要时创建一个类。
好处
- 很简单。
- 依赖类(在本例中为
ClassA
)完全控制如何以及何时创建依赖项。
缺点
ClassA
和ClassB
密切相关。 因此,无论何时需要使用ClassA
,我们都将不得不使用ClassB
,并且不可能用其他东西替代ClassB
。- 在对
ClassB
类的初始化进行任何更改后,您将需要调整ClassA
类(以及所有其他依赖ClassB
类)中的代码。 这使更改依赖关系的过程变得复杂。 ClassA
无法测试。 如果您需要测试一门课程,但这是软件开发中最重要的方面之一,那么您将必须分别对每个课程进行单元测试。 这意味着,如果您想ClassA
验证ClassA
类的正确操作并创建多个单元测试来对其进行测试,则如示例中所示,无论如何您都将创建ClassB
类的实例,即使您不感兴趣也是如此。 如果在测试过程中发生错误,那么您将无法理解它的位置ClassA
或ClassB
。 毕竟,当ClassA
正常工作时, ClassB
中的部分代码有可能导致错误。 换句话说,不可能进行单元测试,因为模块(类)不能彼此分离。- 必须配置
ClassA
以便它可以注入依赖项。 在我们的示例中,他需要知道如何创建ClassC
并使用它来创建ClassB
。 如果他对此一无所知,那会更好。 怎么了 由于单一责任的原则 。
每个班级应该只做自己的工作。
因此,我们不希望类负责其自身任务以外的任何事情。 依赖关系的实现是我们为其设置的另一项任务。
第二种方式:通过自定义类注入依赖项
因此,了解将依赖项注入到依赖类中并不是一个好主意,让我们探索另一种方法。 在这里,依赖类定义了构造函数内部所需的所有依赖,并允许用户类提供它们。 这是我们问题的解决方案吗? 我们稍后会发现。
看下面的示例代码:
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
现在, ClassA
获得了构造函数内部的所有依赖关系,并且可以简单地调用ClassB
类的方法而无需初始化任何东西。
好处
- 现在,
ClassA
和ClassB
松散耦合的,我们可以替换ClassB
而不会破坏ClassA
的代码。 例如, AssumeClassB
传递ClassB
我们可以传递AssumeClassB
,它是ClassB
的子类,我们的程序可以正常工作。 - 现在可以测试
ClassA
。 在编写单元测试时,我们可以创建自己的ClassB
版本(测试对象)并将其传递给ClassA
。 如果通过测试时发生错误,现在我们可以确定这绝对是ClassA
的错误。 ClassB
从使用依赖项的工作中解放了出来,可以专注于其任务。
缺点
- 此方法类似于链机制,并且在某些时候应该中断链。 换句话说,
ClassA
类的用户必须了解有关ClassB
初始化的所有知识,而这又需要有关ClassC
初始化的ClassC
,等等。 因此,您看到这些类中任何一个的构造函数的任何更改都可能导致调用类的更改,更不用说ClassA
可以有多个用户,因此将重复创建对象的逻辑。 - 尽管事实是我们的依存关系清晰易懂,但用户代码却并非易事且难以管理。 因此,一切都不是那么简单。 另外,该代码违反了单一责任的原则,因为它不仅对其工作负责,而且还对依赖类中的依赖项实现负责。
第二种方法显然比第一种方法更好,但是仍然存在缺陷。 是否有可能找到更合适的解决方案? 在考虑第三种方法之前,让我们首先讨论依赖注入的概念。
什么是依赖注入?
当依赖类不需要执行任何操作时,依赖注入是一种处理依赖类外部的依赖的方法。
基于此定义,我们的第一个解决方案显然没有使用依赖注入的想法,第二种方法是依赖类不提供任何依赖。 但是我们仍然认为第二种解决方案是不好的。 为什么?
由于依赖项注入的定义没有说明应该执行依赖项的位置(在依赖类之外),因此开发人员必须选择合适的位置进行依赖项注入。 从第二个示例中可以看到,用户类的位置不合适。
如何做得更好? 让我们看一下处理依赖关系的第三种方法。
第三种方式:让别人代替我们来处理依赖关系
根据第一种方法,依赖类负责获取其自己的依赖关系,而在第二种方法中,我们将对依赖的处理从依赖类移至用户类。 假设有其他人可以处理依赖项,因此依赖项和用户类都无法完成这项工作。 此方法使您可以直接在应用程序中使用依赖项。
依赖注入的“干净”实现(我个人认为)
处理依赖关系的责任在于第三方,因此应用程序的任何部分都不会与它们进行交互。
依赖注入不是一种技术,框架,库或类似的东西。 这只是一个想法。 这个想法是使用依赖类之外的依赖(最好是在专门分配的部分中)。 您可以应用此想法,而无需使用任何库或框架。 但是,我们通常转向实现依赖关系的框架,因为它简化了工作并避免了编写模板代码。
任何依赖项注入框架都有两个固有的特征。 您可能还可以使用其他附加功能,但是这两个功能将始终存在:
首先,这些框架提供了一种确定应实施的字段(对象)的方法。 一些框架通过使用@Inject
注释对字段或构造函数进行注释来实现此目的,但是还有其他方法。 例如, Koin使用Kotlin的内置语言功能来确定实现。 Inject
意味着依赖项必须由DI框架处理。 该代码将如下所示:
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
其次,框架允许您确定如何提供每个依赖项,并且这在单独的文件中进行。 大概看起来像这样(请记住,这只是一个示例,可能因框架而异):
class OurThirdPartyGuy { fun provideClassC(){ return ClassC()
因此,如您所见,每个函数负责处理一个依赖项。 因此,如果我们需要在应用程序中的某处使用ClassA
,则会发生以下情况:我们的DI框架通过调用ClassC
,将其传递给provideClassB
并接收ClassB
的实例来创建ClassC
类的一个实例,该实例被传递给provideClassA
,结果就是创建了ClassA
。 这几乎是魔术。 现在,让我们检查第三种方法的优缺点。
好处
- 一切都尽可能简单。 依赖类和提供依赖的类都清晰,简单。
- 类是松散耦合的,很容易被其他类替换。 假设我们要用
ClassC
替换ClassC
,这是ClassC
的子类。 为此,您只需ClassC
如下所示更改提供者代码,并且无论在何处使用ClassC
,新版本都将自动使用:
fun provideClassC(){ return AssumeClassC() }
请注意,应用程序内部的任何代码都不会更改,只会更改provider方法。 似乎没有什么比这更简单,更灵活了。
- 令人难以置信的可测试性。 您可以在测试过程中轻松地将依赖项替换为测试版本。 实际上,在进行测试时,依赖项注入是您的主要帮助者。
- 改进代码结构 该应用程序有一个单独的地方进行依赖项处理。 因此,该应用程序的其余部分可以仅专注于其功能,而不会与依赖项重叠。
缺点
- DI框架具有一定的入门门槛,因此项目团队需要花费时间并对其进行研究,然后才能有效地使用它。
结论
- 没有DI的依赖关系处理是可能的,但是它可能导致应用程序崩溃。
- DI只是一个有效的想法,据此可以处理依赖类之外的依赖。
- 在应用程序的某些部分中使用DI最为有效。 许多框架对此做出了贡献。
- DI不需要框架和库,但是它们可以提供很多帮助。
在本文中,我试图解释使用依赖注入的概念的基础知识,并列出了使用此思想的原因。 您可以探索更多资源,以了解有关在自己的应用程序中使用DI的更多信息。 例如,我们的Android专业课程的高级部分中的单独部分专门针对此主题。