依赖倒置原则、控制反转和依赖注入的绝对初学者教程






4.92/5 (420投票s)
在本文中,我们将讨论依赖倒置原则、控制反转和依赖注入。
引言
在本文中,我们将讨论依赖倒置原则、控制反转和依赖注入。我们将首先看依赖倒置原则。然后我们将看到如何使用控制反转来实现依赖倒置原则,最后我们将探讨依赖注入是什么以及如何实现它。
背景
在我们开始讨论依赖注入(DI)之前,我们首先需要理解DI要解决的问题。要理解这个问题,我们需要了解两件事。首先是依赖倒置原则(DIP),其次是控制反转(IoC)。让我们从DIP开始讨论,然后我们将讨论IoC。一旦我们讨论了这两点,我们将能够更好地理解依赖注入,因此我们将详细介绍依赖注入。然后最后我们将讨论如何实现依赖注入。
依赖倒置原则
依赖倒置原则是一项软件设计原则,它为我们提供了编写松耦合类的指导。根据依赖倒置原则的定义
- 高级模块不应依赖于低级模块。两者都应依赖于抽象。
- 抽象不应依赖于细节。细节应依赖于抽象。
这个定义是什么意思?它试图传达什么?让我们通过例子来理解这个定义。几年前,我参与编写了一个需要在Web服务器上运行的Windows服务。该服务的唯一职责是在IIS应用程序池出现问题时将消息记录到事件日志中。所以我们团队最初所做的是创建了两个类。一个用于监视应用程序池,另一个用于将消息写入事件日志。我们的类如下所示
class EventLogWriter
{
public void Write(string message)
{
//Write to event log here
}
}
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
EventLogWriter writer = null;
// This function will be called when the app pool has problem
public void Notify(string message)
{
if (writer == null)
{
writer = new EventLogWriter();
}
writer.Write(message);
}
}
乍一看,上述类设计似乎已足够。代码看起来完美无缺。但上述设计存在一个问题。这种设计违反了依赖倒置原则。即,高级模块
依赖于AppPoolWatcher
EventLogWriter
,这是一个具体的类而不是一个抽象。这有什么问题吗?好吧,让我告诉你我们收到的下一个服务需求,问题就会变得非常清楚。
我们收到的下一个服务需求是针对某些特定错误向网络管理员的电子邮件ID发送电子邮件。那么,我们该如何做呢?一种想法是创建一个用于发送电子邮件的类,并在AppPoolWatcher
中保留其句柄,但随时我们只会使用一个对象,即EventLogWriter
或EmailSender
。
当我们需要选择性地执行更多操作,例如发送短信时,问题会变得更糟。然后,我们将不得不拥有另一个类,其实例将保存在AppPoolWatcher
内部。依赖倒置原则说,我们需要以一种方式解耦这个系统,使得高级模块(在本例中为AppPoolWatcher
)依赖于一个简单的抽象并使用它。然后,该抽象将映射到执行实际操作的某个具体类。(接下来我们将看到如何做到这一点)
控制反转
依赖倒置是一项软件设计原则,它只是说明了两个模块应该如何相互依赖。现在的问题来了,我们究竟要如何做到这一点?答案是控制反转。控制反转是我们让高级模块依赖于抽象而不是低级模块具体实现的实际机制。
所以,如果我要在上述问题场景中实现控制反转,第一件事就是创建一个高级模块将依赖的抽象。所以,让我们创建一个接口,它将提供抽象来处理从AppPoolWacther
接收到的通知。
public interface INofificationAction
{
public void ActOnNotification(string message);
}
现在,让我们更改高级模块,即AppPoolWatcher
,使其使用此抽象而不是低级具体类。
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
INofificationAction action = null;
// This function will be called when the app pool has problem
public void Notify(string message)
{
if (action == null)
{
// Here we will map the abstraction i.e. interface to concrete class
}
action.ActOnNotification(message);
}
}
那么我们的低级具体类将如何改变?这个类如何符合抽象,即我们需要在这个类中实现上述接口:
class EventLogWriter : INofificationAction
{
public void ActOnNotification(string message)
{
// Write to event log here
}
}
所以,如果我现在需要发送电子邮件和短信的具体类,这些类也将实现相同的接口。
class EmailSender : INofificationAction
{
public void ActOnNotification(string message)
{
// Send email from here
}
}
class SMSSender : INofificationAction
{
public void ActOnNotification(string message)
{
// Send SMS from here
}
}
所以最终的类设计将是:

因此,我们所做的是,我们已经反转了控制以符合依赖倒置原则。现在我们的高级模块仅依赖于抽象,而不是低级具体实现,这正是依赖倒置原则所说的。
但仍然有一个遗漏的部分。当我们查看AppPoolWatcher
的代码时,我们可以看到它正在使用抽象,即接口,但我们到底在哪里创建具体类型并将其分配给这个抽象呢?为了解决这个问题,我们可以做一些事情,比如:
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
INofificationAction action = null;
// This function will be called when the app pool has problem
public void Notify(string message)
{
if (action == null)
{
// Here we will map the abstraction i.e. interface to concrete class
writer = new EventLogWriter();
}
action.ActOnNotification(message);
}
}
但是我们又回到了起点。具体类的创建仍然在高级类内部。我们不能完全解耦它,以便即使我们添加了从INotificationAction
派生的新类,我们也不必更改此类。
这正是依赖注入发挥作用的地方。所以现在是时候详细研究依赖注入了。
依赖注入
现在我们知道了依赖倒置原则,并且看到了实现依赖倒置原则的控制反转方法论,依赖注入主要是为了将具体实现注入到使用抽象(即接口)的类中。依赖注入的主要思想是减少类之间的耦合,并将抽象和具体实现的绑定移出依赖类。
依赖注入有三种方式。
- 构造函数注入
- 方法注入
- 属性注入
构造函数注入
在这种方法中,我们将具体类的对象传递到依赖类的构造函数中。所以,要实现这一点,我们需要在依赖类中有一个构造函数,它将接受具体类的对象并将其分配给该类正在使用的接口句柄。所以,如果我们想为我们的AppPoolWatcher
类实现这个
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
INofificationAction action = null;
public AppPoolWatcher(INofificationAction concreteImplementation)
{
this.action = concreteImplementation;
}
// This function will be called when the app pool has problem
public void Notify(string message)
{
action.ActOnNotification(message);
}
}
在上面的代码中,构造函数将接受具体类的对象并将其绑定到接口句柄。所以,如果我们想将EventLogWriter
的具体实现传递到这个类中,我们只需要
EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher(writer);
watcher.Notify("Sample message to log");
现在,如果我们希望这个类发送电子邮件或短信,我们所要做的就是在AppPoolWatcher
的构造函数中传递相应类的对象。当用户知道依赖类的实例将在其整个生命周期中使用相同的具体类时,此方法很有用。
方法注入
在构造函数注入中,我们看到依赖类将在其整个生命周期中使用相同的具体类。现在,如果我们需要在每次调用方法时传递不同的具体类,我们必须仅在方法中传递依赖项。
因此,在方法注入方法中,我们将具体类的对象传递给实际调用操作的依赖类的方法。所以,要实现这一点,我们需要操作函数也接受具体类的对象作为参数,并将其分配给该类正在使用的接口句柄,并调用操作。所以,如果我们想为我们的AppPoolWatcher
类实现这个
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
INofificationAction action = null;
// This function will be called when the app pool has problem
public void Notify(INofificationAction concreteAction, string message)
{
this.action = concreteAction;
action.ActOnNotification(message);
}
}
在上面的代码中,操作方法,即Notify
,将接受具体类的对象并将其绑定到接口句柄。所以,如果我们想将EventLogWriter
的具体实现传递到这个类中,我们只需要
EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
watcher.Notify(writer, "Sample message to log");
现在,如果我们希望这个类发送电子邮件或短信,我们所要做的就是在AppPoolWatcher
的调用方法,即上面示例中的Notify
方法中传递相应类的对象。
属性注入
现在我们已经讨论了两种情况,在构造函数注入中,我们知道依赖类将在其整个生命周期中使用一个具体类。第二种方法是使用方法注入,我们可以将具体类的对象本身传递到操作方法中。但是,如果选择具体类和调用方法的职责在不同的地方怎么办?在这种情况下,我们需要属性注入。
因此,在这种方法中,我们将具体类的对象通过依赖类公开的setter属性传递。所以,要实现这一点,我们需要在依赖类中有一个setter属性或函数,它将接受具体类的对象并将其分配给该类正在使用的接口句柄。所以,如果我们想为我们的AppPoolWatcher
类实现这个
class AppPoolWatcher
{
// Handle to EventLog writer to write to the logs
INofificationAction action = null;
public INofificationAction Action
{
get
{
return action;
}
set
{
action = value;
}
}
// This function will be called when the app pool has problem
public void Notify(string message)
{
action.ActOnNotification(message);
}
}
在上面的代码中,Action属性的setter将接受具体类的对象并将其绑定到接口句柄。所以,如果我们想将EventLogWriter
的具体实现传递到这个类中,我们只需要
EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
// This can be done in some class
watcher.Action = writer;
// This can be done in some other class
watcher.Notify("Sample message to log");
现在,如果我们希望这个类发送电子邮件或短信,我们所要做的就是在AppPoolWatcher
类公开的setter中传递相应类的对象。此方法适用于选择具体实现和调用操作的职责在不同的地方/模块中完成的情况。
在不支持属性的语言中,有一个单独的函数用于设置依赖项。此方法也称为setter注入。在此方法中需要注意的重要一点是,有可能有人创建了依赖类,但没有人设置了具体类的依赖项。在这种情况下,如果我们尝试调用操作,那么我们应该有一个默认依赖项映射到依赖类,或者有一个机制来确保应用程序能够正常运行。
关于IoC容器的说明
在实现依赖注入时,构造函数注入是最常用的方法。如果我们需要在每次方法调用时传递不同的依赖项,那么我们就使用方法注入。属性注入的使用频率较低。
我们讨论过的所有三种依赖注入方法都可以,如果我们只有一个依赖级别。但是,如果具体类也依赖于其他抽象怎么办?因此,如果我们存在链式和嵌套依赖,实现依赖注入将变得相当复杂。这时我们就可以使用IoC容器。当我们存在链式或嵌套依赖时,IoC容器将帮助我们轻松地映射依赖项。
关注点
在本文中,我们讨论了依赖倒置原则(它是SOLID面向对象原则的D部分)。我们还讨论了如何使用控制反转来实现依赖倒置,最后我们看到了依赖注入如何帮助创建松耦合类以及如何实现依赖注入。本文是从初学者的角度撰写的。希望这篇文章内容丰富。
历史
- 2013年7月3日:初稿。
- 2013年7月9日:修复了拼写错误和一些语法错误。