2013年6月5日    作者 :subaochen  

    重读Martin Fowler的著作:http://martinfowler.com/articles/injection.html, 不仅觉得有些意犹未尽,一方面原因是DI的概念发展到现在已经15年之久了,DI的世界已经发生了一些变化,Martin Fowler的大作并没有及时跟进这些变化;另一方面,文中所举的例子没有办法完整运行,读者如果希望亲自操练体验DI的话有些困难,这让我产生了为 Martin Fowler的大作写个补注的想法。Martin Fowler的大作写的实在是精彩,为其写个补注其实也是一个艰巨而光荣的任务,在这里先鼓励一下自己,能够坚持把这篇文章写完。本文希望能够提供几个完 整的实例让读者立刻就能够亲身体验DI的威力。当然,这些实例大多还是以Martin Fowler的大作为原型的,只是每个实例都是完整的Eclipse项目/Netbeans项目,简单的导入后即可运行,也提供了命令行执行的方法指导。 本文涉及到的每个实例都可以从github下载相应的压缩包,或者,更希望大家能够积极Fork出来参与其中。

目录

组件和面向组件的编程

本文反复提到组件(Component,java中的bean也被作者称为组件),有必要首先澄清什么组件,什么是面向组件的编程。

组件的定义

通常认为,组件是这样的一组代码:

  1. 符合松耦合原则:组件之间的依赖性小,即能够独立的描述事物的特征和行为而不依赖于其他组件的状态;

  2. 符合可替换原则:一个组件能够被同样功能的另外一个组件替换而不影响系统的稳定性。

  3. 符合OCP原则:允许扩展,但是不允许修改。

看起来,组件和对象的概念何其相似!实际上,在大多数情况下,组件就是对象,只不过组件通常是若干对象的组合罢了,或者说,组件通常是较为复杂的对象。

组件和编程语言并没有确定的关系,本文以Java语言为例说明组件的概念和面向组件的编程常见问题及其解决方法。

面向组件的编程(COP:Component Oriented Programming?)

使用组件的主要目的是代码复用,提高代码的模块化程度。

面向组件的编程主要考虑如下的几个问题:

  1. 组件封装的粒度控制:这里似乎没有很好的明确的定义?一个总的原则是,在能够独立完成特定功能的基础上,组件的尺寸通常越小越好,即小的就是美的,或者KISS(Keep It Stupid Simple)原则。

  2. 组件的命名规则:通常,大家遵守“camel命名法”。CDI自动将类名转化为camel命名法作为组件的“名称”(注意,在CDI中,组件的名称这样的说法并不合适,因为在CDI中,组件并没有名字,只有类型)

  3. 组件的可灵活配置:组件的初始状态最好是可灵活配置的,比如通过xml配置文件,或者在注解中设定组件的初始状态。

  4. 组件的相互依赖关系:通常一个组件要使用其他组件的成熟功能来达成自己的目的:为什么要重新发明轮子呢?这就要很好的解决组件之间的依赖关系,这也是面向组件编程的难点和重点之一。

  5. 组件的生命周期管理:组件在不同的场景下可能需要不同的生命周期,如何管理组件的生命周期也是面向组件变成的重点和难点之一。

  6. 组件的测试:由于组件符合OCP原则,组件的自动化测试便是保证组件质量的重要手段。

在编程实践中,我们可以完全掌控每个组件的细节及其生命周期,但是随着软件规模的增大和团队规模的增长,对组件的管理越来越困难,越来越混乱,软件质量越来越难以得到保证。怎么办?把组件的管理交给容器!这就是IoC/DI。

DI的概念

什么是容器

简单的讲,容器是一个组件管理器,我们可以向容器注册组件,或者向容器申请一个特定状态的组件。下面是一些常见的容器:

  • Tomcat:著名的Servlet容器,管理Servlet组件为主

  • Spring:著名的MVC开发框架,管理SpringBean组件为主

  • PicoContainer:轻量级IoC容器,理论上可以管理任何组件

  • Weld:著名的JavaEE/CDI容器,理论上可以管理任何组件,是JavaEE7的核心和基石。

DI只是IoC的一个应用场景

基于组件的开发模式少不了和容器打交道,容器负责管理组件的生命周期,当我们需要一个组件的时候,只需要简单的伸手向容器索要即可:我们并不关心组 件从哪里来到哪里去以及创建这个组件需要哪些先决条件,我们只是向容器提交一个申请,告诉容器我们需要一个什么样的组件对象,容器就会准备好这样的一个对 象,我们就可以直接拿来使用了,如下图所示:

container_bean

这就是IoC:作为程序员的我们无须再通过new操作符创建组件对象,需要组件的时候只需要申请使用即可,即将组件的控制权交给容器而不是我们程序员亲自打理。这样带来的好处显然是:

  1. 减轻了程序员的代码负担:衣来伸手,饭来张口的日子难道不舒服吗?

  2. 便于组件的测试:当我们无须操心组件的生命周期的时候,对组件的自动化测试就更加容易了,我们只专注于组件的业务逻辑就可以了。

  3. 便于写出更容易复用的组件:我们使用组件的目的就是为了让组件更容易复用,不是吗?容器在管理组件生命周期的同时也解决了组件之间依赖关系的解析,即实现了组件的松耦合(这似乎是一个较长的话题,希望有机会展开)。

上面描述的其实IoC的其中一个应用场景(关于IoC的历史演变,大家可以参考:http://picocontainer.codehaus.org/inversion-of-control-history.html),这个应用场景通常又被称作DI:解决的是基于组件的开发模式中的以下问题:

  • 组件的定位(查询):如何将这个组件和另外的组件唯一区分开来?也就是说,我们伸手向容器索要组件的时候,容器是根据什么找到我们指定的组件的?

  • 组件的依赖关系:容器能够自动解决组件间的依赖关系,即如果创建组件A的时候需要首先创建组件B,则容器会首先自动创建组件B,然后再创建组件A。

  • 组件的配置:容器对组件的管理不是铁板一块的,我们程序员可以通过传入适当的参数或者编写适当的配置文件(比如xml配置文件)告诉容器应该如何创建组件或者规范组件的行为。这其实是外部应用程序和容器交互的一个窗口。

  • 组件的生命周期:容器通常采取默认的策略管理组件的生命周期,好的容器允许扩展默认的生命周期以提供更多的组件生命周期。现代的容器一般允许以注解的方式声明组件的生命周期。

首先有几点说明:

  • 本文所有代码均可以在github自由下载和fork:https://github.com/subaochen/movieLister.git,下面的每个实例也有说明具体的branch名称。

  • 运行每个实例最简单的方法是进入src目录执行javac *.java,然后执行java Client即可看到结果。

实例一:不使用DI的MovieLister

本文所有的实例均取材于Martin Fowler关于IoC的大作,即实现一个简单的电影列表器:列出某个导演的所有代表作。首先是不使用DI模式的MovieLister,完整的代码可以从这里下载(branch:movieLister_without_DI):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



Zip压缩包的下载地址是:https://github.com/subaochen/movieLister/archive/master.zip

例子中涉及到的java类之间的关系如下图所示:

movie_lister_without_di

 

其中,MovieFinder接口规定了查询电影的方法:

 



public interface MovieFinder {    List findAll();     }



1

2

3

public interface MovieFinder {

    List findAll();    

}



电影可以存储在文件、数据库、网络…,我们这里使用文件保存电影库,ColonDelimitedMovieFinder则是MovieFinder的一个具体实现:



public class ColonDelimitedMovieFinder implements MovieFinder{    private String movieFile;    public ColonDelimitedMovieFinder(String movieFile){        this.movieFile = movieFile;    }    @Override    public List findAll() {        List movies = new ArrayList(0);        try {            BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(movieFile)));            String movieLine = br.readLine();            while(movieLine != null){                String[] items = movieLine.split(",");                Movie movie = new Movie(items[0],items[1]);                movies.add(movie);                movieLine = br.readLine();            }            br.close();        } catch (FileNotFoundException ex) {            ex.printStackTrace();        } catch (IOException ex) {            ex.printStackTrace();        }        return movies;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

public class ColonDelimitedMovieFinder implements MovieFinder{

    private String movieFile;

    public ColonDelimitedMovieFinder(String movieFile){

        this.movieFile = movieFile;

 

    }

 

    @Override

    public List findAll() {

        List movies = new ArrayList(0);

        try {

            BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(movieFile)));

            String movieLine = br.readLine();

            while(movieLine != null){

                String[] items = movieLine.split(",");

                Movie movie = new Movie(items[0],items[1]);

                movies.add(movie);

 

                movieLine = br.readLine();

            }

 

            br.close();

        } catch (FileNotFoundException ex) {

            ex.printStackTrace();

        } catch (IOException ex) {

            ex.printStackTrace();

        }

 

        return movies;

    }

 

}



最后是测试文件:



public class Client {    public static void main(String[] args){        List movies = new MovieLister("movies.txt").moviesDirectedBy("zhang yi mou");        for(Movie movie:movies)            System.out.println(movie.getTitle());    } }



1

2

3

4

5

6

7

8

public class Client {

    public static void main(String[] args){

        List movies = new MovieLister("movies.txt").moviesDirectedBy("zhang yi mou");

 

        for(Movie movie:movies)

            System.out.println(movie.getTitle());

    }

}



要运行这个测试,最简单的方法是进入src目录,执行如下的命令序列:



javac *.java java Client



1

2

javac *.java

java Client



则可以得到如下的执行结果:



subaochen@debian:~/NetBeansProjects/movieLister/src$ java Client hong gao liang qiu ju da guan si



1

2

3

subaochen@debian:~/NetBeansProjects/movieLister/src$ java Client

hong gao liang

qiu ju da guan si



在这个实例中,我们看到有两个问题:

  • Client直接创建了MovieLister对象(new MovieLister(…))的,即movieLister对象的生杀予夺大权是掌握在我们程序员手里的,这显然不是IoC(组件或者对象的生杀予夺大权是掌握在容器手里的)。

  • 在MovieLister中是和ColonDelimitedMovieFinder绑定在一起的:finder = new ColonDelimitedMovieFinder(movies)。这样如果我们需要更改电影库的存储方式,就需要修改MovieLister的代 码。这显然是不够灵活的,或者说,MovieLister组件和ColonDelimitedMovieFinder组件是紧密耦合在一起的,这违反了组 件之间应该是松耦合的设计理念,不利于组件的复用。

下面我们看看DI如何解决上面的两个问题(当然,DI不仅仅能够解决上面的两个问题)。

实例二:使用构造器依赖注入的movieLister

这里的例子可以从github获得(branch:movieLister_constructor_DI):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



或者直接下载ZIP压缩包:https://github.com/subaochen/movieLister/archive/movieLister_constructor_DI.zip

实例中涉及到的类之间的关系如下图所示:

di_constructor

从图中可以看出,MovieLister组件现在和MovieFinder的具体实现ColonDelimitedMovieFinder不再有直 接的关联,这正是我们需要的:当MovieLister作为一个独立组件使用的时候,MovieLister并不关心从何种介质和途径搜索电影。那么如何 方便的实现MovieLister和ColonDelimitedMovieFiner的解耦呢?这正是DI的威力所在。

要发挥DI的威力,就必须有一个能够管理组件的容器。在这里设计了一个简单的Container而没有使用成熟的容器(比如 PicoContainer)的目的是让大家了解在DI中Container到底起什么作用,以消除对Container的神秘感。当然,这个 Container的确是非常简陋和初级的一个Container(其中部分代码也参考了PicoContainer早期的代码),只是示意性的实现了组 件的注册(registerComponent)、定位(getComponent)功能,并没有涉及组件的诸如配置、缓存、生命周期等问题。这个简单的 Container包含了一个HashMap,以组件的Class为key保存了组件对象,即组件的Class是定位(查询)组件的关键词。容器的实现类 MyContainer如下所示:

 



public class MyContainer implements Container {    private Map<Class, Object> compMap = new HashMap<Class, Object>(0);    @Override    public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {        if (compMap.get(compKey) != null) {            return;        }        Constructor[] constructors = compImplementation.getConstructors();        try {            // 这里只支持一个构造方法            Constructor constructor = constructors[0];            Class[] params = null;            if (compKey == compImplementation) {                params = constructor.getParameterTypes();                // 这里只支持基于构造方法的DI                // TODO 递归构造?                Object[] args = new Object[params.length];                for (int i = 0; i < params.length; i++) {                    Class param = params[i];                    args[i] = getComponentForParam(param);                }                if (!hasNullArgs(args)) {                    compMap.put(compKey, constructor.newInstance(args));                }            } else {                compMap.put(compKey, constructor.newInstance(parameters));            }        } catch (InstantiationException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalAccessException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalArgumentException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (InvocationTargetException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        }    }    @Override    public void registerComponent(Class clazz) {        registerComponent(clazz, clazz, null);    }    @Override    public Object getComponent(Class clazz) {        // TODO Auto-generated method stub        return compMap.get(clazz);    }    private boolean hasNullArgs(Object[] args) {        for (int i = 0; i < args.length; i++) {            Object arg = args[i];            if (arg == null) {                return true;            }        }        return false;    }    private Object getComponentForParam(Class param) {        for (Iterator iterator = compMap.entrySet().iterator(); iterator.hasNext();) {            Map.Entry entry = (Map.Entry) iterator.next();            Class clazz = (Class) entry.getKey();            if (param.isAssignableFrom(clazz)) {                return entry.getValue();            }        }        return param;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

public class MyContainer implements Container {

 

    private Map<Class, Object> compMap = new HashMap<Class, Object>(0);

 

    @Override

    public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {

        if (compMap.get(compKey) != null) {

            return;

        }

 

        Constructor[] constructors = compImplementation.getConstructors();

        try {

            // 这里只支持一个构造方法

            Constructor constructor = constructors[0];

            Class[] params = null;

            if (compKey == compImplementation) {

                params = constructor.getParameterTypes();

                // 这里只支持基于构造方法的DI

                // TODO 递归构造?

                Object[] args = new Object[params.length];

                for (int i = 0; i < params.length; i++) {

                    Class param = params[i];

                    args[i] = getComponentForParam(param);

                }

 

                if (!hasNullArgs(args)) {

                    compMap.put(compKey, constructor.newInstance(args));

                }

            } else {

                compMap.put(compKey, constructor.newInstance(parameters));

            }

        } catch (InstantiationException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalAccessException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalArgumentException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (InvocationTargetException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        }

 

    }

 

    @Override

    public void registerComponent(Class clazz) {

        registerComponent(clazz, clazz, null);

    }

 

    @Override

    public Object getComponent(Class clazz) {

        // TODO Auto-generated method stub

        return compMap.get(clazz);

    }

 

    private boolean hasNullArgs(Object[] args) {

        for (int i = 0; i < args.length; i++) {

            Object arg = args[i];

            if (arg == null) {

                return true;

            }

        }

        return false;

    }

 

    private Object getComponentForParam(Class param) {

        for (Iterator iterator = compMap.entrySet().iterator(); iterator.hasNext();) {

            Map.Entry entry = (Map.Entry) iterator.next();

            Class clazz = (Class) entry.getKey();

            if (param.isAssignableFrom(clazz)) {

                return entry.getValue();

            }

 

        }

        return param;

    }

}



其中第11行使用java的反射机制获取组件的构造器,后面会使用这个构造器和相应的参数创建组件对象并保存到HashMap中。注意到在这个简单的Container中,我们仅仅支持一个构造方法。

所谓的构造方法依赖注入是指在组件的构造方法中声明对其他组件的依赖关系,即组件的构造方法的参数为所依赖的组件。这样,我们从组件的构造方法即可一目了然的看出当前组件依赖于哪些其他组件。这就要求,在注册这个组件之前首先要注册所依赖的其他组件。结合本例,MovieLister组件必须提供一个构造方法,构造方法的参数是MovieFinder:

 



public MovieLister(MovieFinder finder){        this.finder = finder;    }



1

2

3

    public MovieLister(MovieFinder finder){

        this.finder = finder;

    }



这即说明了MovieLister组件依赖于MovieFinder组件。当然在这个例子中,MovieLister组件只依赖于 MovieFinder一个组件,如果MovieLister组件依赖于多个组件,就需要在构造方法中一一列出所依赖的组件(作为构造方法的参数)。

那么,Container如何知道MovieLister是使用MovieFinder的哪个实现类呢?这里就要求Container中只能够有一 个组件的索引是MovieFinder.class,否则可能产生歧义。我们看到,在MyContainer的第23行,我们根据构造方法每个参数的类型 (class)去查找已经注册的组件并获得组件对象,然后才能正确的创建MovieLister组件对象。

由于这个简单的Container还没有实现递归的组件注册机制,因此我们在注册MovieLister之前必须首先注册 ColonDelimitedMovieFinder这个组件,否则Container在创建MovieLister组件时无法实例化 MovieFinder类型的组件,即ColonDelimitedMovieFinder,这就是为什么configContainer这样写的原因:



public static Container configureContainer(){        Container container = new MyContainer();        Object[] finderParams = new String[]{"movies.txt"};        container.registerComponent(MovieFinder.class, ColonDelimitedMovieFinder.class, finderParams);        container.registerComponent(MovieLister.class);        return container;    }



1

2

3

4

5

6

7

    public static Container configureContainer(){

        Container container = new MyContainer();

        Object[] finderParams = new String[]{"movies.txt"};

        container.registerComponent(MovieFinder.class, ColonDelimitedMovieFinder.class, finderParams);

        container.registerComponent(MovieLister.class);

        return container;

    }



注意到,这里在configContainer中注册了组件ColonDelimitedMovieFinder和MovieLister,这是在 代码中写死了的做法,更加灵活的方法是将需要注册的组件及其依赖关系放到配置文件中,然后在configContainer方法中通过加载和分析配置文件 的方法注册相应的组件,这里不再赘述。

Client就比较容易理解了:



public static void main(String[] args){        Container container = configureContainer();        MovieLister lister = (MovieLister)container.getComponent(MovieLister.class);        List movies = lister.moviesDirectedBy("zhang yi mou");        for(Movie movie:movies)            System.out.println(movie.getTitle());    }



1

2

3

4

5

6

7

8

    public static void main(String[] args){

        Container container = configureContainer();

        MovieLister lister = (MovieLister)container.getComponent(MovieLister.class);

        List movies = lister.moviesDirectedBy("zhang yi mou");

 

        for(Movie movie:movies)

            System.out.println(movie.getTitle());

    }



从MovieLister.java我们可以看出,MovieLister现在只和MovieFinder接口有关系,而没有和任何具体的 MovieFinder实现绑定在一起,也就是说,MovieLister组件和MovieFinder组件之间是松耦合的。另 外,MovieLister组件、ColonDelimitedMovieFinder组件的生杀予夺大权现在到了Container手里,我们不再需要 使用new操作符去创建这两个组件对象了。


欢迎访问肖海鹏老师的课程中心:http://edu.51cto.com/lecturer/user_id-10053053.html

欢迎加入肖海鹏老师技术交流群:2641394058(QQ)