如何使用Kotlin的反转控制原理

204 阅读12分钟

使用Kotlin的反转控制原理

反转控制(IoC)是一项软件设计原则,它有助于在面向对象的编程中实现反向控制。这里的控制是指一个类在其主要责任之外的任何额外责任。

简介

IoC控制一个对象的创建,以及一个应用程序在容器或框架中的流动。IoC将创建和管理对象的控制权从程序员手中倒置到了容器中。这个容器负责管理应用程序中对象的创建和生命周期。IoC通过消除创建对象的责任来帮助我们创建大型系统。

想象一下,假设你有一个专门制作蛋卷的面包店。没有鸡蛋的蛋卷是什么?所以鸡蛋在制作蛋卷的过程中是必不可少的。你可能会问,为什么是蛋卷?嗯,你可以说这是普通的裙带关系。

那么,我们如何获得一个遵守IoC原则的面包店呢?好吧,与其拥有一个家禽养殖场并让自己参与所有与家禽有关的工作,不如订购鸡蛋并将其送到你的手中。这一次,你不必担心小鸡和鸡蛋是否健康或使用合适的动物饲料。有了这个新设计,你可以专注于制作蛋卷,从而使烘焙过程更加有效。

这就是IoC背后的大思想:分离关注点。反转控制使消费者有可能对软件有更多的控制。它给了它更多的灵活性和自由来选择其他选项。

前提条件

在我们开始之前,你应该具备。

  • Kotlin编程语言的基本知识。
  • 安装了一个IDE。最好是[IntelliJ]。

使用IoC的一些好处

  • 在运行时很容易在某个特定类的不同实现之间进行切换。
  • 它提高了程序的模块化程度。
  • 它可以管理一个对象的生命周期和配置。一些对象可以是单子,而我们可以根据请求创建其他对象。

在我们看一些代码之前,这里有一些你应该牢记的东西。

1.耦合性

耦合性是衡量软件模块之间的紧密程度。它是指对一个组件所做的改变迫使其他组件或模块也需要改变的程度。

紧耦合是指组件A的改变需要组件B的改变。松耦合是指组件A和B是独立的。因此,组件A的改变不会影响到组件B。松散耦合通常是可测试、结构良好、可维护和可读的软件的标志。严密的耦合会导致代码的脆弱性和代码的僵化。

代码的脆弱性是指每次有变化时,软件在很多地方都会损坏的可能性。

代码僵化是指对软件进行修改的困难程度。

松散耦合的优点

  • 开发人员可以快速发展松散耦合的软件,因为它鼓励在不破坏现有代码的情况下进行许多改变。
  • 松散耦合增强了软件的敏捷性,因为它促进了迭代,这意味着人们可以快速添加一个新的特征或功能。
  • 松散耦合减少了技术债务。

2.2.抽象性

当你称某个东西为抽象的时候,它意味着它是不完整的或没有明确定义的。抽象是一种隐藏实现细节的编程方法,只向用户揭示功能(相关操作)。它是面向对象编程的基本概念之一。

优点

  • 它简化了编程的复杂性。
  • 它促进了相关类和对象的分组。

3.单一责任原则

这个原则指出,每个函数、类或模块都应该有一个单一的改变理由,并且只有一个责任。

优点

  • 它使代码易于理解、修正和维护。
  • 类的耦合度更低,对变化的适应性更强。
  • 更加可测试的设计。

4.依赖反转原则

依赖反转原则(DIP)使我们能够创建松散耦合的系统。使它们易于改变和维护。

DIP指出。

  • 高级模块不应该依赖低级模块。它们应该依靠抽象。
  • 抽象不应该依赖于细节,但细节应该依赖于抽象。

高级模块是为解决问题和用例而编写的模块。它们是抽象的,是对业务领域(业务逻辑)的映射。他们关注的是软件应该做什么,而不是应该如何做。

低级模块是执行业务策略(逻辑)所需的实施细节。它们是一个系统的管道或内部结构,它们告诉我们系统(软件)应该如何完成各种任务。它们往往是非常具体的。

dependency flow

对于A类的工作,它依赖于两个低级别的模块。这不符合依赖性反转原则,因为A类是一个高级模块,它依赖于B类和C类,而B类是低级模块。

为了使这段代码遵守DIP原则,提取低级模块的接口。提取接口将给我们提供类似这样的东西。

dependency flow

这个抽象可以是一个接口或一个抽象类。现在我们可以回到主要焦点-控制反转。

控制反转只提供设计准则,而不是实现细节。一个设计原则不受任何编程语言的限制。你可以用你喜欢的任何方式来实现它。然而,设计模式推荐一个实际的实现。设计模式更像是在特定情况下对一个问题的可重复使用的解决方案。

因此,我们可以在面向对象编程中以多种方式应用IoC。其中一些是。

  • 依赖性注入模式。
  • 策略设计模式。
  • 服务定位器模式,以及其他许多方式。

让我们来了解一下依赖注入和策略设计模式。

1.依赖性注入(DI)

依赖性注入(DI)是一种主要用于依赖反转原则的设计模式。它使依赖对象在一个类之外被创建成为可能。然后它将这些对象提供给类。

例如,我们有一个类LoginManager ,它依赖于UserRepository 的实现。

class LoginManager {
   val userRepository: UserRepository = UserRepositoryImpl()
}

我们可以看到,LoginManagerUserRepository. 之间存在着高度的依赖性LoginManager 直接依赖于UserRepository ,因为UserRepository 处理它的创建。这违反了依赖性反转和单一责任原则。其结果是LoginManager 和 之间的紧密耦合。UserRepository.

有几种方法可以解决这个问题。

  • 使用公共设置器:不推荐这样做,因为它可能会使对象处于未初始化的状态。
class LoginManager {
    lateinit var userRepository: UserRepository
}

fun main(args: Array) {
    val loginManager = LoginManager()
    loginManager.userRepository = UserRepositoryImpl()
}
  • **在组件的构造函数中声明所有的依赖关系。**这将看起来像这样。
class LoginManager (val userRepository: UserRepository){

    //Do something...
}

fun main(args: Array) {

// In the caller function, create an instance of UserRepository
    val userRepository = UserRepositoryImpl()

// use the UserRepository instance to construct an instance of LoginManager
    val loginManager = LoginManager(userRepository)
}

LoginManager 现在有一个构造函数,接受 抽象作为参数。 现在可以以任何方式使用 ,因为 现在是 类中的一个字段。UserRepository LoginManager UserRepository UserRepository LoginManager

LoginManager 不再负责创建它的依赖关系。在这种情况下,现在是调用者的工作--主函数。主函数提供了所需的依赖关系,也就是 这样,我们可以有不同的 的实现,并在其他情况下快速测试它。UserRepositoryImpl. LoginManager

让我们来看看一个更复杂的场景。

dependency flow

我们可以看到,A类和B类没有依赖关系。类C依赖于类A,类D依赖于类B,而类E同时依赖于类C和类D。如果我们想在类E上调用一个方法或创建一个实例,我们将不得不创建它所有需要的依赖关系。我们需要按照特定的顺序创建A、B、C和D类的具体实例。

首先,我们将创建A类和B类的实例,因为它们没有依赖关系。接下来,我们会创建类C和D的实例,因为我们有它们各自的依赖关系A和B的实例。最后,我们可以创建类E的实例。

呜呼!对吗?这只是一个简单的例子,只有五个类。想象一下在现实生活中的项目会发生什么。可能有数百个类,每次都要被实例化。这将是一个巨大的冗余工作的负荷。依赖注入是实现松散耦合类所需的一项伟大技术。但我们在这里可以看到,手动进行依赖注入并不是一个好主意。

另外,如果你想考虑这些对象的生命周期,假设你想让C类成为一个单子,并在每个请求中创建D。手动处理这个请求将涉及大量的逻辑和多余的代码。这就是手动依赖注入的作用。通过手动DI,你可以创建你自己的依赖性容器类。这个容器将容纳你的应用程序的依赖关系。

IoC或DI容器现在控制对象的创建和生命周期。这样一来,你就不必再在每次需要这些对象的时候创建它们的实例了。Dagger和hilt可以将这个过程自动化,并为你生成必要的代码。

IoC容器主要用于一个应用程序中的对象,如。

  • 服务。
  • 数据访问。
  • 控制者。

最好不要在容器中创建实体、数据传输或价值对象的实例。你可以在需要的时候随时创建它们的新实例,从架构的角度来看,这也是可以的。

2.策略设计模式

策略模式是一种行为设计模式。它使得在运行时改变一个类或其算法的行为成为可能。

假设我们有一个名为LaundryBot 的接口,它将有洗涤、干燥和折叠的方法。不同的织物类型将使用这个接口。一个例子是用于洗涤羊绒和丝绸的CashmereLaundryBotSilkLaundryBot

我们应该牢记。

  • 所有提供给洗衣店的物品都可以手洗或机洗。
  • 提供给洗衣店的所有物品都可以晒干或用烘干机烘干。
  • 所有物品都以相同的方式进行折叠,这意味着折叠将是LaundryBot 界面中的一个默认方法。
interface LaundryBot{
   fun wash()
   fun dry()
   fun fold(){
       //fold items
   }
}

我们说过,一个物品既可以手洗,也可以机洗,既可以晒干,也可以用烘干机烘干(机洗),这意味着任何LaundryBot 类都会有这些方法,因此,如果我们有一个DenimLaundryBot ,和一个CottonLaundryBot ,它们的washdry 实现LaundryBot ,都会有同一段代码。这是因为牛仔布和棉花都会被机洗和机烘干。

我们如何处理这种代码的重复?

现在,这就是策略模式出现的地方。策略模式封装了一组在运行时可以互换的算法。我们如何在LaundryBot 中实现这一点?要做到这一点,我们将从LaundryBot 接口中删除Wash()dry() 方法。然后我们将它们变成名为 "洗涤 "和 "干燥 "的接口。这些接口将分别有方法Wash()dry()

interface Wash{
   fun wash()
}
interface Dry{
   fun dry()
}

接下来,我们提供这些接口的具体实现。这样一来,我们就可以为每种情况下的洗涤和干燥封装不同的行为了。为了做到这一点,创建名为MachineWashHandWash 的类,它们继承自wash 。以同样的方式,MachineDrySunDry 将继承自 Dry。

object MachineWash: Wash{
   override fun wash() {
       //Perform Machine Wash
   }
}
 
object HandWash: Wash{
   override fun wash() {
       //Perform Hand Wash
   }
}
 
object SunDry: Dry{
   override fun dry() {
       //Perform Sun Dry
   }
}
 
object MachineDry: Dry{
   override fun dry() {
       //Perform Machine Dry
   } 
}

我们已经成功地创建了一个水洗和干燥的算法系列。

dependency flow

dependency flow

我们已经创建了策略;现在,是时候让我们看看它是如何工作的。记住,我们从LaundryBot 接口中删除了Wash()dry() 方法。这一次,我们不再有Wash()dry() ,而是创建了Wash 和 Dry类型的字段。

interface LaundryBot {
   val wash: Wash
   val dry: Dry
   fun fold() {
       //fold items
   }
}

这样,任何继承自LaundryBot 的类都可以从算法系列中选择它想要的特定行为。让我们看看这是如何工作的。假设我们想创建一个CashmereLaundryBot 。我们选择什么行为的洗涤和干燥?建议用手洗羊绒,以避免撕裂和打结;因此,我们将使用HandWash行为。

对于烘干,羊绒不适合用机器烘干,因为高温会使羊绒毛囊收缩或损坏。那么,用于羊绒的干燥行为是SunDry。让我们看看这在代码中是什么样子的。

class CashmereLaundryBot: LaundryBot{
   override val wash: Wash = HandWash
   override val dry: Dry = SunDry
 
}

看到策略模式是如何使改变这个类的行为变得容易的吗?很好!

总结

反转控制是一种提高代码模块化、减少代码重复和简化测试的实用方法。尽管在开发可重用的库时它是有益的,但它并不适合所有的用例。

必须知道什么时候利用控制权的灵活性和自由度,什么时候不利用。这是一篇很长的文章,但我肯定希望这篇文章能像帮助我一样帮助你。