An Absolute Beginner's Tutorial on Dependency Inversion Principle, Inversion of Control and Dependency Injection(关于依赖倒置原则,控制反转和依赖注入的绝对的新手教程)

原文传送门

翻译方式: 中英文对照, 意译

In this article we will talk about the Dependency Inversion Principle, Inversion of Control and Dependency Injection.
本文我们会谈一下依赖倒置原则,反转控制和依赖注入。

Introduction 介绍

In this article we will talk about the Dependency Inversion Principle, Inversion of Control and Dependency Injection.
本文我们会谈一下依赖倒置原则,反转控制和依赖注入。

We will start by looking at the dependency inversion principle. We will then see how we can use inversion of control to implement dependency inversion principle and finally we will look at what dependency injection is and how can it be implemented.
我们从依赖倒置原则开始,然后看下如何使用控制反转来实现依赖倒置原则,最后我们看一下依赖注入是什么以及如何实现的。

Background 背景

Before we start talking about Dependency Injection(DI), we first need to understand the problem that DI solves.
在讲述依赖注入之前, 我们首先需要了解依赖注入所解决的问题

To understand the problem, we need to know two things.
要明白问题, 我们需要知道两个概念。

First dependency Inversion Principle(DIP) and second Inversion of Controls(IoC).
一是依赖倒置原则(DIP), 二是 控制反转(IoC).

let us start our discussion with DIP, then we will talk about IoC.
我们首先谈下 DIP, 然后是 IoC。

Once we have discussed these two, we will be in a better position to understand Dependency Injection, so we will look at dependency injection in details.
一旦我们明白了这两个概念, 依赖注入就容易理解了, 因此我们详细地看一下依赖注入。

Then finally we will discuss see how can we implement Dependency injection.
最后, 我们谈下如何实现依赖注入。

Dependency Inversion Principle 依赖倒置原则

Dependency inversion principle is a software design principle which provides us the guidelines to write loosely coupled classes.
依赖倒置原则是一种软件设计原则, 可以为我们写松散耦合的类提供指引。

According to the definition of Dependency inversion principle:
根据依赖倒置原则的定义:

1、High-level modules should not depend on low-level modules. Both should depend on abstractions.
高层模块不应该依赖于底层模块。它们都应当依赖于抽象。

2、Abstractions should not depend upon details. Details should depend upon abstractions.
抽象不应当依赖于细节。细节应当依赖于抽象。

What does this definition mean? What is it trying to convey?
这个定义是什么意思? 它所要传达的是什么?

let us try to understand the definition by looking at examples.
让我们通过几个例子来理解这个定义。

A few years back I was involved in writing a windows service which was supposed to run on a Web server.
几年前, 我参与了编写一项应当运行在 Web server 的 windows 服务。

The sole responsibility of this service was to log messages in event logs whenever there is some problem in the IIS application Pool.
该服务的唯一职责是: 无论何时, 只要 IIS 应用程序池出现问题, 就会在事件日志中记录消息。

So what our team has done initially that we created two classes.
我们的团队刚开始创建了2个类。

One for monitoring the Application Pool and second to write the messages in the event log.
一个用于监控应用程序池, 另一个用于在事件日志中写消息。

Our classes looked like this:
我们的类看起来是这样子的:

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;

    // 这个函数会在app pool 出现问题时调用
    public void Notify(string message)
    {
        if (writer == null)
        {
            writer = new EventLogWriter();
        }
        writer.Write(message);
    }
}

From the first look, the above class design seems to be sufficient.It looks perfectly good code.
一眼看去, 上面的设计的类看起来已经足够了。看上去是很好的代码。

But there is a problem in the above design.
但是上面的这个设计有个问题。

This design violates the dependency inversion principle. i.e. the high level module <code>AppPoolWatcher depends on EventLogWriter which is a concrete class and not an abstraction.
这个设计破坏了依赖倒置原则。也就是说, 高层模块 AppPoolWatcher 依赖于 EventLogWriter, 而 EventLogWriter 是个具体的类并不是抽象

How is it a problem?
这有什么问题呢?

Well let me tell you the next requirement we received for this service and the problem will become very clearly visible.
好吧, 当我告诉你我们接收到的这个服务的下一个需求后, 这个问题就会显而易见。

The next requirement we received for this service was to send email to network administrator's email ID for some specific set of error.
我们所接受的下一个需求是: 当发生一些特定的错误,向网络管理员的电子邮件ID 发送邮件。

Now, how will we do that?
现在, 我们怎么办呢?

One idea is to create a class for sending emails and keeping its handle in the AppPoolWatcher but at any moment we will be using only one object either EventLogWriter or EmailSender.
一个想法是创建一个类用于发送邮件并把它的句柄放在 AppPoolWatcher 中,但是任何时刻, 我们只能用一个对象, 不是 EventLogWriter 就是 EmailSender

The problem will get even worse when we have more actions to take selectively, like sending SMS.
当我们有选择地采取更多动作,比如发送短信,问题就会变得更糟。

Then we will have to have one more class whose instance will be kept inside the AppPoolWatcher.
我们将不得不再添加一个类, 类的实例保存在 AppPoolWatcher 中。

The dependency inversion principle says that we need to decouple this system in such a way that the higher level modules i.e. the AppPoolWatcher in our case will depend on a simple abstraction and will use it.
依据依赖倒置原则, 我们需要将这个系统解耦,高层模块(即 AppPoolWatcher) 应当依赖于简单的抽象并使用它。

This abstraction will in turn will be mapped to some concrete class which will perform the actual operation. (Next we will see how this can be done)
该抽象会映射到一些具体的类, 由这些类进行真正的操作.(稍后我们会看到这是如何做到的)

Inversion of Control 控制反转

Dependency inversion was a software design principle, it just states that how two modules should depend on each other.
依赖倒置是一种软件设计原则,它只是说明两个模块之间应该如何相互依赖。

Now the question comes, how exactly we are going to do it?
现在问题来了,我们到底要怎么做呢?

The answer is Inversion of control.
答案是控制反转。

Inversion of control is the actual mechanism using which we can make the higher level modules to depend on abstractions rather than concrete implementation of lower level modules.
使用控制反转机制, 我们可以让高层模块依赖于抽象而不是底层模块的具体实现。

So if I have to implement inversion of control in the above mentioned problem scenario, the first thing we need to do is to create an abstraction that the higher levels will depend on.
因此, 如果必须在上述问题场景中实现控制反转,我们需要做的第一件事就是创建高层模块所依赖的抽象。

So let us create an interface that will provide the abstraction to act on the notification received from AppPoolWacther.
因此, 我们创建一个接口, 这个接口会提供对 AppPoolWatcher 所发送的通知的操作的抽象。

public interface INofificationAction
{
    public void ActOnNotification(string message);
}

Now let us change our higher level module i.e. the AppPoolWatcher to use this abstraction rather than the lower level concrete class.
现在, 我们修改下高层模块,即 AppPoolWatcher, 使用这个抽象, 而不是底层的具体类。

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs 
    // EventLogWriter 写日志的句柄
    INofificationAction action = null;

    // 这个函数会在app pool 出现问题时调用
    public void Notify(string message)
    {
        if (action == null)
        {
            // Here we will map the abstraction i.e. interface to concrete class
            // 这里 我们会将抽象(即接口)映射到具体的类
        }
        action.ActOnNotification(message);
    }
}

So how will our lower level concrete class will change? how will this class conform to the abstraction i.e. we need to implement the above interface in this class:
那么,我们的底层具体类将如何改变呢?这个类如何符合抽象,即我们需要在这个类中实现上述接口:

class EventLogWriter : INofificationAction
{   
    public void ActOnNotification(string message)
    {
        // Write to event log here
    }
}

So now if I need to have the concrete classes for sending email and sms, these classes will also implement the same interface.
这样,如果我需要有发送电子邮件和短信的具体类,这些类也将实现相同的接口。

class EmailSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // Send email from here
    }
}

class SMSSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // Send SMS from here
    }
}

So the final class design will look like:
那么, 最终的类设计看起来会是这样子的:


So what we have done here is that, we have inverted the control to conform to dependency inversion principle.
我们在这里所做的是, 为和依赖倒置原则相一致, 我们反转了控制。

Now our high level modules are dependent only on abstractions and not the lower level concrete implementations, which is exactly what dependency inversion principle states.
现在我们的高层模块只依赖于抽象而不依赖于底层的具体实现,这正是依赖倒置原则所陈述的。

But there is still one missing piece.
但是仍然有一块缺失。

When we look at the code of our AppPoolWatcher, we can see that it is using the abstraction i.e. interface but where exactly are we creating the concrete type and assigning it to this abstraction.
当我们看一下我们的 AppPoolWatcher 代码, 我们可以看到它使用了抽象,也就是接口, 但是我们在哪里创建具体的类,并把它分配给这个抽象呢。

To solve this problem, we can do something like:
为了解决这个问题, 我们可以这样做:

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);
    }
}

But we are again back to where we have started.
但是我们又一次回到了起点。

The concrete class creation is still inside the higher level class.
具体类的创建仍然在高层类中。

Can we not make it totally decoupled so that even if we add new classes derived from INotificationAction, we don't have to change this class.
我们能不能让它完全解耦, 这样即使我们添加了从INotificationAction 派生出的新类, 我们也不用改变这个类。

This is exactly where Dependency injection comes in picture.
这正是依赖注入的所在。

So its time to look at dependency injection in detail now.
现在是时候去详细地看一下依赖注入了。

Dependency Injection 依赖注入

Now that we know the dependency inversion principle and have seen the inversion of control methodology for implementing the dependency inversion principle, Dependency Injection is mainly for injecting the concrete implementation into a class that is using abstraction i.e. interface inside.
现在我们已经了解了依赖倒置原则,并且已经看到了为实现依赖倒置原则而进行的控制方法的反转,依赖注入主要是将具体实现注入到一个使用抽象的类中,即接口内部。

The main idea of dependency injection is to reduce the coupling between classes and move the binding of abstraction and concrete implementation out of the dependent class.
依赖注入的主要思想是减少类之间的耦合,将抽象和具体实现的绑定移出依赖类。

Dependency injection can be done in three ways.
依赖注入可以用三种方式实现。

1、Constructor injection
构造器注入

2、Method injection
方法注入

3、Property injection
属性注入

Constructor Injection 构造器注入

In this approach we pass the object of the concrete class into the constructor of the dependent class.
在这种方式中,我们将具体类的对象传给依赖类的构造函数

So what we need to do to implement this is to have a constructor in the dependent class that will take the concrete class object and assign it to the interface handle this class is using.
因此,我们需要做的是在依赖类中放一个构造函数,它将使用具体的类对象并将其赋值给这个类使用的接口。

So if we need to implement this for our AppPoolWatcher class:
因此, 如果我们需要为我们的 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);
    }
}

In the above code, the constructor will take the concrete class object and bind it to the interface handle.
上述代码中, 构造器将使用具体类对象,并把它绑定到接口句柄。

So if we need to pass the EventLogWriter's concrete implementation into this class, all we need to do is
如果我们要把 EventLogWriter 的具体实现传入这个类中, 我们所要做的是

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher(writer);
watcher.Notify("Sample message to log");

Now if we want this class to send email or sms instead, all we need to do is to pass the object of the respective class in the AppPoolWatcher's constructor.
现在, 如果我们想让这个类发送邮件或者是短信, 我们要做的就是把各自类的对象传入到 AppPoolWatcher 的构造器中。

This method is useful when we know that the instance of the dependent class will use the same concrete class for its entire lifetime.
这个方法非常有用, 当我们知道依赖类的实例将在其整个生命周期中会使用相同的具体类。

Method Injection 方法注入

In constructor injection we saw that the dependent class will use the same concrete class for its entire lifetime.
在构造器注入中, 我们可以看到依赖类会在其整个生命周期使用相同的具体类。

Now if we need to pass separate concrete class on each invocation of the method, we have to pass the dependency in the method only.
现在,如果我们需要在方法的每次调用中传入单独的具体类,我们必须只在方法中的传递依赖。

So in method injection approach we pass the object of the concrete class into the method the dependent class which is actually invoking the action.
因此, 在方法注入方式中, 我们传递 具体类的对象到 事实上正调用操作的依赖类的方法中。

So what we need to do to implement this is to have the action function also accept an argument for the concrete class object and assign it to the interface handle this class is using and invoke the action.
因此, 要实现这种方式,我们所要做的就是让动作函数也接收具体类对象参数, 并把它赋值给这个类正使用的句柄,然后调用该操作。

So if we need to implement this for our AppPoolWatcher class:
因此, 我们需要为我们的 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);
    }
}

In the above code the action method i.e. Notify will take the concrete class object and bind it to the interface handle.
上述代码中 动作方法 即 Notify 会使用具体类对象并且把它绑定到接口句柄。

So if we need to pass the EventLogWriter's concrete implementation into this class, all we need to do is
所以, 如果我们需要传递 EventLogWriter的具体实现到这个类中, 我们要做的是:

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
watcher.Notify(writer, "Sample message to log");

Now if we want this class to send email or sms instead, all we need to do is to pass the object of the respective class in the AppPoolWatcher's invocation method i.e. Notify method in the above example.
现在, 如果我们要让这个类发送邮件或者是短信, 我们要做的就是把各自类的对象传入到 AppPoolWatcher 的调用方法, 即 上例中的 Notify 方法。

Property Injection 属性注入

Now we have discussed two scenarios where in constructor injection we knew that the dependent class will use one concrete class for the entire lifetime.
现在我们已经讨论了两种场景, 在构造器注入中我们了解了依赖类在整个生命期间会使用一个具体类。

The second approach is to use the method injection where we can pass the concrete class object in the action method itself.
第二种方法是使用方法注入, 我们可以把具体类的对象传入到动作方法。

But what if the responsibility of selection of concrete class and invocation of method are in separate places.
但是如果具体类的选择以及方法调用的责任在不同的地方会怎样?

In such cases we need property injection.
这种情况下我们需要属性注入。

So in this approach we pass the object of the concrete class via a setter property that was exposed by the dependent class.
在这种方法中, 我们通过依赖类所暴露的 setter 属性 传入具体类的对象。

So what we need to do to implement this is to have a Setter property or function in the dependent class that will take the concrete class object and assign it to the interface handle this class is using.
要实现这种方法, 我们需要做的是 在依赖类中添加 Setter 属性或者是函数, 它会将具体类的对象赋值给这个类使用的句柄。

So if we need to implement this for our AppPoolWatcher class:
因此, 如果我们需要为我们的 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);
    }
}

In the above code the setter of Action property will take the concrete class object and bind it to the interface handle.
上述代码中, Action 属性会使用具体类的对象并把它绑定到接口句柄。

So if we need to pass the EventLogWriter's concrete implementation into this class, all we need to do is
所以, 如果我们把 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");

Now if we want this class to send email or sms instead, all we need to do is to pass the object of the respective class in the setter exposed by AppPoolWatcher class.
现在, 如果我们想要这个类发送邮件或者是短信, 我们所要做的就是把相应类的对象 传入到AppPoolWatcher类暴露的setter

This approach is useful when the responsibility of selecting the concrete implementation and invoking the action are done in separate places/modules.
当选择具体实现和调用操作的责任在不同的地方/模块中完成时,这种方法非常有用。

In languages where properties are not supported, there is a separate function to set the dependency.
在不支持属性的语言中, 有单独的函数设置依赖。

This approach is also known as setter injection.
这种方法也成为 setter 注入。

The important thing to note in this approach is that there is a possibility that someone has created the dependent class but no one has set the concrete class dependency yet.
这种方法中需要注意的是, 有可能已经创建了依赖类但是没有设置具体类的依赖。

If we try to invoke the action in such cases then we should have either some default dependency mapped to the dependent class or have some mechanism to ensure that application will behave properly.
如果我们试图在这种情况下调用该操作, 那么我们应该将默认依赖映射到依赖类, 或者有某种机制可以确保应用程序可以正常运行。

A Note on IoC Containers 关于 IoC 容器的说明

Constructor injection is the mostly used approach when it comes to implementing the dependency injection.
在实现依赖注入时, 构造器注入是最常用的方法

If we need to pass different dependencies on every method call then we use method injection.
如果我们需要在每次方法调用的时候传递不同的依赖, 那么我们需要方法注入。

Property injection is used less frequently.
属性注入的使用频率较低。

All the three approaches we have discussed for dependency injection are ok if we have only one level of dependency.
如果我们只有一个依赖级别, 我们所谈到的依赖注入的这三种方法都是可以的。

But what if the concrete classes are also dependent of some other abstractions.
但是如果具体的类也是其他抽象的依赖该怎么办呢

So if we have chained and nested dependencies, implementing dependency injection will become quite complicated.
那么, 如果我们有链式的以及嵌套的依赖, 实现依赖注入将会变得非常复杂。

That is where we can use IoC containers.
这时, 我们就可以使用 IoC 容器。

IoC containers will help us to map the dependencies easily when we have chained or nested dependencies.
当我们使用链式的或嵌套依赖时,IoC 容器会帮助我们轻松地映射依赖关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容