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






4.96/5 (86投票s)
如何在没有 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日:初始版本