控制反转与依赖注入详解

前言

前段时间看了Spring源码,有许多概念不是很清晰,所以看了一些资料,有不少收获,在此总结一下Inversion of Control Containers and the Dependency Injection pattern 这篇文章。

目前java的轻量级容器非常流行,可以把不同项目的组件装配起来,进而成为一个独立的应用。而这些容器底层都是用同一种模式来进行组件的装配,也就是“IOC(Inversion of Control/控制反转)”,更具体来说叫“DI(Dependency Injection/依赖注入)”。下文将主要对比IOC/DI和Service Locator(服务定位器)的不同之处,当然他们也有共同的作用:将组件的配置和使用分离

关键代码

有一个非常好用的电影查询组件。

 /**
 * 查询电影列表接口:findAll方法用于查询电影列表。
 */
public interface MovieFinder
{
    
    
    List findAll();
}
 
 /**
 * 查询电影列表实现类:用于适配不同的存储方式:txt、xml文件、数据库等。
 * 下面省略get set 构造方法。
 */
class MovieFinderImpl implements MovieFinder{
    
      
    public MovieFinderImpl(String filename)
    {
    
    
        this.filename = filename;
    }
    public List findAll(){
    
    
		//具体实现
	}
}

/**
* moviesDirectedBy可以根据电影导演名从列表中筛选电影。
* 控制反转的目的是让查询和存储解耦,下面省略get set 构造方法。
*/
class MovieLister{
    
      
  private MovieFinder finder;
  
  public MovieLister() {
    
    
    finder = new MovieFinderImpl("movies1.txt");
  }

  public Movie[] moviesDirectedBy(String arg) {
    
    
      List allMovies = finder.findAll();
      for (Iterator it = allMovies.iterator(); it.hasNext();) {
    
    
          Movie movie = (Movie) it.next();
          if (!movie.getDirector().equals(arg)) it.remove();
      }
      return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
  }
}

这个组件如果是个人使用,完全是没有问题的。但是如果其他人觉得好用,而他们的电影列表存储方式和我不同的话,就得重新提供一个 MovieFinder的实现类。所以说最终这个组件最终会有多个MovieFinder的实现类,而我们希望MovieLister类能够与MovieFinder的任何实现类配合工作,并且允许在运行期插入具体的实现类,插入动作完全脱离插件作者的控制。这里的问题就是:如何设计这个连接过程,使MovieLister类在不知道实现类细节的前提下与其实例协同工作。生产环境和测试环境可能会选择不同的部署方案,也就可能选择不同的接口实现类。这些新兴的轻量级荣日就是帮我们将这些插件组装成一个应用程序。这是这种新型轻量级容器面临的主要问题之一,并且普遍使用控制反转来实现。

控制反转

所以有一个问题:它们反转了哪方面的控制?来举两个例子对比一下吧:

1 普通方式:
早期的用户界面(控制台)完全由应用程序来控制的,你预先设计一系列命令,例如输入身高、输入体重等,应用程序逐条输出提示信息,并取回用户的响应,最后显示结果。

2 控制反转:
图形用户界面环境下,UI框架将负责执行一个主循环,你的应用程序只需为屏幕的各个区域提供事件处理函数即可。

看以看出图形用户界面下,程序的主控制权从程序移到了框架,也就是控制权发生了反转。 对于像Spring这样的轻量级容器,他的控制指的是确定接口的具体实现。 在上面那个demo中,MovieLister类负责确定MovieFinder的具体实现,然后MovieFinder也就不成其为一个插件了,因为它并不是在运行期插入应用程序中的。而这些轻量级容器则使用了更为灵活的办法,只要插件遵循一定的规则,一个独立的组装模块就能够将插件的具体实现注射到应用程序中。这个模式后来被称作"依赖注入"(Dependency Injection)。需要注意的是,为了消除程序和插件实现的依赖,除了DI还可以用ServiceLocater。

DI的方式

DI的思想简单来说就是有独立的类、装配类,装配类用于选择合适的接口实现,并填充对应的字段,如下图所示(引用的图):
在这里插入图片描述
下面将通过Spring举例三种依赖注入的方式,文件目录如下图,。
在这里插入图片描述

构造函数注入

xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieLister" class="MovieLister">
        <constructor-arg ref="movieFinder"/>   写法不唯一,这里不展开了
    </bean>
</beans>

测试

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");

        MovieFinderImpl finder = applicationContext.getBean("movieFinder",MovieFinderImpl.class);
        System.out.println(finder.toString());

    }
}

结果
在这里插入图片描述

set注入

xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieFinder" class="MovieFinderImpl">
        <property name="fileName" value="电影名单.txt"></property>    这里是set方法注入
    </bean>
    <bean id="movieLister" class="MovieLister">
        <constructor-arg ref="movieFinder"/>
    </bean>
</beans>

测试

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");

        MovieLister lister = applicationContext.getBean("movieLister",MovieLister.class);
        System.out.println(lister.toString());

    }
}

结果
在这里插入图片描述

接口注入

查了不少博客,看到**从注入方式的使用上来说,接口注入是现在不提倡的一种方式,因为它强制被注入对象实现不必要的接口,带有侵入性。**为了演示,我们需要在实例代码中新增IMovieLister接口,并且让MovieLister实现它

public interface IMovieLister {
    
    
    abstract MovieFinder createMovieFinder();
}
/**
* 注意这里改成了抽象类。构造函数,get,set,toString方法省略
*/
public abstract class MovieLister implements IMovieLister {
    
    

    private MovieFinder finder;
    
    public Movie[] moviesDirectedBy(String arg) {
    
    
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext(); ) {
    
    
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }
    
    @Override
    public abstract MovieFinder createMovieFinder();
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieFinder" class="MovieFinderImpl">
        <property name="fileName" value="电影名单.txt"></property>
    </bean>
    <bean id="movieLister" class="MovieLister">
        <lookup-method name="createMovieFinder" bean="movieFinder"></lookup-method>
        <constructor-arg ref="movieFinder"/>
    </bean>
</beans>

测试代码

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
        MovieLister lister = applicationContext.getBean("movieLister",MovieLister.class);
        System.out.println("接口注入和构造方法注入比较: "+ (lister.createMovieFinder()==lister.getFinder()) );
        System.out.println(lister.toString());
    }
}


结果可以看出,两种方式注入的是同一个bean。
在这里插入图片描述

Service Locator

依赖注入解决了MovieLister对MovieFinder实现类的依赖,这样MovieLister的作者就可以把工具分享给其他人,使用者可以根据自身的情况完成MovieFinder的实现类。打破这种依赖的方法不止一种,ServiceLocater也可以做到。

Service Locator 基本思想就是有一个Service Locator ,他知道如何获取应用程序可能需要的所有服务。 当然这只是把问题转移了,我们仍然需要将Service Locator 放入MovieLister中,依赖关系如下:
在这里插入图片描述

简单例子


public class MovieLister {
    
    
    private MovieFinder finder = ServiceLocator.MovieFinder();  //从ServiceLocator获取实现类

    public Movie[] moviesDirectedBy(String arg) {
    
    
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext(); ) {
    
    
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }

    @Override
    public String toString() {
    
    
        return "MovieLister{" +
                "finder=" + finder +
                '}';
    }
}
public class ServiceLocator {
    
    
    private static ServiceLocator soleInstance;  
    private MovieFinder movieFinder;

    public static MovieFinder MovieFinder() {
    
      //获取接口实例
        return soleInstance.getMovieFinder();
    }

    public MovieFinder getMovieFinder() {
    
       
        return movieFinder;
    }

    public static void load(ServiceLocator serviceLocator) {
    
      
        soleInstance = serviceLocator;
    }

    public ServiceLocator(MovieFinder movieFinder) {
    
       
        this.movieFinder = movieFinder;
    }
}

测试代码

public class Application {
    
    
    public static void main(String[] args){
    
    
        configureServiceLocator(); //当然也可以用配置文件读取配置
        MovieLister lister = new MovieLister();
        System.out.println(lister);
    }

    private static void configureServiceLocator() {
    
    
        ServiceLocator.load(new ServiceLocator(new MovieFinderImpl("movies1.txt")));
    }
}

运行结果
在这里插入图片描述
对于更复杂的场景,我们可以通过新增ServiceLocator的子类;可以通过调用实例的方法而不是直接获取实例中的属性;可以通过进程级别的存储实现进程级别的ServiceLocator。

独立接口方式

上文中我们只需要ServiceLocator的MovieFinder方法,但是却直接引入了ServiceLocator,我们可以通过定义接口来做好隔离。具体的代码如下:

定义接口

public interface MovieFinderLocator {
    
    
    MovieFinder MovieFinder();
}

接口实现

public class ServiceLocator implements MovieFinderLocator...
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;
    
    public  MovieFinder MovieFinder() {
    
    
        return soleInstance.getMovieFinder();
    }
    public static MovieFinderLocator locator() {
    
    
        return soleInstance;
    }
    public static void load(ServiceLocator serviceLocator) {
    
    
        soleInstance = serviceLocator;
    }

赋值

public class MovieLister...
    private MovieFinderLocator locator = ServiceLocator.locator();
    private MovieFinder finder = locator.MovieFinder();

代码测试

public class Application {
    
    
    public static void main(String[] args){
    
    
        configureServiceLocator();
        MovieLister lister = new MovieLister();
        System.out.println(lister);
    }

    private static void configureServiceLocator() {
    
    
        ServiceLocator.load(new ServiceLocator(new MovieFinderImpl("movies1.txt")));
    }
}

运行结果
在这里插入图片描述

动态ServiceLocator

上面的例子中,你需要的每一个服务都是静态的定义好的,当然我们也可以采用HashMap动态地存取Service。

class ServiceLocator...
  private static ServiceLocator soleInstance;
  private Map services = new HashMap();
  
  public static Object getService(String key){
    
    
      return soleInstance.services.get(key);
  }
  public void loadService (String key, Object service) {
    
    
      services.put(key, service);
  }

配置

  private void configure() {
    
    
      ServiceLocator locator = new ServiceLocator();
      locator.loadService("MovieFinder", new MovieFinderImlp("movies1.txt"));
      ServiceLocator.load(locator);
  }

获取

class MovieLister...
  MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

模式选择

DI / ServiceLocator

这两个模式都提供了基本的解耦合能力,使得应用程序代码都不依赖于服务接口的具体实现。两者之间最重要的区别在于:具体实现以什么方式提供给应用程序代码。 使用Service Locator模式时,应用程序代码直接向ServiceLocator显式要求服务的实现;使用Dependency Injection模式时,应用程序代码不发出显式的请求,服务的实现被注入在应用程序中,发生了控制反转。

人们倾向于使用DI模式的一个常见理由是:它简化了测试工作。这里的关键是:出于测试的需要,你必须能够轻松地在真实的服务实现与供测试用的伪组件之间切换。但是,如果单从这个角度来考虑,Dependency Injection模式和Service Locator模式其实并没有太大区别。

在前面的例子中,作者要把 MovieLister 类交给朋友去用,这种情况下使用服务定位器就很好:朋友们只需要对定位器做一点配置,使其提供合适的服务实现就可以了。在这种情况下,我看不出 Dependency Injection 模式提供的控制反转有什么吸引人的地方。

但是,如果把 MovieLister 看作一个组件,要将它提供给别人写的应用程序去使用,情况就不同了,作者无法预测使用者会使用什么样的服务定位器API,每个使用者都可能有自己的服务定位器,而且彼此之间无法兼容。一种解决办法是为每项服务提供单独的接口,使用者可以编写一个适配器,让我的接口与他们的服务定位器相配合。但即便如此,我仍然需要到服务定位器中寻找我规定的接口。而且一旦用上了适配器,ServiceLocator所提供的简单性就被大大削弱了。

所以,主要的问题在于:代码的作者是否希望自己编写的组件能够脱离自己的控制、被使用在另一个应用程序中。如果答案是肯定的,那么他就不要对服务定位器做任何假设——哪怕最小的假设也会给使用者带来麻烦。

构造器注入 / set方法注入

上文提到过,接口注入的侵略性比较强,比起Service Locator模式的优势也不那么明显。所以,我们一般选择构造器注入或者set方法注入,下面将仔细对比这两种方法。

构造函数初始化的另一个好处是:你可以隐藏任何不可变的字段——只要不为它提供set方法就行了。如果某个字段是不应该被改变的,没有针对该字段的set方法就很清楚地说明了这一点。如果通过set方法完成初始化,暴露出来的设值方法很可能会造成一些问题。

  1. 如果参数太多,构造函数会显得凌乱不堪。如果有不止一种的方式可以构造一个合法的对象,也很难通过构造函数描述这一信息,因为构造函数之间只能通过参数的个数和类型加以区分(Factory Method模式可以解决)。

  2. 如果要传入的参数是像字符串这样的简单类型,构造函数注入也会带来一些麻烦。使用设值方法注入时,你可以在每个设值方法的名字中说明参数的用途;而使用构造函数注入时,你只能靠参数的位置来决定每个参数的作用,而记住参数的正确位置显然要困难得多。

  3. 如果对象有多个构造函数,对象之间又存在继承关系,事情就会变得特别讨厌。为了让所有东西都正确地初始化,你必须将对子类构造函数的调用转发给超类的构造函数,然后处理自己的参数。这可能造成构造函数规模的进一步膨胀。

尽管有这些缺陷,但仍然建议你首先考虑构造函数注入。不过,一旦面对上面提到的问题,你就应该准备转为使用set方法注入。

代码/配置文件

有些时候直接在程序代码中实现装配会更简单。譬如一个简单的应用程序,也没有很多部署上的变化,这时用几句代码来配置就比XML 文件要清晰得多。

有时应用程序的组装非常复杂,涉及大量的条件步骤。一旦编程语言中的配置逻辑开始变得复杂,你就应该用一种合适的语言来描述配置信息,使程序逻辑变得更清晰。然后,你可以编写一个构造器类来完成装配工作。如果使用构造器的情景不止一种,你可以提供多个构造器类,然后通过一个简单的配置文件在它们之间选择。

总结

在时下流行的轻量级容器都使用了一个共同的模式来组装应用程序所需的服务,称为Dependency Injection,它可以有效地替代Service Locator模式。在开发应用时,两者各有千秋,Service Locator模式的行为方式更为直观。但是,如果你开发的组件要交给多个应用程序去使用,那么Dependency Injection模式会是更好的选择。

如果你决定使用Dependency Injection模式,建议你首先考虑构造函数注入;如果遇到了某些特定的问题,再改用设值方法注入。如果你要选择一个容器,在其之上进行开发,我建议你选择同时支持这两种注入方式的容器。

学习资源

Inversion of Control Containers and the Dependency Injection pattern
跟我学Sprig
Spring的依赖注入(接口注入)

猜你喜欢

转载自blog.csdn.net/weixin_44532671/article/details/123424858