3.2依赖注入容器

2018.8.12 本博客调整为 《第3章·3.2依赖注入容器》

God是简单工具类,而Spring等依赖注入(Dependency Injection、DI)容器则是功能强大的工具箱。某种程度上,God比Spring更优秀,因为它用最简短的代码,言简意赅地说明了God和Spring的相同的本质——针对接口编程的使能工具

Martin Fowler有一篇非常著名的文章——简单一个网页的PageRank7,《IoC容器和依赖注入模式》(Inversion of Control Containers and the Dependency Injection pattern[1] )。在该文中,Martin Fowler把一些创建对象的工具箱如PicoContainer和Spring背后的“模式”,称为依赖注入(Dependency Injection、DI)。既然依赖注入已经被业界广为接受,本文也就使用依赖注入容器称呼Spring(的对象创建模块)。必须事先说明的是,笔者不想按照Martin Fowler的文章,逐字逐句地解释该文的哪些是正确的,哪些是错误的。下面将介绍“正确”的知识,在必要的时候指出Martin Fowler文章的错误。

3.2.1 God Vs. Spring

3.2.2注入的方式

3.2.3什么是依赖注入

 

【2017.12.30:

1.原博客 依赖注入(Dependency Injection)模式 大概的意思有,写得较垃圾:不流畅,例子不好。

2.不再将DI称为一种模式,而仅仅视为工具箱;强调 依赖注入(Dependency Injection)和框架、控制反转IoC,一点关系都没有

3.例子 不再是A依赖2个具体类,而改成Client→IA→IB;

4.伸手的方式,它不属于正常编程代码,不再更多讨论。所以构造器注入、接口注入不讨论;也不讨论@的应用。

3.2.1 God Vs. Spring

God是简单工具类,而Spring等依赖注入(Dependency Injection、DI)容器则是功能强大的工具箱,其核心仍为反射+配置文件。

1. 单层依赖

对于简单地单层依赖,如Client→IServer,Spring显得强大而繁琐。

God使用属性配置文件my.properties保存如下键值对

 

IIServer =commons.S1

Spring使用XML保存配置文件。




       

其中的id表示源代码中使用的键,而class表示将创建对象的类全名。从源代码中可以看出,Spring从(BlueJ项目根目录下的) spring1.xml中获得键IIServer的值并创建其对象。

 

package chap1.init.springDemo;
import commons.*;//IServer
import java.io.IOException;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.beans.factory.BeanFactory;
public class Client {
    public static void test() throws IOException {
        BeanFactory bf = new FileSystemXmlApplicationContext("spring1.xml");
        IServer h = (IServer)bf.getBean("IIServer");
        h.doSth();
        //比较   
        h = (IServer)yqj2065.util.God.create("IIServer");
        h.doSth();
    }    
}

 

2.多层依赖

对于多层依赖场合,例如Client→IA→IB,这里假定Client不需要知道IB。

采用God工具时,通常Client在某个方法中使用God创建IA的对象,调用该对象的setter方法和模板方法;而IA也需要使用God创建相关的IB对象。由于God不支持有参数构造器,各用户需要调用下级的setter方法。

 

package chap1.init.springDemo;//其他类的包语句略
public interface IB{
    public void setName(String name);//
    public void b();
}

public class B1 implements IB{
    private String name; 
    @Override public void setName(String name){
        this.name =name;
    }
    public void b(){
        System.out.println("B1.name="+name);
    }
}

public abstract class IA{ 
    private IB b =(IB)yqj2065.util.God.create("IB");
    public final void foo(){//模板方法模式
        a();
        b.setName("yqj2065");
        b.b();
    }
    public abstract void setId(int id);
    public abstract void a();    
}
public class A1 extends IA{    
    private int id; 
    public void setId(int id){
        this.id =id;
    }    
    public void a(){
        System.out.println("A1.ID="+id);       
    }
}

// Client
    public static void test2() throws IOException {
        ApplicationContext ctx = new FileSystemXmlApplicationContext("spring2.xml");
        IAA a0 = (IAA)ctx.getBean("IAA"); 
        a0.a();
        //比较
        pln("God");
        IA a = (IA)yqj2065.util.God.create("IA");
        a.setId(5);
        a.foo();
    }

Spring最引人注目的强大之处,在此得到体现。只需要在Client中使用一次ApplicationContext,其他对象都按照配置文件中指定的类全名和值进行初始化。Client→IAA→IB→IC…更多的依赖层,也全部按照配置文件创建对象。

Spring的核心仍为反射+配置文件。配置文件将指导反射机制来处理相应的类名、setter方法名或有参数构造器。

3.2.2注入的方式

Spring DI作为创建对象的工具,要求用户按照一些严格地规定,去编写源代码和相应的配置文件。可以把DI的概念形容为:伸手-等待

  • 等待意味着所有对象的创建,等待那一次ApplicationContext的出现;
  • 伸手则意味着需要通过反射来创建对象的类,要按照配置文件规定动作提供代码

 

1. Setter注入(Setter Injection)

Spring支持的一个规定动作——Setter注入,就是每个被注入的对象由其setter方法加以配置。按照Client→IAA→IB顺序,IAA的、需要被注入的子类如AA1,有两个成员变量IB和int,就需要提供2个设置方法,源代码中设置方法的名字必须是set+xxxx,而xxxx【忽略大小写】将在配置文件中使用。

反过来说,假定配置文件spring2.xml如下



    
       
       
   
   
       
   

则可以预料,chap1.init.springDemo.AA1源代码中有setIB(IB )和setIiiddd(数字类型)。因为标签的属性name,表示有一个set+(name值)的设置方法;属性ref,指向本配置文件中另外一个标签;属性value,为Java基本类型和String等变量初始化。

 

package chap1.init.springDemo;
public interface IAA{
    public abstract void a();    
}
package chap1.init.springDemo;
public class AA1 implements IAA{
    private IB bbb ;
    public void setIB(IB b){
        bbb =b;
    }
    private int id; 
    public void setIiiddd(int id){
        this.id =id;
    }
    public void a(){
        System.out.println("AA1.ID="+id);  
        bbb.b();
    }
    
}
// IB、Client等不变,略

既然Client→IAA→IB,而IAA、IB的对象“自动”创建,设置方法是Setter注入需要的“伸手”行为【值得注意的是,“伸手”代码如setIServer (IServer),虽然看起来很平常,但这些代码不是给Client的用户常规调用的,而是为依赖注入容器如Spring准备的。】。Clien知道IAA对象,但无法调用其设置方法;Clien不知道IB对象,即使IB在源代码中定义设置方法,Clien无法(不应该)获得IB对象地引用。【注意比较Client→IA→IB和Client→IAA→IB代码的不同

3. “伸手”代码问题

Spring所需的“伸手”代码,在不使用Spring时,应该删除。换言之,“伸手”代码不是作为通常的代码而出现,它干扰了通常的代码。如例程1-9的AA1的setIiiddd(int id),稍微不注意就可能对该无聊的命名重构为setID(int id),但是重构将导致Spring抛出运行时异常,因为Spring按照配置文件找不到setIiiddd(int id)。

使用配置文件最主要的缺陷,在于源代码的重构难以与配置文件同步。

可以使用 @Autowired 注解,提醒程序员这是“伸手”代码,也使得Spring以更多的方式完成注入。

3.2.3什么是依赖注入

4. Spring等DI容器是工具箱

Spring与God一样,仅仅是一个被调的工具库。框架是一个骨架式方案,应用程序员至少需要编写框架的@Override方法以提供代码支持。Spring不是框架,而是完整的方案。

Martin Fowler在Inversion of Control Containers and the Dependency Injection pattern中提出了依赖注入/DI,写到「控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好象在说"我的轿车是与众不同的,因为它有四个轮子"」,但他强化大众印象——DI容器是框架。

事实上,Spring DI容器作为ADP的使能工具【依赖注入的意义,在于保证Client仅仅与(通常是接口或抽象类)IServer耦合,而不与IServer的子类型耦合,使得程序符合OCP或依赖于抽象类型原则。,仅仅是工具箱。它和框架、控制反转IoC,一点关系都没有它完全是一个自行车,只有两个轮子


-----------------------------------------------------------
<遗留文字>

后头看例程2-4,客户类TestSortor依赖抽象类型IntSort,但是TestSortor赖皮地不考虑创建IntSort的对象,而且有两种形式:

 等待注入。
 参数传递。

 

public class TestSortor {    
    public static void test(int[] array,IntSort s){//IntSort作为参数
        array = s.sort(array);
        pln(array);
    } 
    //或者,依赖传入
    private static IntSort s;    
    public static void setSortor(IntSort s){
        TestSortor.s =s;
    }
    public static void test(int[] array){
        array = s.sort(array);
        pln(array);
    }
}

 

2.2.3 依赖注入模式

 

对于Client依赖于IServer,Client可以赖皮地不考虑创建IServer的对象,而将IServer初始化的工作交给依赖注入容器。

站在Client的角度,依赖注入(Dependency Injection、DI) 的概念很简单,简言之:伸手-等待

(1) Client的类体中没有初始化IServer变量的代码,它不想自己创建IServer对象,而是提供public构造器Client(IServer)或设置方法setIServer (IServer)等。是为伸手;

(2) 坐等外界为其初始化IServer对象。是为等待。

值得注意的是,“伸手”代码如setIServer (IServer),虽然看起来很平常,但这些代码不是给Client的用户常规调用的,而是为依赖注入容器如Spring准备的。


如果使用God,假设Main不操心Client依赖对象的初始化,则

 

例程 2 9 Spring Vs. God
package init;
public class Client {
    private IServer s;
    public static void testGod(){       
        s  = (IServer) yqj2065.util.God.create("IServer");
        s.doSth();
    }
}
package init;
public class Main {
    public static void main(String[] args) {        
        Client c1 = new Client(); 
        c1.testGod();
    }
}
testGod(){       
        s  = (IServer) yqj2065.util.God.create("IServer");
        s.doSth();
    }
}
package init;
public class Main {
    public static void main(String[] args) {        
        Client c1 = new Client(); 
        c1.testGod();
    }
}

 

 

 

Main不操心Client所依赖对象的初始化的情况下,Spring Vs. God

(1)Main的代码,使用Spring时,import Spring相关包,调用相关方法获得Client对象,而这个过程就是Client所"等待"的;使用God时,Main不需要God参与。

(2) Client的代码,使用Spring时,需要伸手方法——可以写成setIiiServer(IServers);使用God时,需要赋值语句。

(3) 使用Spring时,反射获得(Client);使用God时,反射获得(IServer);

现在说明使用Spring的细节,Spring可以按照XML配置文件或源代码中的Annotation自动装配。例程2-8中Main显式调用Spring,而Spring的自动装配按照XML配置文件。本例程依赖的配置文件为netbeans 中<项目文件夹>下的client.xml。

 
 
    
     
         
     
     
     

 

其中,描述一个“键-值对”,例程2-10中getBean("cli")使用了键"cli"。也描述一个“键-值对”,而键"sv"则在本xml文件中用于自动装配。注意,

当使用"iiiServer"为name时,Spring要求init.Client中有一个“伸手”方法setIiiServer(IServer s),也就是说,忽略大小写,iiiServer前面加set的方法

 

 

★单就学习设计模式而言,工具类God已经足够。

 

God能够完成Spring目前的工作(Spring作为庞大的框架,有其他用途),然而,God和Spring的软肋在于,它们在配置文件中需要一个能够绑定的子类型的名字,如果IServer的实现类是匿名类或lambda表达式——这也意味着Main不在意Client的成员变量s是否完成了初始化,Main自己将创建IServer的实现并传递给Client。

此外,对于SortTest拥有的static成员IntSort,Spring的注入,显得难看。

因此,很多时候可以选择参数传递方式。特别是Java8后,函数接口的实参用lambda表达式。

 

 

 


 

<遗留文字>

 

 

 

1.2.1 注入的方式

站在Client的角度,Client接受注入有3种的方式。

 

package principle;
public class Client{ 
    private IServer s; 
    public Client(IServer s){         this.s = s;    }
    public void set IServer(IServer s){        this.s = s;    }    
}

 

 

 

1. 构造器注入(Constructor Injection):Client提供构造器public  Client (IServer s),等待外界创建IServer的(实现类的)对象后将其引用传递进来/注入。

2. Setter注入(Setter Injection):Client提供设置方法如setIServer (IServers),等待注入。

3.接口注入,相当于将Setter注入的setIServer (IServer s) 方法封装到一个专用的接口如InjectIServer中,而Client实现InjectIServer并给出如下的方法体。

    @Override public void setIServer (IServer s){

        this.s=s;

    }

接口注入针对的场景是,有大量Client、Client1等都需要依赖于IServer。

public interface InjectIServer{

    public void setIServer (IServer s);

}

代码中使用了[4.1虚域模式]。然而这一方式并不一定被各种依赖注入容器所支持。

1.2.2 设计注射器

 

Client伸手-等待,注入器如何设计呢?

注入器可以设计为下层包中的一个工具类如tool.God,或者更为强大的依赖注入容器,如Spring、PicoContainer等。

任何应用程序App都可以用God作为注射器。因而,依赖注入通常意味着某注射器使用反射机制创建对象。换言之,注射器通常使用反射机制创建对象,以作为通用工具。

代码中,IServer对象的创建使用了tool.God的静态方法create(), create()不过是一个使用反射+属性配置文件(.properties文件)创建对象的静态工厂。

 

package creational.di;
import tool.God
public class App{ //Injection
    public static void test(){
         IServer s = (IServer) God.create("1-6"); //1-6 = creational.di.Server
         Client c = new Client();
         c.setIServer(s);//注入         
         c.show();
    }
}

 

站在Client的角度,依赖注入模式等待外界创建并传入对象


3. 但是,比工具类God更为强大的依赖注入容器,如Spring、PicoContainer等,它们认为使用/依赖关系是面向对象编程的最基本的程序结构,各种各样的使用关系如Client与IServer、C与S等等广泛存在,作为一个依赖注入的工具或框架,希望程序员不再编写如下代码:

 

      各种用于依赖注入的专用框架被开发出来如Spring等,它们被称为依赖注入或控制反转容器(DI/IoC Container)

 

依赖注入模式依赖注入容器、设计依赖注入容器所使用的技术(回调机制或控制反转)是3个东西,虽然密切相关——像爸爸、妈妈和孩子一样密切。

/*请注意,至少到目前为止,我们不需要任何特别的术语——依赖倒置原则DIP控制反转IoC,而此时,我会将依赖注入(Dependency Injection)作为一种设计模式。(也就是说,依赖注入与IoC不是同一个概念)*/

 

/*[吐槽]在我眼里,目前常见的两个术语依赖倒置原则DIP控制反转IoC,基本上是没有价值的术语。(因为有回调这个术语足以),我把作为依赖注入模式和设计依赖注入容器所使用的技术区别开来。更重要的原因,设计依赖注入容器所使用的技术,或者说,设计任一框架所使用的技术就是回调。凭什么要把依赖注入容器称为回调容器或控制反转容器。既然回调是一个常用术语,控制反转作为回调机制的同义词就没有什么价值*/

下面说明spring的两个用法:

1.如同God,仅仅作为一个利用反射+配置文件来创建对象的工具类。

2.按照XML配置文件自动装配——反映出依赖注入容器比God牛x之处。

……

/**

早期网络文章【Ioc容器的革命性优点】写道:“我们知道,在Java基本教程中有一个定律告诉我们:所有的对象都必须创建;或者说:使用对象之前必须创建,但是现在我们可以不必一定遵循这个定律了,我们可以从Ioc容器中直接获得一个对象然后直接使用,无需事先创建它们”。

这是什么话?使用对象之前必须创建,现在我们仍然遵循这个定律

*/

链接:

  1. Martin Fowler Inversion of Control Containers and the Dependency Injection pattern  中文
  2. InversionOfControl
  3. Shivprasad koirala, 12 Jun 2010 Design pattern – Inversion of control and Dependency injection 好多图。

 

你可能感兴趣的:(#,面向对象设计(Java),例解OOD(Java8))