代码改变世界

【简译】关于依赖反转原则、控制反转和依赖注入的抽象的初学者指南

2013-10-08 20:06 by muzinian, ... 阅读, ... 评论, 收藏, 编辑

原文在此

======================================分割线====================================

介绍

文章以介绍依赖反转原则开始,接下来介绍如何使用控制反转来实现依赖反转原则,最后将阐述什么是依赖注入和如何实现它。

背景

在我们开始讲依赖注入前,首先要了解依赖注入要解决的问题。为了理解这个问题,我们需要知道两个事情:一,依赖反转原则;二,控制反转(Inversion of Controls(IoC)。我们先讨论依赖反转原则然后讲IoC。一旦完成这两个,我们就可以更好地理解依赖注入,然后我们就可以一窥依赖注入的细节。最后我们讨论如何实现依赖注入。

依赖反转原则

依赖反转原则是一个指导我们写出松耦合类的软件设计原则。根据依赖反转原则的定义:
1. 高层模块不应该以依赖低层模块。他们都应该依赖抽象层。

2.抽象层不应该依赖细节,细节应该依赖抽象层。

这个定义是什么意思?它想传达什么?让我们通过例子理解这个。几年前我被委派写一个本应该运行在web server的windows service。这个service唯一的需求是无论何时在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 ,EventLogWriter 不是一个抽象而是一个具体类。让我告诉你我们收到的关于这个service的下一个需求,问题就变得明朗。

下一个需求是向网络管理员的email发送关于指定的错误的邮件。一个想法是生成一个发送邮件的类并且把他的handle放到 AppPoolWatcher 但是在任意时刻我们只能使用一个对象,要么是 EventLogWriter 要么是 EmailSender 

一旦我们有更多可选的动作,像发送SMS,问题就变得更加严重。那样,我们就不得不添加一个实例保存在 AppPoolWatcher 的类。依赖反转原则告诉我们,我们需要解耦这个系统,方法是高级模块在我们的例子中是 AppPoolWatcher 将依赖一个简单的抽象并使用它。这个抽象反过来将被映射到一些会执行实际操作的具体类上。

控制反转

依赖反转是原则,他只是指明两个模块如何相互依赖。控制反转来实现他。控制反转使得我们可以使高层模块依赖抽象而不是底层模块的具体实现。

为了使用控制反转实现上述问题方案,第一步,我们需要创建一个抽象让高层模块可以依赖。我们创建一个接口来提供这个抽象。这个接口实现从 AppPoolWatcher 收到的通知。

 

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

 

修改高层模块来使用这个抽象

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

如果我要发送email和sms,这些类只要实现相同的接口就可以了:

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

但是我们有回到原点了。具体类的创建还是在高层类里。依赖注入进入眼帘。是时候介绍依赖注入的细节了。

依赖注入

依赖注入主要是注入具体实现到一个使用抽象例如接口的类里面。他的主要思想是减少类之间的耦合和移动抽象和具体实现之间的绑定到依赖雷的外面。

有三个方式实现依赖注入:

  1. Constructor injection
  2. Method injection
  3. Property injection

 Constructor injection(构造函数注入)

在这个方法里,我们传递具体类对象到依赖类的构造函数里。在依赖类中我们需要一个可以获取具体类对象的构造函数并把这个具体类赋值给这个类正在使用的接口handle,用此方法实现构造函数注入。代码如下:

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

在上面的代码中,构造函数将接受具体类对象并和接口handle绑定。如果我们要传EventLogWriter的具体实现到这个类中,我们要:

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

如果像发送email或SMS,我们只需要传递各自类对象到AppPoolWatcher的构造函数里。当我们知道这个依赖类的实例将在整个生命期一直使用一样的具体类,这个方法是有用的。

Method Injection

如果我们想传递单独的具体类都每个方法调用中,我们只需要传递依赖到这个方法里。

在方法注入里,我们传具体类对象到实际调用这个动作的依赖类的方法里。我们需要这个动作函数接受具体类对象的实参并赋值个这个类正在使用接口handle同时调用这个动作。代码如下:

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

如果我们要传EventLogWriter的具体实现到这个类,只要:

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

 

如果像发送email或SMS,我们只需要像上面一样传递各自类对象到AppPoolWatcher的调用方法里。

property injection

 

如果具体类的选择的责任和调用的方法不在一个地方。我们需要属性注入

在这个方式里,我们通过依赖类暴露的setter属性传递具体类对象。我们通过使用在依赖类的setter属性或函数来获取具体类对象并赋值给这个类正在使用的接口handle。代码如下:

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

如果要传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");

如果像发送email或SMS,我们只需要像上面一样传递各自类对象到AppPoolWatcher暴露的setter中里。当选择的具体实现的职责和调用的完成的动作在不同的位置/模块。

在没有支持属性的语言里,有单独的函数设置依赖。这个方法叫做setter 注入。需要注意的是,有可能有些人已经创建了依赖类(dependent class),但是没有人设置具体类依赖(dependency)。如果我们试图调用在这种情况下调用这个动作,那我们应该要么有一些映射到依赖类(dependent class)的默认依赖(dependency),要么有一些机制确保应用行为正确。

关于IoC Containers

构造函数注入是实现依赖注入最广泛使用的方法,如果需要传递不同的依赖(dependencies)在每个方法调用里,我们使用方法注入。属性注入使用的频率少一些。

如果我们只有一级的依赖(dependency),上面的方法可以工作的很好。但是如果具体类是其他抽象的依赖又如何。如果我们嵌套或链接依赖(dependencies),那实现依赖注入会有点复杂。这时可以使用IoC containers。IoC containers 可以容易的映射依赖(dependencies)当我们嵌套或链接依赖(dependencies)。