65.9K
CodeProject 正在变化。 阅读更多。
Home

依赖倒置原则与依赖注入模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (86投票s)

2012年11月20日

CPOL

5分钟阅读

viewsIcon

182189

如何在没有 DI 容器的情况下使用 DI

引言

我们每天编写大量紧密耦合的代码,随着复杂度的增长,代码最终会退化成意大利面条代码,这违反了依赖倒置原则。在软件设计中,紧密耦合通常被认为是设计上的负债。当一个类明确知道另一个类的设计和实现时,它会增加一个类发生变化而破坏另一个类的风险。

另一方面,松耦合的代码可以保持可维护性和良好的设计。松耦合的好处并不总是立即可见的,但随着代码复杂度的增长,它们会随着时间的推移而显现。依赖注入模式只是实现松耦合的最佳方式。在本文中,我将尝试解释如何通过简单的方法在日常实践中使用 DI,而无需 DI 容器。

背景

Uncle Bob 的“SOLID”面向对象设计原则,“D”代表依赖倒置原则

依赖倒置原则陈述如下:

  • 高层模块不应依赖于低层模块。两者都应依赖于抽象。
  • 抽象不应依赖于细节。细节应依赖于抽象。

有时,在编写代码时很难维护 DIP。实践和经验会帮助你解决这个问题。依赖倒置原则 (DIP) 通过确保高层模块依赖于抽象而不是低层模块的具体实现,来帮助实现代码的松耦合。依赖注入模式是此原则的应用/实现。

本文的基本思想是如何使我们的代码松耦合。

Using the Code

让我们从代码开始。我们中的许多人可能已经见过(或写过)这样的代码

public class Email
{
    public void SendEmail()
    {
        // code
    }
}

public class Notification
{
    private Email _email;
    public Notification()
    {
        _email = new Email();
    }

    public void PromotionalNotification()
    {
        _email.SendEmail();
    }
}

在这里,Notification 类依赖于 Email 类。在这种情况下,通知在其构造函数内部直接创建 e-mail 实例,并且确切地知道它正在创建和使用的电子邮件类的类型。这违反了 DIP。

一个类依赖于另一个类,并且非常了解它所交互的其他类,被称为紧密耦合。当一个类明确知道另一个类的设计和实现时,它会增加一个类发生变化而破坏另一个类的风险。

如果我们想发送其他类型的通知,例如 SMS 或保存到数据库怎么办?要实现此行为,我们必须修改通知类的实现。

为了减少依赖,我们需要执行几个步骤。首先,在这两个类之间引入一个抽象层。我们可以使用接口/抽象类来表示 Notification 和 Email 之间的抽象。

public interface IMessageService
{
    void SendMessage();
}
public class Email : IMessageService
{
    public void SendMessage()
    {
        // code
    }
}
public class Notification
{
    private IMessageService _iMessageService;

    public Notification()
    {
        _iMessageService = new Email();
    }
    public void PromotionalNotification()
    {
        _iMessageService.SendMessage();
    }
}

在这里,我们引入了一个接口 IMessageService 来表示抽象,并确保 Notification 类仅调用该接口上的方法或属性。

其次,我们需要将 Email 类的创建移出 Notification。我们可以通过 DI 模式来实现这一点。

DI 是提供服务所需的所有类的行为,而不是让服务负责获取依赖类。DI 通常有三种形式:

  • 构造函数注入
  • 属性注入
  • 方法注入

通过这些注入,我们可以实现第二步。我将这些注入应用到我们紧密耦合的代码中。并使我们的代码松耦合并维护 DIP。

构造函数注入

这是最常见的依赖注入。当一个类需要一个 DEPENDENCY 的实例来工作时,我们可以通过类的构造函数来提供该 DEPENDENCY。现在让我们更改 Notification 类以支持构造函数注入

public class Notification
{
    private IMessageService _iMessageService;

    public Notification(IMessageService _messageService)
    {
        this._iMessageService = _messageService;
    }
    public void PromotionalNotification()
    {
        _iMessageService.SendMessage();
    }
}

在这段代码中,有几个好处:构造函数的实现非常简单,它减少了 Notification 需要了解的事物数量,任何想要创建 Notification 实例的代码都可以查看构造函数,并确切地知道使 Notification 正常运行所必需的事物的种类。所以,我们的代码现在是松耦合且易于维护的。

属性注入

属性注入/setter 注入是一种不太常见的依赖注入,当依赖是可选的时,它最适合使用。我们必须公开一个可写属性,允许客户端提供与类 DEPENDENCY 默认值不同的实现。我们必须更改我们的 Notification 类以应用属性注入

public class Notification
{
    public IMessageService MessageService
    {
        get;
        set;
    }
    public void PromotionalNotification()
    {
        if (MessageService == null)
        {
            // some error message
        }
        else
        {
            MessageService.SendMessage();
        }
    }
}

我们删除了构造函数,并用属性替换了它。现在我们通过属性而不是构造函数来提供依赖。在 PromotionalNotifications 方法中,我们必须检查服务依赖是否已提供,方法是检查 MessageService 的值。在这里,我们能够实现代码的松耦合。

方法注入

当 DEPENDENCY 可以在每次方法调用时变化时,你可以通过方法参数来提供它。假设我们的应用程序还将发送 Email、SMS 或将通知消息保存到数据库。我们可以使用方法注入。我们可以按以下方式编写代码

public class Email : IMessageService
{
    public void SendMessage()
    {
        // code for the mail send
    }
}
public class SMS : IMessageService
{
    public void SendMessage()
    {
        // code for the SMS send
    }
}

public class Notification
{
    public void PromotionalNotification(IMessageService _messageService)
    {
        _messageService.SendMessage();
    }
}

在这里,IMessageService 已在 Email 类和 SMS 类中实现。我们可以将不同类型的 MessageService 作为参数提供给 PromotionalNotification 方法,以相应地执行通知。所以,这里的依赖项通过不同的参数在每次方法调用时都会变化。

在像示例这样的紧密耦合场景中,我们可以应用任何这些注入来实现代码的松耦合。这取决于我们将应用哪种注入的场景。我们可以在单个场景中应用多种注入。

结论

令人惊讶的是,编写紧密耦合的代码非常容易!许多原因之一是,在向代码引入新的/附加功能时,每次使用 new 关键字时,我们都会引入紧密耦合。通过引入构造函数注入,我们可以最大限度地减少这种情况。总的来说,开发人员喜欢使用构造函数注入而不是其他两种注入。但当然,我们可以混合使用这两种,其中构造函数将是强制性的,而其他两种将是可选的。希望本文能帮助你在日常代码中开始使用 DI。

历史

  • 2012年11月20日:初始版本
© . All rights reserved.