DI与IOC——控制反转容器和依赖注入模式
Spring中,重看的话就是重看知识点,到docs里面找到index文档,看到Dependency injection removes the responsibility for object creation and object linking from the objects themselves to a factory. The factory is often provided by an Inversion of Control (IoC) container. For an overview of Inversion of Control containers and the Dependency Injection pattern,please see Martin Fowler's article.
原文地址:http://www.martinfowler.com/articles/injection.html 这才是所谓”真正的博文“。 点击进去发现时一篇经典的文章,百度刚好有一份中文翻译,咳咳,那么长就不翻译了,转过来。转自http://wenku.baidu.com/link?url=ShiAHZ2vv-ADwhKuceee_AftRKdBzpbxmU0EQLf-pL2a-dW8LRuVlkmL5H_eIz9RqsLLMT4bcc0pJkrJXCMMiCibQ4HoXRB0F2Q2FgTTdKe 。 IoC容器和Dependency Injection模式 组件和服务 一个简单的例子 class MovieLister... 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()]); } 你可以看到,这个功能的实现极其简单:moviesDirectedBy方法首先请求finder(影片搜寻者)对象(我们稍后会谈到这个对象)返回后者所知道的所有影片,然后遍历finder对象返回的清单,并返回其中由特定的某个导演执导的影片。非常简单,不过不必担心,这只是整个例子的脚手架罢了。我们真正想要考察的是finder对象,或者说,如何将MovieLister对象与特定的finder对象连接起来。为什么我们对这个问题特别感兴趣?因为我希望上面这个漂亮的moviesDirectedBy方法完全不依赖于影片的实际存储方式。所以,这个方法只能引用一个finder对象,而finder对象则必须知道如何对findAll 方法作出回应。为了帮助读者更清楚地理解,我给finder定义了一个接口: public interface MovieFinder { List findAll(); } 现在,两个对象之间没有什么耦合关系。但是,当我要实际寻找影片时,就必须涉及到MovieFinder的某个具体子类。在这里,我把涉及具体子类的代码放在MovieLister类的构造函数中。 class MovieLister... private MovieFinder finder; public MovieLister() { finder = new ColonDelimitedMovieFinder("movies1.txt"); } 这个实现类的名字就说明:我将要从一个逗号分隔的文本文件中获得影片列表。你不必操心具体的实现细节,只要设想这样一个实现类就可以了。如果这个类只由我自己使用,一切都没问题。但是,如果我的朋友叹服于这个精彩的功能,也想使用我的程序,那又会怎么样呢?如果他们也把影片清单保存在一个逗号分隔的文本文件中,并且也把这个文件命名为“ movie1.txt ”,那么一切还是没问题。如果他们只是给这个文件改改名,我也可以从一个配置文件获得文件名,这也很容易。但是,如果他们用完全不同的方式——例如SQL 数据库、XML 文件、web service,或者另一种格式的文本文件——来存储影片清单呢?在这种情况下,我们需要用另一个类来获取数据。由于已经定义了MovieFinder接口,我可以不用修改moviesDirectedBy方法。但是,我仍然需要通过某种途径获得合适的MovieFinder实现类的实例。 图1:在MovieLister 类中直接创建MovieFinder 实例时的依赖关系 控制反转 依赖注入的几种形式 图2:引入依赖注入器之后的依赖关系 class MovieLister... public MovieLister(MovieFinder finder) { this.finder = finder; } MovieFinder实例本身也将由PicoContainer来管理,因此文本文件的名字也可以由容器注入: class ColonMovieFinder... public ColonMovieFinder(String filename) { this.filename = filename; } 随后,需要告诉PicoContainer:各个接口分别与哪个实现类关联、将哪个字符串注入MovieFinder组件。 private MutablePicoContainer configureContainer() { MutablePicoContainer pico = new DefaultPicoContainer(); Parameter[] finderParams = {new ConstantParameter("movies1.txt")}; pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder.class,finderParams); pico.registerComponentImplementation(MovieLister.class); return pico; } 这段配置代码通常位于另一个类。对于我们这个例子,使用我的MovieLister 类的朋友需要在自己的设置类中编写合适的配置代码。当然,还可以将这些配置信息放在一个单独的配置文件中,这也是一种常见的做法。你可以编写一个类来读取配置文件,然后对容器进行合适的设置。尽管PicoContainer本身并不包含这项功能,但另一个与它关系紧密的项目NanoContainer提供了一些包装,允许开发者使用XML配置文件保存配置信息。NanoContainer能够解析XML文件,并对底下的PicoContainer进行配置。这个项目的哲学观念就是:将配置文件的格式与底下的配置机制分离开。 使用这个容器,你写出的代码大概会是这样: public void testWithPico() { MutablePicoContainer pico = configureContainer(); MovieLister lister = (MovieLister)pico.getComponentInstance(MovieLister.class); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West",movies[0].getTitle()); } 尽管在这里我使用了构造函数注入,实际上PicoContainer也支持设值方法注入,不过该项目的开发者更推荐使用构造函数注入。 使用Spring 进行设值方法注入 class MovieLister... private MovieFinder finder; public void setFinder(MovieFinder finder) { this.finder = finder; } 类似地,在MovieFinder的实现类中,我也定义了一个设值方法,接受类型为String 的参数: class ColonMovieFinder... public void setFilename(String filename) { this.filename = filename; } 第三步是设定配置文件。Spring 支持多种配置方式,你可以通过XML 文件进行配置,也可以直接在代码中配置。不过,XML 文件是比较理想的配置方式。 <beans> <bean id="MovieLister" class="spring.MovieLister"> <property name="finder"> <ref local="MovieFinder"/> </property> </bean> <bean id="MovieFinder" class="spring.ColonMovieFinder"> <property name="filename"> <value>movies1.txt</value> </property> </bean> </beans> 于是,测试代码大概就像下面这样: public void testWithSpring() throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml"); MovieLister lister = (MovieLister) ctx.getBean("MovieLister"); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West",movies[0].getTitle()); } 接口注入 除了前面两种注入技术,还可以在接口中定义需要注入的信息,并通过接口完成注入。Avalon框架就使用了类似的技术。在这里,我首先用简单的范例代码说明它的用法,后面还会有更深入的讨论。首先,我需要定义一个接口,组件的注入将通过这个接口进行。在本例中,这个接口的用途是将一个MovieFinder实例注入继承了该接口的对象。 public interface InjectFinder { void injectFinder(MovieFinder finder); } 这个接口应该由提供MovieFinder接口的人一并提供。任何想要使用MovieFinder实例的类(例如MovieLister类)都必须实现这个接口。 class MovieLister implements InjectFinder... public void injectFinder(MovieFinder finder) { this.finder = finder; } 然后,我使用类似的方法将文件名注入MovieFinder的实现类: public interface InjectFilename { void injectFilename (String filename); } class ColonMovieFinder implements MovieFinder,InjectFilename... public void injectFilename(String filename) { this.filename = filename; } 现在,还需要用一些配置代码将所有的组件实现装配起来。简单起见,我直接在代码中完成配置,并将配置好的MovieLister 对象保存在名为lister的字段中: class IfaceTester... private MovieLister lister; private void configureLister() { ColonMovieFinder finder = new ColonMovieFinder(); finder.injectFilename("movies1.txt"); lister = new MovieLister(); lister.injectFinder(finder); } 测试代码则可以直接使用这个字段: class IfaceTester... public void testIface() { configureLister(); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West",movies[0].getTitle()); } 使用Service Locator 依赖注入的最大好处在于:它消除了MovieLister类对具体MovieFinder实现类的依赖。这样一来,我就可以把MovieLister类交给朋友,让他们根据自己的环境插入一个合适的MovieFinder实现即可。不过,Dependency Injection模式并不是打破这层依赖关系的唯一手段,另一种方法是使用Service Locator模式。 Service Locator模式背后的基本思想是:有一个对象(即服务定位器)知道如何获得一个应用程序所需的所有服务。也就是说,在我们的例子中,服务定位器应该有一个方法,用于获得一个MovieFinder实例。当然,这不过是把麻烦换了一个样子,我们仍然必须在MovieLister中获得服务定位器,最终得到的依赖关系如图3 所示: 图3:使用Service Locator 模式之后的依赖关系 在这里,我把ServiceLocator类实现为一个Singleton的注册表,于是MovieLister就可以在实例化时通过ServiceLocator获得一个MovieFinder实例。 class MovieLister... MovieFinder finder = ServiceLocator.movieFinder(); class ServiceLocator... public static MovieFinder movieFinder() { return soleInstance.movieFinder; } private static ServiceLocator soleInstance; private MovieFinder movieFinder; 和注入的方式一样,我们也必须对服务定位器加以配置。在这里,我直接在代码中进行配置,但设计一种通过配置文件获得数据的机制也并非难事。 class Tester... private void configure() { ServiceLocator.load(new ServiceLocator( new ColonMovieFinder("movies1.txt"))); } class ServiceLocator... public static void load(ServiceLocator arg) { soleInstance = arg; } public ServiceLocator(MovieFinder movieFinder) { this.movieFinder = movieFinder; } 下面是测试代码: class Tester... public void testSimple() { configure(); MovieLister lister = new MovieLister(); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West",movies[0].getTitle()); } 我时常听到这样的论调:这样的服务定位器不是什么好东西,因为你无法替换它返回的服务实现,从而导致无法对它们进行测试。当然,如果你的设计很糟糕,你的确会遇到这样的麻烦;但你也可以选择良好的设计。在这个例子中,ServiceLocator实例仅仅是一个简单的数据容器,只需要对它做一些简单的修改,就可以让它返回用于测试的服务实现。 对于更复杂的情况,我可以从ServiceLocator派生出多个子类,并将子类型的实例传递给注册表的类变量。另外,我可以修改ServiceLocator的静态方法,使其调用ServiceLocator实例的方法,而不是直接访问实例变量。我还可以使用特定于线程的存储机制,从而提供特定于线程的服务定位器。所有这一切改进都无须修改ServiceLocator的使用者。 一种改进的思路是:服务定位器仍然是一个注册表,但不是Singleton。Singleton的确是实现注册表的一种简单途径,但这只是一个实现时的决定,可以很轻松地改变它。 为定位器提供分离的接口 上面这种简单的实现方式有一个问题:MovieLister类将依赖于整个ServiceLocator类,但它需要使用的却只是后者所提供的一项服务。我们可以针对这项服务提供一个单独的接口,减少MovieLister对ServiceLocator的依赖程度。这样一来,MovieLister就不必使用整个的ServiceLocator 接口,只需声明它想要使用的那部分接口。 此时,MovieLister 类的提供者也应该一并提供一个定位器接口,使用者可以通过这个接口获得MovieFinder实例。 public interface MovieFinderLocator { public MovieFinder movieFinder(); 真实的服务定位器需要实现上述接口,提供访问MovieFinder实例的能力: MovieFinderLocator locator = ServiceLocator.locator(); MovieFinder finder = locator.movieFinder(); public static ServiceLocator locator() { return soleInstance; } public MovieFinder movieFinder() { return movieFinder; } private static ServiceLocator soleInstance; private MovieFinder movieFinder; 你应该已经注意到了:由于想要使用接口,我们不能再通过静态方法直接访问服务——我们必须首先通过ServiceLocator类获得定位器实例,然后使用定位器实例得到我们想要的服务。 动态服务定位器 上面是一个静态定位器的例子——对于你所需要的每项服务,ServiceLocator类都有对应的方法。这并不是实现服务定位器的唯一方式,你也可以创建一个动态服务定位器,你可以在其中注册需要的任何服务,并在运行期决定获得哪一项服务。 在本例中,ServiceLocator使用一个map来保存服务信息,而不再是将这些信息保存在字段中。此外,ServiceLocator还提供了一个通用的方法,用于获取和加载服务对象。 class ServiceLocator... private static ServiceLocator soleInstance; public static void load(ServiceLocator arg) { soleInstance = arg; } 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); } 同样需要对服务定位器进行配置,将服务对象与适当的关键字加载到定位器中: class Tester... private void configure() { ServiceLocator locator = new ServiceLocator(); locator.loadService("MovieFinder",new ColonMovieFinder("movies1.txt")); ServiceLocator.load(locator); } 我使用与服务对象类名称相同的字符串作为服务对象的关键字: class MovieLister... MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder"); 总体而言,我不喜欢这种方式。无疑,这样实现的服务定位器具有更强的灵活性,但它的使用方式不够直观明朗。我只有通过文本形式的关键字才能找到一个服务对象。相比之下,我更欣赏通过一个方法明确获得服务对象的方式,因为这让使用者能够从接口定义中清楚地知道如何获得某项服务。 用Avalon 兼顾服务定位器和依赖注入 Dependency Injection和Service Locator两个模式并不是互斥的,你可以同时使用它们,Avalon框架就是这样的一个例子。Avalon使用了服务定位器,但如何获得定位器的信息则是通过注入的方式告知组件的。 对于前面一直使用的例子,Berin Loritsch发送给了我一个简单的Avalon实现版本: public class MyMovieLister implements MovieLister,Serviceable { private MovieFinder finder; public void service( ServiceManager manager ) throws ServiceException { finder = (MovieFinder)manager.lookup("finder"); } service方法就是接口注入的例子,它使容器可以将一个ServiceManager对象注入MyMovieLister对象。ServiceManager则是一个服务定位器。在这个例子中,MyMovieLister并不把ServiceManager对象保存在字段中,而是马上借助它找到MovieFinder 实例,并将后者保存起来。 作出一个选择 代码配置 vs. 配置文件 分离配置与使用 结论和思考 致谢在此,我要向帮助我理解本文中所提到的问题、并对本文提出宝贵意见的几个人表示感谢,他们是Rod Johnson、Paul Hammant、Joe Walnes、Aslak Hellesoy、Jon Tirsen和Bill Caputo。另外,Berin Loritsch和Hamilton Verissimo de Oliveira在Avalon方面给了我非常有用的建议,一并向他们表示感谢。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |