控制反转IoC简介
在实际的应用开发中,我们需要尽量避免和降低对象间的依赖关系,即降低耦合度。通常的业务对象之间都是互相依赖的,业务对象与业务对象、业务对象与持久层、业务对象与各种资源之间都存在这样或那样的依赖关系。但是如何才能做到降低类之间的依赖关系呢?这就是本文核心IoC需要解决的问题,下面从两大点具体介绍IoC:
(1)IoC与DI的基本概念
IoC(Inversion Of Control)即控制反转,其具体就是由容器来控制业务对象之间的依赖关系,而不是像传统方式中由代码来直接控制。控制反转的本质,是控制权由应用代码转到了外部容器,控制权的转移即是所谓的反转。控制权的转移带来的好处就是降低了业务对象之间的依赖程度,即实现了解耦。
IoC的实现策略有两种:
1)依赖查找:容器中的受控对象通过容器的API来查找自己所依赖的资源和协作对象。这种方式虽然降低了对象间的依赖,但是同时也使用到了容器的API,造成了我们无法在容器外使用和测试对象;
2)依赖注入(又称DI:Dependency Injection):对象只提供普通的方法让容器去决定依赖关系,容器全权负责组建的装配,它会把符合依赖关系的对象通过属性或者是构造函数传递给需要的对象。通过属性注射依赖关系的做法称为设值方法注入,将构造子参数传入的做法称为构造子注入。
依赖注入的好处如下:
查询依赖操作和应用代码分离;
受控对象不会使用到容器的特定的API,这样我们的受控对象可以搬出容器单独使用。
(2)IoC模式的实例讲解
IoC代表的是一种思想,也是一种开发模式,但它不是什么具体的开发方法。要理解IoC的概念,最简单的方式就是看它的实际应用,下面将着重介绍几个实例来讲解IoC的内涵。
我们在开发一个应用系统时,会需要开发大量的Java类,系统将会通过这些Java类之间的相互调用来产生作用。类与类之间的调用关系是系统类之间最直接的关系。因此,我们可以将系统中的类分为两类:调用者和被调用者。具体如下图一所示:
图一:调用方法问题
软件设计方法及设计模式的发展,共产生了三种类调用的方法:自己创建(new)、工厂模式(get)、外部注入(set),其中外部注入即为IoC/DI的模式。
无论是哪一种方法,都存在两个角色——调用者和被调用者。下面我们通过实例来讲解这三种方法的具体含义。首先,我们设定调用者对象为学生对象Student,被调用者对象为图书对象Book,要设计的代码功能是学生学习图书知识。
从GoF设计模式中,我们已经习惯一种思维编程方式:Interface Driven Design接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等。为了演示不同的方法在Student取得不同Book对象时的区别,我们采用接口来设计被调用者,实现的代码如下面三个类所示:
//Book接口类 public interface IBook{ public void learn(); } //BookA实现类 public class BookA implements IBook{ public void learn(){ System.out.println("学习BookA"); } } //BookB实现类 public class BookB implements IBook{ public void learn(){ System.out.println("学习BookB"); } }
其中IBook为图书的接口,它定义了一个学习接口learn(),并定义了两个图书类BookA和BookB来实现该接口,表示是两本不同的图书,其中learn()方法分别表示不同图书学习过程。
下面将从这三种方法讲解如何调用图书类:
1)new——自己创建
Student要学习BookA,就要定义一个learnBookA()的方法,并自己来创建BookA的对象;同样,要学习BookB,就要定义一个learnBookB()的方法,并自己来创建BookB的对象。然后我们建立一个测试类Test.java来创建一个Student对象,可以分别调用learnBookA()和learnBookB()方法来分别执行两本书的学习过程。具体实现代码如下:
//学生类 public class Student{ public void learnBookA(){ IBook book = new BookA(); book.learn(); } public void learnBookB(){ IBook book = new BookB(); book.learn(); } } //测试运行 public class Test{ public static void main(){ Student student = new Student(); student.learnBookA(); student.learnBookB(); } }
该方法在调用者Student需要调用被调用者IBook时,需要由自己创建一个IBook对象。这种做法的缺点是,无法更换被调用者,并且要负责被调用者的整个生命周期。具体形式如下图二所示:
图二:自己创建方式
2)get——工厂模式
一切对象都由自己创建的缺点是,每一次调用都需要自己来负责创建对象,创建的对象会到处分散,造成管理上的麻烦,比如异常处理等。因此,我们可以将对象创建的过程提取出来,由一个工厂(Factory)统一来创建,需要什么对象都可以从工厂中取得。
例如下例中,我们创建了一个工厂类BookFactory,为该类添加两个函数getBookA()和getBookB(),分别用于创建BookA和BookB的对象。然后再创建Student中learnBookA()和learnBookB()中的方法,改为分别在该工厂类中取得这两个对象。具体实现代码如下;
//图书工厂 public class BookFactory{ public static IBook getBookA(){ IBook book = new BookA(); } public static IBook getBookB(){ IBook book = new BookB(); } } //学生类 public class Student{ public void learnBookA(){ IBook book = BookFactory.getBookA(); book.learn(); } public void learnBookB(){ IBook book = BookFactory.getBookB(); book.learn(); } } //测试运行 public class Test{ public static void main(){ Student student = new Student(); student.learnBookA(); student.learnBookB(); } }
此时与第一种方法的区别是,多了一个工厂类,并将Student中创建对象的代码提取到了工厂类,Student直接从工厂类中取得要创建的对象。这种方法的优点是,实现了对象的统一创建,调用者无须关心对象创建的过程,只管从工厂中取得即可。具体形式如下图三所示:
图三:工厂模式
这种方法实现了一定程度的优化,使得代码的逻辑也更趋向于统一。但是,对象的创建依然不灵活,因为对象的取得完成取决于工厂,又多了中间一道工序。
3)set——外部注入
显然,第一种方式依赖于被调用者对象,第二种方式依赖于工厂,都存在依赖性。为了彻底解决依赖性的问题,我们又取消了工厂类,并仅仅为Student添加一个学习的方法learnBook(),输入的参数是接口类型IBook。在使用Student的方法时,我们先创建IBook的具体对象,然后再把该对象作为learnBook()的输入参数注入到Student,调用接口IBook的统一方法learn()即可完成学习过程。具体实现代码如下所示:
//学生类 public class Student{ public void learnBook(IBook book){ book.learn(); } } //测试运行 public class Test{ public static void main(){ IBook bookA = new BookA(); IBook bookB = new BookB(); Student student = new Student(); student.learnBook(bookA); student.learnBook(bookB); } }
这样我们完全简化了Student类的方法,learnBook()的方法不再依赖于某一个特定的Book,而是使用了接口类IBook,这样只要在外部创建任意IBook的实现对象输入到该方法即可,使得Student类完全解脱了与具体某一种Book的依赖关系。上例中的Test.java,分别创建了bookA和bookB对象,同样都可以调用Student的learnBook()方法,使得Student变得完全通用。具体形式如下图四所示:
图四:外部注入方式
可见,set——外部注入方式完全抛开了依赖关系的枷锁,可以自由的由外部注入,这就是IoC,将对象的创建个获取提前到外部,由外部容器提供需要的组件。