Ioc 器管理的应用程序设计,前奏:容器属于哪里? 控制容器的反转和依赖注入模式
Ioc 器管理的应用程序设计,前奏:容器属于哪里?
我将讨论一些我认为应该应用于“容器管理”应用程序设计的原则。
模式1:服务字典
字典或关联数组是我们在软件工程中学到的第一个构造。 很容易看到使用依赖注入组成对象的字典和IoC容器之间的类比:
未使用容器
使用容器
这两看起来不同的是:
一些花哨的新术语'Register'和'Resolve'被使用,而不是索引器访问
记录器是通过反射创建的,在相互依赖的情况下保存一些打字和配置代码
如果你进入一个已经建立了依赖注入容器的项目,并且必须弄清楚如何编写你需要与系统的其他部分进行交互的代码。
模型2:抽象 new()运算符
经常称赞依赖注入容器是为了消除实现类型依赖。回到我们的例子,我们已经提出了一个独立于提供它的实现类型的ILogger请求(在这种情况下为ConsoleLogger)。
ASP.NET MVC等Web框架使用我们的依赖注入容器以类似的方式创建Controller实例。
将容器想象为一个抽象的new()运算符会更有意义?
将上面代码重构成下面代码
下面代码
在这里,我们的容器使用不仅抽象出了我们的MailTransport的具体类型,还负责配置其主机属性。 (配置必须是我们的abstract new()运算符的一个特性,因为实例上的配置参数取决于它的具体类型而不是它提供的服务。)
设计成果:the Global Container
遵循这些思想模式之一鼓励您将容器视为您从中检索的东西。即使是“容器”这个名字也会推动我们朝这个方向发展。
从这个角度来看,应用程序架构设计任务中没有太多的挑战!
我们将在一个类上创建一个静态属性Container,例如Global,并使用Global.Container.Resolve <MyService>()从它获取实例。
这段代码应该熟悉另一个名称的模式:静态服务定位器(the static Service Locator)。
the Global Container 作为服务定位器的问题
这种模式可以被正确地称为反模式有充分的理由:
脆弱性。容器就像一桶全局变量。强制组件公开声明依赖关系所实现的所有健壮性都会丢失。意外的依赖关系可能出现在应用程序的看似无关的部分之间,使维护变得复杂。
降低组合性。当注入依赖关系时,可以将容器配置为向不同的消费者提供不同的实现。组合独立构建的组件时,这通常是必需的。当从全局容器检索依赖项时,容器不知道请求者的身份,并且每次都被迫返回相同的实现。
有限的重用。全局容器所在的位置可能会限制依赖于它的组件的可重用性。将组件迁移到WPF应用程序将需要更改代码,如果它们依赖于附加到HttpApplication类的全局容器。
实施问题。在全球基于容器的解决方案中,并发性,重入性,循环引用检测和组件生命周期管理更加困难/混乱。
恢复控制:一般来说,这些问题的产生是因为直接调用全球容器是恢复控制。在容器管理的应用程序中,容器负责将正确的实例放到正确的位置以完成工作。调用一个全球容器以一种不易管理的方式来承担一部分责任。有时候,由于许多当代框架中的设计决策,the Global Container是必要的,但为了推进软件工程的发展,我们需要超越这一点。
箱子里的思考
上面列出的心理模型应该已经敲响了一些警钟:
该容器是一个服务字典? 不挂断! 如果我两次要求同样的服务,为什么我有时会得到不同的实例?
好问题。 配置一个依赖注入容器很容易,每次解析时都会返回一个新的ConsoleLogger实例。 不是很像字典,是吗?
所以容器真的是一个抽象的new()操作符? 等待! 如果我有时得到相同的共享实例,我何时可以Dispose()它?
这两种模式甚至不兼容。 有时候容器提供了新的实例,但有时从调用中返回的是一个单例,或者是一个将在某种上下文中共享的实例,比如当前事务。
所以事情现在令人困惑。
修订后的思想模型:一个互相关联的对象系统
问题来了:看看Autofac主页上的例子。 它显示了Autofac中如何使用Register()和Resolve()。 这是依赖注入的一个相当常见的例子。 注意缺少了什么?
此示例中最重要的类不是ContainerBuilder或Container。 从应用程序架构的角度来看,我们需要看到Straight6TwinTurbo和Car类!
解析操作只是找到一个独立系统的入口点的一种方法。
整个应用程序驻留在容器中。 除了无论什么入场点滚球,都没有“外线”。 从这个角度理解IoC并不是围绕像Register()和Resolve()这样的外部API。
使用这个模型,容器能够在合适的时间将组件实例放在正确的位置,应用程序设计重新关注组件本身的实现。
设计结果:注入上下文
在实施我们改进的IControllerProvider时,这个世界的新视角给我们带来了挑战。 该服务需要返回(可能)新的,参数化的Controller类型实例。
通常在当今应用中实现的解决方案是允许容器为控制器提供商提供上下文:
我们在这里假设ControllerProvider本身在容器中托管。 IContext是Autofac自动提供给需要它的任何组件的接口。 您可以在任何其他流行的IoC容器中创建和使用等效界面(以下链接了一个MEF示例)。
IContext以可控方式向应用程序提供容器的实例解析功能。 实现IContext的对象提供了容器的服务,但可能不是容器本身。
由于此模式为容器或应用程序开发人员提供了定制或代理发送给任何特定组件的IContext实现的机会,因此与全局容器关联的大多数问题都会得到缓解。
容器管理的应用程序设计
如果你觉得这篇文章及其解释有些不完整,那你是对的。 正如这篇文章的标题所表明的那样,它是关于该主题的一系列介绍。 我没有全部的答案,我甚至没有全部的问题.。
控制容器的反转和依赖注入模式
本文依旧是一篇译文,写于作者在开发.net core 半年后的进阶学习时刻!
这篇文章很长,一口气看完得花二十分钟,大家要做好心理准备!
摘要:Java社群近来掀起了一阵轻量级容器的热潮,这些容器能够帮助开发者将来自不同项目的组件组装成为一个内聚的应用程序。在它们的背后有着同一个模式,这个模式决定了这些容器进行组件装配的方式。人们用一个大而化之的名字来称呼这个模式:”控制反转”(Inversion ofControl,IoC)。在本文中,我将深入探索这个模式的工作原理,给它一个更能描述其特点的名字——”依赖注入”(Dependency Injection),并将其与”服务定位器”(Service Locator)模式作一个比较。不过,这两者之间的差异并不太重要,更重要的是:应该将组件的配置与使用分离开——两个模式的目标都是这个。
在企业级Java的世界里存在一个有趣的现象:有很多人投入很多精力来研究主流J2EE 技术的替代品——自然,这大多发生在open source社群。在很大程度上,这可以看作是开发者对主流J2EE技术的笨重和复杂作出的回应,但其中的确有很多极富创意的想法,的确提供了一些可供选择的方案。J2EE开发者常遇到的一个问题就是如何组装不同的程序元素:如果web控制器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,你应该如何让它们配合工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的”组装各层组件”的方案。这样的框架通常被称为”轻量级容器”,PicoContainer和Spring都在此列中。
在这些容器背后,一些有趣的设计原则发挥着作用。这些原则已经超越了特定容器的范畴,甚至已经超越了Java平台的范畴。在本文中,我就要初步揭示这些原则。我使用的范例是Java代码,但正如我的大多数文章一样,这些原则也同样适用于别的OO环境,特别是.NET。
组件和服务
装配程序元素,这样的话题立即将我拖进了一个棘手的术语问题:如何区分”服务”(service)和”组件”(component)?你可以毫不费力地找出关于这两个词定义的长篇大论,各种彼此矛盾的定义会让你感受到我所处的窘境。有鉴于此,对于这两个遭到了严重滥用的词汇,我将首先说明它们在本文中的用法。
所谓”组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,但后者不能对组件进行修改。也就是说,使用一个组件的应用程序不能修改组件的源代码,但可以通过作者预留的某种途径对其进行扩展,以改变组件的行为。
服务和组件有某种相似之处:它们都将被外部的应用程序使用。在我看来,两者之间最大的差异在于:组件是在本地使用的(例如JAR文件、程序集、DLL、或者源码导入);而服务是要通过同步或异步的远程接口来远程使用的(例如web service、消息系统、RPC,或者socket)。
在本文中,我将主要使用”服务”这个词,但文中的大多数逻辑也同样适用于本地组件。实际上,为了方便地访问远程服务,你往往需要某种本地组件框架。不过,”组件或者服务”这样一个词组实在太麻烦了,而且”服务”这个词当下也很流行,所以本文将用”服务”指代这两者。
一个简单的例子
为了更好地说明问题,我要引入一个例子。和我以前用的所有例子一样,这是一个超级简单的例子:它非常小,小得有点不够真实,但足以帮助你看清其中的道理,而不至于陷入真实例子的泥潭中无法自拔。
在这个例子中,我编写了一个组件,用于提供一份电影清单,清单上列出的影片都是由一位特定的导演执导的。实现这个伟大的功能只需要一个方法:
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 实例时的依赖关系
图1展现了这种情况下的依赖关系:MovieLister类既依赖于MovieFinder接口,也依赖于具体的实现类。我们当然希望MovieLister类只依赖于接口,但我们要如何获得一个MovieFinder子类的实例呢?
在Patterns of Enterprise Application Architecture一书中,我们把这种情况称为插件(plugin):MovieFinder的实现类不是在编译期连入程序之中的,因为我并不知道我的朋友会使用哪个实现类。我们希望MovieLister类能够与MovieFinder的任何实现类配合工作,并且允许在运行期插入具体的实现类,插入动作完全脱离我(原作者)的控制。这里的问题就是:如何设计这个连接过程,使MovieLister类在不知道实现类细节的前提下与其实例协同工作。
将这个例子推而广之,在一个真实的系统中,我们可能有数十个服务和组件。在任何时候,我们总可以对使用组件的情形加以抽象,通过接口与具体的组件交流(如果组件并没有设计一个接口,也可以通过适配器与之交流)。但是,如果我们希望以不同的方式部署这个系统,就需要用插件机制来处理服务之间的交互过程,这样我们才可能在不同的部署方案中使用不同的实现。所以,现在的核心问题就是:如何将这些插件组合成一个应用程序?这正是新生的轻量级容器所面临的主要问题,而它们解决这个问题的手段无一例外地是控制反转(Inversion of Control)模式。
控制反转
几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了控制反转。这样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好象在说我的轿车是与众不同的,因为它有四个轮子。
问题的关键在于:它们反转了哪方面的控制?我第一次接触到的控制反转针对的是用户界面的主控权。早期的用户界面是完全由应用程序来控制的,你预先设计一系列命令,例如输入姓名、输入地址等,应用程序逐条输出提示信息,并取回用户的响应。而在图形用户界面环境下,UI框架将负责执行一个主循环,你的应用程序只需为屏幕的各个区域提供事件处理函数即可。在这里,程序的主控权发生了反转:从应用程序移到了框架。对于这些新生的容器,它们反转的是如何定位插件的具体实现。在前面那个简单的例子中,MovieLister类负责定位MovieFinder的具体实现——它直接实例化后者的一个子类。这样一来,MovieFinder也就不成其为一个插件了,因为它并不是在运行期插入应用程序中的。而这些轻量级容器则使用了更为灵活的办法,只要插件遵循一定的规则,一个独立的组装模块就能够将插件的具体实现注射到应用程序中。因此,我想我们需要给这个模式起一个更能说明其特点的名字——”控制反转”这个名字太泛了,常常让人有些迷惑。与多位IoC 爱好者讨论之后,我们决定将这个模式叫做”依赖注入”(Dependency Injection)。
下面,我将开始介绍Dependency Injection模式的几种不同形式。不过,在此之前,我要首先指出:要消除应用程序对插件实现的依赖,依赖注入并不是唯一的选择,你也可以用ServiceLocator模式获得同样的效果。介绍完Dependency Injection模式之后,我也会谈到ServiceLocator 模式。
依赖注入的几种形式
Dependency Injection模式的基本思想是:用一个单独的对象(装配器)来获得MovieFinder的一个合适的实现,并将其实例赋给MovieLister类的一个字段。这样一来,我们就得到了图2所示的依赖图:
图2:引入依赖注入器之后的依赖关系
依赖注入的形式主要有三种,我分别将它们叫做构造函数注入(Constructor Injection)、属性注入(Setter Injection)和接口注入(Interface Injection)。如果读过最近关于IoC的一些讨论材料,你不难看出:这三种注入形式分别就是type 1 IoC(接口注入)、type 2 IoC(属性注入)和type 3 IoC(构造函数注入)。我发现数字编号往往比较难记,所以我使用了这里的命名方式。
使用PicoContainer 进行构造函数注入
首先,我要向读者展示如何用一个名为PicoContainer的轻量级容器完成依赖注入。之所以从这里开始,主要是因为我在ThoughtWorks公司的几个同事在PicoContainer的开发社群中非常活跃——没错,也可以说是某种偏袒吧。
PicoContainer通过构造函数来判断如何将MovieFinder实例注入MovieLister 类。因此,MovieLister类必须声明一个构造函数,并在其中包含所有需要注入的元素:
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 = {newConstantParameter("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 进行属性注入
Spring 框架是一个用途广泛的企业级Java 开发框架,其中包括了针对事务、持久化框架、web应用开发和JDBC等常用功能的抽象。和PicoContainer一样,它也同时支持构造函数注入和属性注入,但该项目的开发者更推荐使用设值方法注入——恰好适合这个例子。为了让MovieLister类接受注入,我需要为它定义一个属性,该方法接受类型为MovieFinder的参数:
class MovieLister... private MovieFinder finder; public void setFinder(MovieFinder finder) { this.finder = finder; }类似地,在MovieFinder的实现类中,我也定义了一个设值方法(Net 中属性),接受类型为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 = newFileSystemXmlApplicationContext("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()); }
public interface InjectFilename { void injectFilename (String filename); } class ColonMovieFinder implements MovieFinder, InjectFilename... public void injectFilename(String filename) { this.filename = filename; }