Java 控制反转和依赖注入模式【翻译】【整理】
Inversion of Control Containers and the Dependency Injection pattern
——Martin Fowler
本文内容
- Component and Service(组件和服务)
- A Naive Example(一个超级简单的例子)
- Inversion of Control(控制反转)
- Forms of Dependency Injection(依赖注入的几种形式)
- Constructor Injection with PicoContainer(用 PicoContainer 进行构造函数注入)
- Setter Injection with Spring(用 Spring 进行 Setter 方法注入)
- Interface Injection(接口注入)
- Using a Service Locator(使用服务定位器)
- Using a Segregated Interface for the Locator(为定位器提供分离的接口)
- A Dynamic Service Locator(动态服务定位器)
- Using both a locator and injection with Avalon(用 Avalon 同时使用定位器和依赖注入)
- Deciding which option to use(作出选择)
- Service Locator vs Dependency Injection(服务定位器 vs. 依赖注入)
- Constructor versus Setter Injection(构造函数注入 vs. 设值方法注入)
- Code or configuration files(代码配置 vs. 配置文件)
- Separating Configuration from Use(分离配置与使用)
- Some further issues(更多的问题)
- Concluding Thoughts(结论和思考)
- 参考资料
看了几篇关于“依赖注入”的文章,无论是百度,还是 Wiki 等等,觉得原文说得是最清楚的~你可以下载我实现文中示例的 Demo~
这篇文章的另一个意义在于,如果你看不懂该文的英文,说明你应该补充文中的术语了,看看作者用什么词来描述“控制反转和依赖注入”~
下载 Demo 1(使用 PicoContainer 2.14.1)
下载 Demo 2(使用 PicoContainer 1.2 和 nanocontainer-1.1.2)
In the Java community there's been a rush of lightweight containers that help to assemble components from different projects into a cohesive application. Underlying these containers is a common pattern to how they perform the wiring, a concept they refer under the very generic name of "Inversion of Control". In this article I dig into how this pattern works, under the more specific name of "Dependency Injection", and contrast it with the Service Locator alternative. The choice between them is less important than the principle of separating configuration from use.
Java 社群近来掀起了一阵轻量级容器的热潮,这些容器能够帮助开发者将来自不同项目的组件组装成为一个内聚的应用程序。在这些容器的背后是同一个模式,决定了这些容器进行组件装配的方式。人们称这个模式为“控制反转”( Inversion of Control,IoC)。本文我将深入探索这个模式的工作原理,给它一个更能描述其特点的名字——“依赖注入”(Dependency Injection),并将其与“服务定位器”(Service Locator)模式作一个比较。不过,它们之间的差异并不太重要,重要的是:将组件的配置与使用分离——而这都是两个模式的目标。
One of the entertaining things about the enterprise Java world is the huge amount of activity in building alternatives to the mainstream J2EE technologies, much of it happening in open source. A lot of this is a reaction to the heavyweight complexity in the mainstream J2EE world, but much of it is also exploring alternatives and coming up with creative ideas. A common issue to deal with is how to wire together different elements: how do you fit together this web controller architecture with that database interface backing when they were built by different teams with little knowledge of each other.A number of frameworks have taken a stab at this problem, and several are branching out to provide a general capability to assemble components from different layers. These are often referred to as lightweight containers, examples include PicoContainer, and Spring.
在企业级 Java 的世界里存在一个有趣的现象:有很多人投入大量精力来研究主流 J2EE 技术的替代品——大都发生在 open source 社群。在很大程度上,这可以看作是开发者对主流 J2EE 技术的笨重和复杂作出的回应,但其中的确有很多极富创意的想法,确实提供了一些可供选择的方案。J2EE 开发者常遇到的一个问题就是如何组装不同的程序元素:如果 web 控制器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,你应该如何让它们配合工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的“组装各层组件”的方案。这样的框架通常被称为“轻量级容器”,包括 PicoContainer 和 Spring 等。
Underlying these containers are a number of interesting design principles, things that go beyond both these specific containers and indeed the Java platform. Here I want to start exploring some of these principles. The examples I use are in Java, but like most of my writing the principles are equally applicable to other OO environments, particularly .NET.
这些容器的背后是一些有趣的设计原则,这些原则已经超越了特定容器的范畴,甚至已经超越了Java 平台。下面我就开始揭示这些原则。我使用的范例是 Java 代码,但正如我的大多数文章一样,这些原则也同样适用于其他 OO 环境,特别是 .NET。
Component and Service(组件和服务)
The topic of wiring elements together drags me almost immediately into the knotty terminology problems that surround the terms service and component. You find long and contradictory articles on the definition of these things with ease. For my purposes here are my current uses of these overloaded terms.
“装配程序元素”的话题立即将我拖进了一个棘手的术语问题:如何区分“服务”(service)和“组件”(component)?你可以很容易地找到关于这两个词长篇大论、彼此矛盾的定义。鉴于此,对于这两个遭到了严重滥用的词,我将首先说明它们在本文中的用法。
I use component to mean a glob of software that's intended to be used, without change, by an application that is out of the control of the writers of the component. By 'without change' I mean that the using application doesn't change the source code of the components, although they may alter the component's behavior by extending it in ways allowed by the component writers.
所谓“组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,而后者不能对组件进行修改,也就是说,使用这个组件的应用程序不能修改组件的源代码,但可以通过作者预留的某种途径对其进行扩展以改变组件的行为。
A service is similar to a component in that it's used by foreign applications. The main difference is that I expect a component to be used locally (think jar file, assembly, dll, or a source import). A service will be used remotely through some remote interface, either synchronous or asynchronous (eg web service, messaging system, RPC, or socket.)
服务与组件相似,它们都将被外部的应用程序使用。两者之间最大的差异在于:组件是在本地使用(例如 JAR 文件、程序集、DLL、或者源码导入)。而服务是要通过——同步或异步的——远程接口来远程使用的(例如 Web Service、消息系统、RPC,或者 socket)。
I mostly use service in this article, but much of the same logic can be applied to local components too. Indeed often you need some kind of local component framework to easily access a remote service. But writing "component or service" is tiring to read and write, and services are much more fashionable at the moment.
本文我将主要使用“服务”这个词,但文中的大多数逻辑也同样适用于本地组件。事实上,为了方便地访问远程服务,你往往需要某种本地组件框架。不过,“组件或者服务”这样一个词组实在太麻烦了,而且“服务”这个词当下也很流行,所以本文将用“服务”指代这两者。
A Naive Example(一个超级简单的例子)
To help make all of this more concrete I'll use a running example to talk about all of this. Like all of my examples it's one of those super-simple examples; small enough to be unreal, but hopefully enough for you to visualize what's going on without falling into the bog of a real example.
为了更好地说明问题,我会引入一个例子。与我之前使用的所有例子一样,这是一个超级简单的例子:它非常小,小到不够真实,但足以帮助你看清其中的道理,而不至于陷入真实例子的泥潭。
In this example I'm writing a component that provides a list of movies directed by a particular director. This stunningly useful function is implemented by a single method.
在这个例子中,我编写一个组件,用于提供一份由特定导演执导的电影清单。实现这个功能只需要一个方法:
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()]);
}
The implementation of this function is naive in the extreme, it asks a finder object (which we'll get to in a moment) to return every film it knows about. Then it just hunts through this list to return those directed by a particular director. This particular piece of naivety I'm not going to fix, since it's just the scaffolding for the real point of this article.
这个功能的实现极其简单,moviesDirectedBy 方法首先请求 finder(影片搜寻者)对象(我们稍后会谈到这个对象)以返回它所知道的所有影片,然后遍历 finder 对象,并返回特定导演执导的影片。我只给出一个简单的代码片段。
The real point of this article is this finder object, or particularly how we connect the lister object with a particular finder object. The reason why this is interesting is that I want my wonderful moviesDirectedBy
method to be completely independent of how all the movies are being stored. So all the method does is refer to a finder, and all that finder does is know how to respond to the findAll
method. I can bring this out by defining an interface for the finder.
我们真正想要考察的是 finder 对象,或者说,如何将 MovieLister 对象与特定的 finder 对象连接起来。为什么我们对这个问题特别感兴趣?因为,我希望上面的 moviesDirectedBy 方法完全不依赖于影片的实际存储方式。所以,所有的方法都会引用一个 finder 对象,而 finder 对象则必须知道如何回应 findAll 方法。为了帮助读者更清楚地理解,我给 finder 定义了一个接口:
public interface MovieFinder {
List findAll();
}
Now all of this is very well decoupled, but at some point I have to come up with a concrete class to actually come up with the movies. In this case I put the code for this in the constructor of my lister class.
现在,这两个对象很好地解耦了。但当我要实际寻找影片时,就必须涉及到 MovieFinder 的某个具体子类。在这里,我把涉及具体子类的代码放在 MovieLister 类构造函数中。
class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies.txt");
}
The name of the implementation class comes from the fact that I'm getting my list from a colon delimited text file. I'll spare you the details, after all the point is just that there's some implementation.
ColonDelimitedMovieFinder 这个实现类的名字就说明,我将要从一个逗号分隔的文本文件中获得影片列表。我不会给出该类的具体细节,毕竟这很简单,你设想有这样一个实现就行。
Now if I'm using this class for just myself, this is all fine and dandy. But what happens when my friends are overwhelmed by a desire for this wonderful functionality and would like a copy of my program? If they also store their movie listings in a colon delimited text file called "movies1.txt" then everything is wonderful. If they have a different name for their movies file, then it's easy to put the name of the file in a properties file. But what if they have a completely different form of storing their movie listing: a SQL database, an XML file, a web service, or just another format of text file? In this case we need a different class to grab that data. Now because I've defined a MovieFinder
interface, this won't alter my moviesDirectedBy
method. But I still need to have some way to get an instance of the right finder implementation into place.
现在,如果这个类只是我自己使用,一切都没有问题。但如果我的朋友叹服于这个精彩的功能,也想使用我的程序,那又会怎样呢?如果他们也想把影片清单保存在一个逗号分隔的文本文件中,并且也把这个文件命名为“movie.txt”,那么一切也没问题。如果他们用完全不同的方式,例如 SQL 数据库、XML 文件、Web Service,或是另一种格式的文本文件,来存储影片清单呢?在这种情况下,我们需要用另一个类来获取数据。由于已经定义了 MovieFinder 接口,我可以不用修改 moviesDirectedBy 方法。但我仍然需要通过某种途径来获得合适的 MovieFinder 接口的实例。
Figure 1: The dependencies using a simple creation in the lister class
Figure 1 shows the dependencies for this situation. The MovieLister
class is dependent on both the MovieFinder
interface and upon the implementation. We would prefer it if it were only dependent on the interface, but then how do we make an instance to work with?
图 1 显示了这种情况下的依赖关系。MovieLister 类既依赖于 MovieFinder 接口,也依赖于该接口的具体的实现类。我们当然希望 MovieLister 类仅仅依赖于接口,但又如何获得一个 MovieFinder 子类的实例呢?
也就是说,MovieLister 类依赖于 MovieFinder 接口,还要在该类中实例化该接口,但我们如何做才能不在 MovieLister 类内实例化 MovieFinder 接口,让这个类仅仅依赖与 MovieFinder 接口?
In my book P of EAA, we described this situation as a Plugin. The implementation class for the finder isn't linked into the program at compile time, since I don't know what my friends are going to use. Instead we want my lister to work with any implementation, and for that implementation to be plugged in at some later point, out of my hands. The problem is how can I make that link so that my lister class is ignorant of the implementation class, but can still talk to an instance to do its work.
在 Patterns Of Enterprise Application Architecture 一书中,我们把这种情况称为插件(Plugin)。MovieFinder 的实现类不是在编译阶段连入程序之中的,因为我并不知道我的朋友会使用哪个实现类。我们希望 MovieLister 类能够与 MovieFinder 的任何实现类工作,并且允许在运行时插入具体的实现类,插入动作完全脱离我(原作者)的控制。问题是,如何设计这个连接过程,使 MovieLister 类在不知道实现类细节的前提下与其实例协同工作。
Expanding this into a real system, we might have dozens of such services and components. In each case we can abstract our use of these components by talking to them through an interface (and using an adapter if the component isn't designed with an interface in mind). But if we wish to deploy this system in different ways, we need to use plugins to handle the interaction with these services so we can use different implementations in different deployments.
将这个例子推广到真实的系统,我们可能有数十个这样的服务和组件。在任何时候,我们总可以对组件的使用进行抽象,通过接口与具体的组件交流(如果组件没有设计接口,也可以通过适配器与之交流)。但如果我们希望以不同的方式部署这个系统,就需要用插件机制来处理服务之间的交互过程,这样我们才能在不同的部署方案中使用不同的实现。
So the core problem is how do we assemble these plugins into an application? This is one of the main problems that this new breed of lightweight containers face, and universally they all do it using Inversion of Control.
所以,现在的核心问题是,如何将这些插件装配到一个应用程序?这正是新生的轻量级容器所面临的一个主要问题,而它们解决这个问题的手段无一例外地是控制反转(Inversion Of Control)模式。
Inversion of Control(控制反转)
When these containers talk about how they are so useful because they implement "Inversion of Control" I end up very puzzled. Inversion of control is a common characteristic of frameworks, so saying that these lightweight containers are special because they use inversion of control is like saying my car is special because it has wheels.
几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了控制反转。这样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好像在说我的轿车是与众不同的,因为它有四个轮子。
The question is: "what aspect of control are they inverting?" When I first ran into inversion of control, it was in the main control of a user interface. Early user interfaces were controlled by the application program. You would have a sequence of commands like "Enter name", "enter address"; your program would drive the prompts and pick up a response to each one. With graphical (or even screen based) UIs the UI framework would contain this main loop and your program instead provided event handlers for the various fields on the screen. The main control of the program was inverted, moved away from you to the framework.
问题的关键在于:它们反转了哪方面的控制?我第一次接触到的控制反转是针对用户界面的主控权。早期的用户界面完全是由应用程序来控制的,你预先设计一系列命令,例如输入姓名、输入地址等,应用程序逐条输出提示信息,并取回用户的响应。而在图形用户界面环境下,UI 框架将负责执行一个主循环,你的应用程序只需为屏幕的各个区域提供事件处理函数即可。在这里,程序的控制权发生了反转:从应用程序转移到了框架。
For this new breed of containers the inversion is about how they lookup a plugin implementation. In my naive example the lister looked up the finder implementation by directly instantiating it. This stops the finder from being a plugin. The approach that these containers use is to ensure that any user of a plugin follows some convention that allows a separate assembler module to inject the implementation into the lister.
对于这些新生的容器,它们反转的是如何查找插件的具体实现。在前面的例子中,MovieLister 类直接实例化 MovieFinder。这样,MovieFinder 也就不成其为插件了,因为它不是在运行时插入应用程序中的。而这些轻量级容器则使用了更为灵活的方法,只要插件遵循一定的规则,一个独立的组件模块就能够将插件的具体实现注入到应用程序中。
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
因此,我想我们需要给这个模式起一个更能说明其特点的名字——“控制反转”,这个名字太宽泛了,常常让人有些迷惑。与多位 IoC 爱好者讨论后,我们决定将这个模式叫做“依赖注入”(Dependency Injection)。
I'm going to start by talking about the various forms of dependency injection, but I'll point out now that that's not the only way of removing the dependency from the application class to the plugin implementation. The other pattern you can use to do this is Service Locator, and I'll discuss that after I'm done with explaining Dependency Injection.
下面,我将开始介绍 Dependency Injection 的几种不同形式。不过,在此之前,我要先指出,要消除应用程序对插件实现的依赖,依赖注入并不是唯一的选择,你也可以用 Service Locator 模式获得同样的效果。介绍完 Dependency Injection 模式之后,我会谈到 Service Locator 模式。
Forms of Dependency Injection(依赖注入的几种形式)
The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface, resulting in a dependency diagram along the lines of Figure 2.
Dependency Injection 模式的基本思想是,用一个单独的对象(装配器)来获得 MovieFinder 的一个合适的实现,并将其实例赋给 MovieLister 类的一个字段。这样,我们就得到了图 2 所示的依赖图。
Figure 2: The dependencies for a Dependency Injector
There are three main styles of dependency injection. The names I'm using for them are Constructor Injection, Setter Injection, and Interface Injection. If you read about this stuff in the current discussions about Inversion of Control you'll hear these referred to as type 1 IoC (interface injection), type 2 IoC (setter injection) and type 3 IoC (constructor injection). I find numeric names rather hard to remember, which is why I've used the names I have here.
依赖注入的形式主要有三种,我们分别将它们叫做构造函数注入(Constructor Injection)、设置方法注入(Setter Injection)和接口注入(Interface Injection)。如果读过最近关于 IoC 的一些讨论材料,你不难看出,这三种方法分别就是 type 1 IoC(接口注入)、type 2 IoC(设置方法注入)和 type 3 IoC(构造函数注入)。我发现数字编号往往比较难记,所以我使用了这里的命名方式。
Constructor Injection with PicoContainer(用 PicoContainer 进行构造函数注入)
I'll start with showing how this injection is done using a lightweight container called PicoContainer. I'm starting here primarily because several of my colleagues at ThoughtWorks are very active in the development of PicoContainer (yes, it's a sort of corporate nepotism.)
首先,我要展示如何用轻量级容器 PicoContainer 完成依赖注入。之所以从它开始,主要是因为我在 ThoughtWorks 公司的几个同事在 PicoContainer 的开发社区中非常活跃(没错,也可以说是某种偏袒吧)。
PicoContainer uses a constructor to decide how to inject a finder implementation into the lister class. For this to work, the movie lister class needs to declare a constructor that includes everything it needs injected.
PicoContainer 使用构造函数来决定如何将 MovieFinder 实例注入 MovieLister 类。因此,MovieLister 类必须声明一个包含所有需要注入的元素的构造函数。
也就是说,所有需要注入的元素,是构造函数的形式参数。
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
The finder itself will also be managed by the pico container, and as such will have the filename of the text file injected into it by the container.
MovieFinder 实例本身也将由 PicoContainer 来管理,因此本文文件的名字也可以由容器注入:
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
The pico container then needs to be told which implementation class to associate with each interface, and which string to inject into the finder.
然后,需要告诉 pico 容器每个接口分别与哪个实现类相关联,将什么字符串注入到 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;
}
This configuration code is typically set up in a different class. For our example, each friend who uses my lister might write the appropriate configuration code in some setup class of their own. Of course it's common to hold this kind of configuration information in separate config files. You can write a class to read a config file and set up the container appropriately. Although PicoContainer doesn't contain this functionality itself, there is a closely related project called NanoContainer that provides the appropriate wrappers to allow you to have XML configuration files. Such a nano container will parse the XML and then configure an underlying pico container. The philosophy of the project is to separate the config file format from the underlying mechanism.
这段配置代码通常位于另一个类。对于我们这个例子,使用我 MovieLister 类的朋友需要在自己的设置类中编写合适的配置代码。当然,还可以将这些配置信息放在一个单独的配置文件中,这也是一种常见的做法。你可以编写一个类来读取配置文件,然后对容器进行适当的设置。尽管 PicoContainer 本身并不包含这项功能,但另一个与它关系密切的项目 NanoContainer 提供了一些包装,允许开发者使用 XML 配置文件保存配置信息。NanoContainer 能够解析 XML 文件,并对底下的 PicoContainer 进行配置。这个项目的哲学就是:将配置文件的格式与底下的配置机制分离。
To use the container you write code something like this.
使用这个容器,你的代码大概是这样:
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());
}
Although in this example I've used constructor injection, PicoContainer also supports setter injection, although its developers do prefer constructor injection.
尽管本例我使用了构造函数注入,实际上 PicoContainer 也支持设置方法注入,不过该项目的开发者更推荐使用构造函数注入。
Setter Injection with Spring(用 Spring 进行 Setter 方法注入)
The Spring framework is a wide ranging framework for enterprise Java development. It includes abstraction layers for transactions, persistence frameworks, web application development and JDBC. Like PicoContainer it supports both constructor and setter injection, but its developers tend to prefer setter injection - which makes it an appropriate choice for this example.
Spring 是一个用途广泛的企业级 Java 开发框架,其中包括了针对事务、持久化、Web 应用开发和 JDBC 等常用功能的抽象。与 PicoContainer 一样,它同时支持构造函数注入和设置方法注入,但该项目的开发者更推荐使用 setter 方法注入——恰好适合这个例子。
To get my movie lister to accept the injection I define a setting method for that service
为了让 MovieLister 类接受注入,我需要为它定义一个 setter 方法,该方法接受类型为 MovieFinder 的参数:
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
Similarly I define a setter for the filename.
类似地,在 MovieFinder 的实现类中,也定义了一个 setter 方法,接受类型为 String:
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
The third step is to set up the configuration for the files. Spring supports configuration through XML files and also through code, but XML is the expected way to do it.
第三步是设置配置文件。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>
The test then looks like this.
于是,测试代码大概像下面这样:
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());
}
Interface Injection(接口注入)
The third injection technique is to define and use interfaces for the injection. Avalon is an example of a framework that uses this technique in places. I'll talk a bit more about that later, but in this case I'm going to use it with some simple sample code.
除了前面两种注入技术,还可以在接口中定义需要注入的信息,并通过接口完成注入。Avalon 框架就使用了类似的技术。在这里,我首先用简单的范例代码说明它的用法,后面还会有更深入的讨论。
With this technique I begin by defining an interface that I'll use to perform the injection through. Here's the interface for injecting a movie finder into an object.
首先,我需要定义一个接口,组件的注入将通过这个接口进行。在本例中,这个接口的用途是将一个 MovieFinder 实例注入继承了该接口的对象。
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
This interface would be defined by whoever provides the MovieFinder interface. It needs to be implemented by any class that wants to use a finder, such as the lister.
这个接口应该由提供 MovieFinder 接口的人一并提供。任何想要使用 MovieFinder 实例的类,例如 MovieLister,都必须实现这个接口。
class MovieLister implements InjectFinder
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
I use a similar approach to inject the filename into the finder implementation.
然后,我使用类似的方法将文件名注入 MovieFinder 的实现类:
public interface InjectFinderFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename...
public void injectFilename(String filename) {
this.filename = filename;
}
Then, as usual, I need some configuration code to wire up the implementations. For simplicity's sake I'll do it in code.
现在,还需要用一些配置代码将所有的组件实现装配起来。简单起见,我直接在代码中完成配置,并将配置好的 MovieLister 对象保存在名为 lister 的字段中:
class Tester...
private Container container;
private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
This configuration has two stages, registering components through lookup keys is pretty similar to the other examples.
这个配置分两个阶段,通过查找键来注册组件,跟其他示例相当类似。
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
A new step is to register the injectors that will inject the dependent components. Each injection interface needs some code to inject the dependent object. Here I do this by registering injector objects with the container. Each injector object implements the injector interface.
接下来是注册注入,注入独立的组件。每次注射接口需要一些代码注入依赖对象。本例,用 container 注册注入组件。每个注入对象都实现了注入接口。
class Tester...
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
public interface Injector {
public void inject(Object target);
}
When the dependent is a class written for this container, it makes sense for the component to implement the injector interface itself, as I do here with the movie finder. For generic classes, such as the string, I use an inner class within the configuration code.
当依赖是这个容器写一个类,它是有道理的组件来实现喷油器界面本身,正如我在这里做的电影取景。对于泛型类,如字符串,我在配置代码中使用内部类。
class ColonMovieFinder implements Injector...
public void inject(Object target) {
((InjectFinder) target).injectFinder(this);
}
class Tester...
public static class FinderFilenameInjector implements Injector {
public void inject(Object target) {
((InjectFinderFilename)target).injectFilename("movies1.txt");
}
}
The tests then use the container.
class IfaceTester...
public void testIface() {
configureContainer();
MovieLister lister = (MovieLister)container.lookup("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
The container uses the declared injection interfaces to figure out the dependencies and the injectors to inject the correct dependents. (The specific container implementation I did here isn't important to the technique, and I won't show it because you'd only laugh.)
Using a Service Locator(使用服务定位器)
The key benefit of a Dependency Injector is that it removes the dependency that the MovieLister
class has on the concrete MovieFinder
implementation. This allows me to give listers to friends and for them to plug in a suitable implementation for their own environment. Injection isn't the only way to break this dependency, another is to use a service locator.
依赖注入的最大好处在于:它消除了 MovieLister 类对具体 MovieFinder 实现类的依赖。这样,我就可以把 MovieLister 类交给朋友,让他们根据自己环境插入一个合适的 MovieFinder 实现即可。不过,Dependency Injection 模式并不是打破这层依赖关系的唯一手段,另一种方法是使用 Service Locator 模式。
The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need. So a service locator for this application would have a method that returns a movie finder when one is needed. Of course this just shifts the burden a tad, we still have to get the locator into the lister, resulting in the dependencies of Figure 3
Service Locator 模式背后的基本思想是,有一个对象(即服务定位器)知道如何获得一个应用程序所需的所有服务。也就是说,在我们的例子中,服务定位器应该有一个方法,用于获取一个 MovieFinder 实例。当然,这不过是把麻烦换了一个样子而已,我们任然必须在 MovieLister 中获得服务定位器,最终得到的依赖关系如图 3 所示:
Figure 3: The dependencies for a Service Locator
In this case I'll use the ServiceLocator as a singleton Registry. The lister can then use that to get the finder when it's instantiated.
在这里,我把 ServiceLocator 类实现为一个 Singleton 的注册表,于是 MovieLister 就可以在实例化时通过 Service Locator 获得一个 MovieFinder 实例。
class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
As with the injection approach, we have to configure the service locator. Here I'm doing it in code, but it's not hard to use a mechanism that would read the appropriate data from a configuration file.
与注入方式一样,我们也必须对服务定位器加以配置。这里,我直接在代码中进行配置,但设计一种通过配置文件获得数据的机制也并非难事。
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;
}
Here's the test code.
下面是测试代码:
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());
}
I've often heard the complaint that these kinds of service locators are a bad thing because they aren't testable because you can't substitute implementations for them. Certainly you can design them badly to get into this kind of trouble, but you don't have to. In this case the service locator instance is just a simple data holder. I can easily create the locator with test implementations of my services.
我时常听到这样的论调:这样的服务定位器不是什么好东西,因为你无法替换它返回的服务实现,从而导致无法对它们进行测试。当然,如果你的设计很糟糕,你的确会遇到这样的麻烦;但你也可以选择良好的设计。在这个例子中,ServiceLocator 实例仅仅是一个简单的数据容器,只需要对它做一些简单的修改,就可以让他返回用于测试的服务实现。
For a more sophisticated locator I can subclass service locator and pass that subclass into the registry's class variable. I can change the static methods to call a method on the instance rather than accessing instance variables directly. I can provide thread–specific locators by using thread–specific storage. All of this can be done without changing clients of service locator.
对于更复杂的情况,我可以从 ServiceLocator 派生出多个子类,并将子类的实例传递给注册表的类变量。另外,我可以修改 ServiceLocator 的静态方法,使其调用 ServiceLocator 实例的方法,而不是直接访问实例变量。我还可以使用特定于线程的存储机制,从而提供特定于线程的服务定位器。所有这一切改进都无需修改 ServiceLocator 的使用者。
A way to think of this is that service locator is a registry not a singleton. A singleton provides a simple way of implementing a registry, but that implementation decision is easily changed.
一个改进的思路是,服务定位器仍然是一个注册表,但不是 Singleton。Singleton 的确是实现注册表的一种简单途径,但这只是一个实现时的决定,可以很轻松地改变它。
Using a Segregated Interface for the Locator(为定位器提供分离的接口)
One of the issues with the simple approach above, is that the MovieLister is dependent on the full service locator class, even though it only uses one service. We can reduce this by using a role interface. That way, instead of using the full service locator interface, the lister can declare just the bit of interface it needs.
上面这种简单的实现方式有一个问题,MovieLister 类将依赖于整个 ServiceLocator 类,但它需要使用的却只是后者提供的一项服务。我们可以针对这项服务提供一个单独的接口,减少 MovieLister 对 ServiceLocator 的依赖程度。这样一来,MovieLister就不必使用整个的 ServiceLocator 接口,只需要声明它想要使用的那部分接口。
In this situation the provider of the lister would also provide a locator interface which it needs to get hold of the finder.
此时,MovieLister 类的提供者也应该一并提供一个定位器接口,使用者可以通过这个接口获得 MovieFinder 实例。
public interface MovieFinderLocator {
public MovieFinder movieFinder();
}
The locator then needs to implement this interface to provide access to a finder.
真实的服务定位器需要实现上述接口,提供访问 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;
You'll notice that since we want to use an interface, we can't just access the services through static methods any more. We have to use the class to get a locator instance and then use that to get what we need.
你应该已经注意到了,由于想要使用接口,我们不能再通过静态方法直接访问服务——我么必须首先通过 ServiceLocator 类获得定位器实例,然后使用定位器实例得到我们想要的服务。
A Dynamic Service Locator(动态服务定位器)
The above example was static, in that the service locator class has methods for each of the services that you need. This isn't the only way of doing it, you can also make a dynamic service locator that allows you to stash any service you need into it and make your choices at runtime.
上面是一个静态定位器的例子,对于你所需要的每项服务,ServiceLocator 类都有对应的方法。这并不是实现服务定位器的唯一方式,你也可以创建一个动态服务定位器,你可以在其中注册需要的任何服务,并在运行时决定获得哪像服务。
In this case, the service locator uses a map instead of fields for each of the services, and provides generic methods to get and load services.
在本例中,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);
}
Configuring involves loading a service with an appropriate key.
同样需要对服务定位器进行配置,将服务对象与适当的关键字加载到定位器中:
class Tester...
private void configure() {
ServiceLocator locator = new ServiceLocator();
locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
ServiceLocator.load(locator);
}
I use the service by using the same key string.
我使用与服务对象类名称相同的字符串作为服务对象的关键字:
class MovieLister...
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
On the whole I dislike this approach. Although it's certainly flexible, it's not very explicit. The only way I can find out how to reach a service is through textual keys. I prefer explicit methods because it's easier to find where they are by looking at the interface definitions.
总体而言,我不喜欢这种方式。无疑,这样实现的服务定位器具有更强的灵活性,但它的使用方式不够直观明朗。我只有通过文本形式的关键字才能找到一个服务对象。相比之下,我更欣赏通过一个方法获得服务对象的方式,因为这让使用者能够从接口定义中清楚地知道如何获得某项服务。
Using both a locator and injection with Avalon(用 Avalon 同时使用定位器和依赖注入)
Dependency injection and a service locator aren't necessarily mutually exclusive concepts. A good example of using both together is the Avalon framework. Avalon uses a service locator, but uses injection to tell components where to find the locator.
Dependency Injection 和 Service Locator 两个模式并不是互斥的,你可以同时使用它们,Avalon 框架就是这样的一个例子。Avalon 使用了服务定位器,但如何获得定位器的信息则是通过注入的方式告知组件的。
Berin Loritsch sent me this simple version of my running example using 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");
}
}
The service method is an example of interface injection, allowing the container to inject a service manager into MyMovieLister. The service manager is an example of a service locator. In this example the lister doesn't store the manager in a field, instead it immediately uses it to lookup the finder, which it does store.
service 方法就是接口注入的例子,它使容器可以将一个 ServiceManager 对象注入 MyMovieLister 对象。ServiceManager 则是一个服务定位器。在这个例子中,MyMovieLister 并不把 ServiceManager 对象保存在字段中,而是马上借助它找到MovieFinder 实例,并将后者保存起来。
Deciding which option to use(作出选择)
So far I've concentrated on explaining how I see these patterns and their variations. Now I can start talking about their pros and cons to help figure out which ones to use and when.
到现在为止,我一直在阐述自己对这两个模式(Dependency Injection 模式和 Service Locator 模式)以及它们的变化形式的看法。现在,我要开始讨论他们的优点和缺点,以便指出它们各自适用的场景。
Service Locator vs Dependency Injection(服务定位器 vs. 依赖注入)
The fundamental choice is between Service Locator and Dependency Injection. The first point is that both implementations provide the fundamental decoupling that's missing in the naive example - in both cases application code is independent of the concrete implementation of the service interface. The important difference between the two patterns is about how that implementation is provided to the application class. With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class - hence the inversion of control.
首先, 我们面临 Service Locator 和 Dependency Injection 之间的选择。 应该注意, 尽管我们前面那个简单的例子不足以表现出来, 实际上这两个模式都提供了基本的解耦合能力。无论使用哪个模式, 应用程序代码都不依赖于服务接口的具体实现。两者之间最重要的区别在于:具体实现以什么方式提供给应用程序代码。使用 Service Locator 模式时,应用程序代码直接向服务定位器发送一个消息,明确要求服务的实现;使用 Dependency Injection 模式时,应用程序代码不发出显式的请求,服务的实现自然会出现在应用程序代码中,这也就是所谓控制反转。
Inversion of control is a common feature of frameworks, but it's something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it unless I need it. This isn't to say it's a bad thing, just that I think it needs to justify itself over the more straightforward alternative.
控制反转是框架的共同特征,但它也要求你付出一定的代价:它会增加理解的难度,并且给调试带来一定的困难。所以,整体来说,除非必要,否则我会尽量避免使用它。这并不意味着控制反转不好,只是我认为在很多时候使用一个更为直观的方案(例如 Service Locator 模式)会比较合适。
The key difference is that with a Service Locator every user of a service has a dependency to the locator. The locator can hide dependencies to other implementations, but you do need to see the locator. So the decision between locator and injector depends on whether that dependency is a problem.
一个关键的区别在于:使用 Service Locator 模式时,服务的使用者必须依赖于服务定位器。定位器可以隐藏使用者对服务具体实现的依赖,但你必须首先看到定位器本身。所以,问题的答案就很明朗了:选择 Service Locator 还是 Dependency Injection ,取决于对定位器的依赖是否会给你带来麻烦。
Using dependency injection can help make it easier to see what the component dependencies are. With dependency injector you can just look at the injection mechanism, such as the constructor, and see the dependencies. With the service locator you have to search the source code for calls to the locator. Modern IDEs with a find references feature make this easier, but it's still not as easy as looking at the constructor or setting methods.
Dependency Injection 模式可以帮助你看清组件之间的依赖关系:你只需观察依赖注入的机制(例如构造函数),就可以掌握整个依赖关系。而使用 Service Locator 模式时,你就必须在源代码中到处搜索对服务定位器的调用。具备全文检索能力的 IDE 可以略微简化这一工作,但还是不如直接观察构造函数或者设值方法来得轻松。
A lot of this depends on the nature of the user of the service. If you are building an application with various classes that use a service, then a dependency from the application classes to the locator isn't a big deal. In my example of giving a Movie Lister to my friends, then using a service locator works quite well. All they need to do is to configure the locator to hook in the right service implementations, either through some configuration code or through a configuration file. In this kind of scenario I don't see the injector's inversion as providing anything compelling.
这个选择主要取决于服务使用者的性质。如果你的应用程序中有很多不同的类要使用一个服务,那么应用程序代码对服务定位器的依赖就不是什么大问题。在前面的例子中, 我要把MovieLister 类交给朋友去用,这种情况下使用服务定位器就很好:我的朋友们只需要对定位器做一点配置(通过配置文件或者某些配置性的代码),使其提供合适的服务实现就可以了。在这种情况下,我看不出Dependency Injection 模式提供的控制反转有什么吸引人的地方。
The difference comes if the lister is a component that I'm providing to an application that other people are writing. In this case I don't know much about the APIs of the service locators that my customers are going to use. Each customer might have their own incompatible service locators. I can get around some of this by using the segregated interface. Each customer can write an adapter that matches my interface to their locator, but in any case I still need to see the first locator to lookup my specific interface. And once the adapter appears then the simplicity of the direct connection to a locator is beginning to slip.
但是,如果把 MovieLister 看作一个组件,要将它提供给别人写的应用程序去使用,情况就不同了。在这种时候,我无法预测使用者会使用什么样的服务定位器API,每个使用者都可能有自己的服务定位器,而且彼此之间无法兼容。一种解决办法是为每项服务提供单独的接口,使用者可以编写一个适配器,让我的接口与他们的服务定位器相配合。但即便如此,我仍然需要到第一个服务定位器中寻找我规定的接口。而且一旦用上了适配器,服务定位器所提供的简单性就被大
大削弱了。
Since with an injector you don't have a dependency from a component to the injector, the component cannot obtain further services from the injector once it's been configured.
另一方面,如果使用 Dependency Injection 模式,组件与注入器之间不会有依赖关系,因此组件无法从注入器那里获得更多的服务,只能获得配置信息中所提供的那些。这也是 Dependency Injection 模式的局限性之一。
A common reason people give for preferring dependency injection is that it makes testing easier. The point here is that to do testing, you need to easily replace real service implementations with stubs or mocks. However there is really no difference here between dependency injection and service locator: both are very amenable to stubbing. I suspect this observation comes from projects where people don't make the effort to ensure that their service locator can be easily substituted. This is where continual testing helps, if you can't easily stub services for testing, then this implies a serious problem with your design.
人们倾向于使用 Dependency Injection 模式的一个常见理由是:它简化了测试工作。这里的关键是:出于测试的需要,你必须能够轻松地在真实的服务实现与供测试用的伪组件之间切换。但是,如果单从这个角度来考虑,Dependency Injection 模式和 Service Locator 模式其实并没有太大区别: 两者都能够很好地支持伪组件的插入。之所以很多人有 Dependency Injection 模式更利于测试的印象,我猜是因为他们并没有努力保证服务定位器的可替换性。这正是持续测试起作用的地方:如果你不能轻松地用一些伪组件将一个服务架起来以便测试,这就意味着你的设计出现了严重的问题。
Of course the testing problem is exacerbated by component environments that are very intrusive, such as Java's EJB framework. My view is that these kinds of frameworks should minimize their impact upon application code, and particularly should not do things that slow down the edit-execute cycle. Using plugins to substitute heavyweight components does a lot to help this process, which is vital for practices such as Test Driven Development.
当然,如果组件环境具有非常强的侵略性(就像 EJB 框架那样),测试的问题会更加严重。我的观点是:应该尽量减少这类框架对应用程序代码的影响,特别是不要做任何可能使编辑-执行的循环变慢的事情。用插件(plugin)机制取代重量级组件会对测试过程有很大帮助,这正是测试驱动开发(Test Driven Development,TDD)之类实践的关键所在。
So the primary issue is for people who are writing code that expects to be used in applications outside of the control of the writer. In these cases even a minimal assumption about a Service Locator is a problem.
所以,主要的问题在于:代码的作者是否希望自己编写的组件能够脱离自己的控制、被使用在另一个应用程序中。如果答案是肯定的,那么他就不能对服务定位器做任何假设——哪怕最小的假设也会给使用者带来麻烦。
Constructor versus Setter Injection(构造函数注入 vs. 设值方法注入)
For service combination, you always have to have some convention in order to wire things together. The advantage of injection is primarily that it requires very simple conventions - at least for the constructor and setter injections. You don't have to do anything odd in your component and it's fairly straightforward for an injector to get everything configured.
在组合服务时,你总得遵循一定的约定,才可能将所有东西拼装起来。依赖注入的优点主要在于:它只需要非常简单的约定——至少对于构造子注入和设值方法注入来说是这样。相比于这两者,接口注入的侵略性要强得多,比起Service Locator 模式的优势也不那么明显。
Interface injection is more invasive since you have to write a lot of interfaces to get things all sorted out. For a small set of interfaces required by the container, such as in Avalon's approach, this isn't too bad. But it's a lot of work for assembling components and dependencies, which is why the current crop of lightweight containers go with setter and constructor injection.
所以,如果你想要提供一个组件给多个使用者,构造子注入和设值方法注入看起来很有吸引力:你不必在组件中加入什么希奇古怪的东西,注入器可以相当轻松地把所有东西配置起来。
The choice between setter and constructor injection is interesting as it mirrors a more general issue with object-oriented programming - should you fill fields in a constructor or with setters.
设值函数注入和构造子注入之间的选择相当有趣,因为它折射出面向对象编程的一些更普遍的问题:应该在哪里填充对象的字段,构造子还是设值方法?
My long running default with objects is as much as possible, to create valid objects at construction time. This advice goes back to Kent Beck's Smalltalk Best Practice Patterns: Constructor Method and Constructor Parameter Method. Constructors with parameters give you a clear statement of what it means to create a valid object in an obvious place. If there's more than one way to do it, create multiple constructors that show the different combinations.
一直以来,我首选的做法是尽量在构造阶段就创建完整、合法的对象——也就是说,在构造子中填充对象字段。这样做的好处可以追溯到 Kent Beck 在Smalltalk Best Practice Patterns 一书中介绍的两个模式:Constructor Method 和 Constructor Parameter Method。带有参数的构造子可以明确地告诉你如何创建一个合法的对象。如果创建合法对象的方式不止一种,你还可以提供多个构造子,以说明不同的组合方式。
Another advantage with constructor initialization is that it allows you to clearly hide any fields that are immutable by simply not providing a setter. I think this is important - if something shouldn't change then the lack of a setter communicates this very well. If you use setters for initialization, then this can become a pain. (Indeed in these situations I prefer to avoid the usual setting convention, I'd prefer a method like initFoo, to stress that it's something you should only do at birth.)
构造子初始化的另一个好处是:你可以隐藏任何不可变的字段——只要不为它提供设值方法就行了。我认为这很重要:如果某个字段是不应该被改变的,“没有针对该字段的设值方法”就很清楚地说明了这一点。如果你通过设值方法完成初始化,暴露出来的设值方法很可能成为你心头永远的痛。(实际上,在这种时候我更愿意回避通常的设值方法约定,而是使用诸如initFoo 之类的方法名,以表明该方法只应该在对象创建之初调用。)
But with any situation there are exceptions. If you have a lot of constructor parameters things can look messy, particularly in languages without keyword parameters. It's true that a long constructor is often a sign of an over-busy object that should be split, but there are cases when that's what you need.
不过,世事总有例外。如果参数太多,构造子会显得凌乱不堪,特别是对于不支持关键字参数的语言更是如此。的确,如果构造子参数列表太长,通常标志着对象太过繁忙,理应将其拆分成几个对象,但有些时候也确实需要那么多的参数。
If you have multiple ways to construct a valid object, it can be hard to show this through constructors, since constructors can only vary on the number and type of parameters. This is when Factory Methods come into play, these can use a combination of private constructors and setters to implement their work. The problem with classic Factory Methods for components assembly is that they are usually seen as static methods, and you can't have those on interfaces. You can make a factory class, but then that just becomes another service instance. A factory service is often a good tactic, but you still have to instantiate the factory using one of the techniques here.
如果有不止一种的方式可以构造一个合法的对象,也很难通过构造子描述这一信息,因为构造子之间只能通过参数的个数和类型加以区分。这就是 Factory Method 模式适用的场合了,工厂方法可以借助多个私有构造子和设值方法的组合来完成自己的任务。经典 Factory Method 模式的问题在于:它们往往以静态方法的形式出现,你无法在接口中声明它们。你可以创建一个工厂类,但那又变成另一个服务实体了。“工厂服务”是一种不错的技巧,但你仍然需要以某种方式实例化这个工厂对象,问题仍然没有解决。
Constructors also suffer if you have simple parameters such as strings. With setter injection you can give each setter a name to indicate what the string is supposed to do. With constructors you are just relying on the position, which is harder to follow.
如果要传入的参数是像字符串这样的简单类型,构造子注入也会带来一些麻烦。使用设值方法注入时,你可以在每个设值方法的名字中说明参数的用途;而使用构造子注入时,你只能靠参数的位置来决定每个参数的作用,而记住参数的正确位置显然要困难得多。
If you have multiple constructors and inheritance, then things can get particularly awkward. In order to initialize everything you have to provide constructors to forward to each superclass constructor, while also adding you own arguments. This can lead to an even bigger explosion of constructors.
如果对象有多个构造子,对象之间又存在继承关系,事情就会变得特别讨厌。为了让所有东西都正确地初始化,你必须将对子类构造子的调用转发给超类的构造子,然后处理自己的参数。这可能造成构造子规模的进一步膨胀。
Despite the disadvantages my preference is to start with constructor injection, but be ready to switch to setter injection as soon as the problems I've outlined above start to become a problem.
尽管有这些缺陷,但我仍然建议你首先考虑构造子注入。不过,一旦前面提到的问题真的成了问题,你就应该准备转为使用设值方法注入。
This issue has led to a lot of debate between the various teams who provide dependency injectors as part of their frameworks. However it seems that most people who build these frameworks have realized that it's important to support both mechanisms, even if there's a preference for one of them.
在将 Dependecy Injection 模式作为框架的核心部分的几支团队之间,“构造子注入还是设值方法注入”引发了很多的争论。不过,现在看来,开发这些框架的大多数人都已经意识到:不管更喜欢哪种注入机制,同时为两者提供支持都是有必要的。
Code or configuration files(代码配置 vs. 配置文件)
A separate but often conflated issue is whether to use configuration files or code on an API to wire up services. For most applications that are likely to be deployed in many places, a separate configuration file usually makes most sense. Almost all the time this will be an XML file, and this makes sense. However there are cases where it's easier to use program code to do the assembly. One case is where you have a simple application that's not got a lot of deployment variation. In this case a bit of code can be clearer than a separate XML file.
另一个问题相对独立,但也经常与其他问题牵涉在一起:如何配置服务的组装,通过配置文件还是直接编码组装?对于大多数需要在多处部署的应用程序来说,一个单独的配置文件会更合适。配置文件几乎都是XML 文件,XML 也的确很适合这一用途。不过,有些时候直接在程序代码中实现装配会更简单。譬如一个简单的应用程序,也没有很多部署上的变化,这时用几句代码来配置就比XML 文件要清晰得多。
A contrasting case is where the assembly is quite complex, involving conditional steps. Once you start getting close to programming language then XML starts breaking down and it's better to use a real language that has all the syntax to write a clear program. You then write a builder class that does the assembly. If you have distinct builder scenarios you can provide several builder classes and use a simple configuration file to select between them.
与之相对的,有时应用程序的组装非常复杂,涉及大量的条件步骤。一旦编程语言中的配置逻辑开始变得复杂,你就应该用一种合适的语言来描述配置信息,使程序逻辑变得更清晰。然后,你可以编写一个构造器(builder)类来完成装配工作。如果使用构造器的情景不止一种,你可以提供多个构造器类,然后通过一个简单的配置文件在它们之间选择。
I often think that people are over-eager to define configuration files. Often a programming language makes a straightforward and powerful configuration mechanism. Modern languages can easily compile small assemblers that can be used to assemble plugins for larger systems. If compilation is a pain, then there are scripting languages that can work well also.
我常常发现,人们太急于定义配置文件。编程语言通常会提供简捷而强大的配置管理机制,现代编程语言也可以将程序编译成小的模块,并将其插入大型系统中。如果编译过程会很费力,脚本语言也可以在这方面提供帮助。
It's often said that configuration files shouldn't use a programing language because they need to be edited by non-programmers. But how often is this the case? Do people really expect non-programmers to alter the transaction isolation levels of a complex server-side application? Non-language configuration files work well only to the extent they are simple. If they become complex then it's time to think about using a proper programming language.
通常认为,配置文件不应该用编程语言来编写,因为它们需要能够被不懂编程的系统管理人员编辑。但是,这种情况出现的几率有多大呢?我们真的希望不懂编程的系统管理人员来改变一个复杂的服务器端应用程序的事务隔离等级吗?只有在非常简单的时候,非编程语言的配置文件才有最好的效果。如果配置信息开始变得复杂,就应该考虑选择一种合适的编程语言来编写配置文件。
One thing we're seeing in the Java world at the moment is a cacophony of configuration files, where every component has its own configuration files which are different to everyone else's. If you use a dozen of these components, you can easily end up with a dozen configuration files to keep in sync.
在 Java 世界里,我们听到了来自配置文件的不和谐音——每个组件都有它自己的配置文件,而且格式还各各不同。如果你要使用一打这样的组件,你就得维护一打的配置文件,那会很快让你烦死。
My advice here is to always provide a way to do all configuration easily with a programmatic interface, and then treat a separate configuration file as an optional feature. You can easily build configuration file handling to use the programmatic interface. If you are writing a component you then leave it up to your user whether to use the programmatic interface, your configuration file format, or to write their own custom configuration file format and tie it into the programmatic interface.
在这里,我的建议是:始终提供一种标准的配置方式,使程序员能够通过同一个编程接口轻松地完成配置工作。至于其他的配置文件,仅仅把它们当作一种可选的功能。借助这个编程接口,开发者可以轻松地管理配置文件。如果你编写了一个组件,则可以由组件的使用者来选择如何管理配置信息:使用你的编程接口、直接操作配置文件格式,或者定义他们自己的配置文件格式,并将其与你的编程接口相结合。
Separating Configuration from Use(分离配置与使用)
The important issue in all of this is to ensure that the configuration of services is separated from their use. Indeed this is a fundamental design principle that sits with the separation of interfaces from implementation. It's something we see within an object-oriented program when conditional logic decides which class to instantiate, and then future evaluations of that conditional are done through polymorphism rather than through duplicated conditional code.
所有这一切的关键在于:服务的配置应该与使用分开。实际上,这是一个基本的设计原则——分离接口与实现。在面向对象程序里,我们在一个地方用条件逻辑来决定具体实例化哪一个类,以后的条件分支都由多态来实现,而不是继续重复前面的条件逻辑,这就是“分离接口与实现”的原则。
If this separation is useful within a single code base, it's especially vital when you're using foreign elements such as components and services. The first question is whether you wish to defer the choice of implementation class to particular deployments. If so you need to use some implementation of plugin. Once you are using plugins then it's essential that the assembly of the plugins is done separately from the rest of the application so that you can substitute different configurations easily for different deployments. How you achieve this is secondary. This configuration mechanism can either configure a service locator, or use injection to configure objects directly.
如果对于一段代码而言,接口与实现的分离还只是“有用”的话,那么当你需要使用外部元素(例如组件和服务)时,它就是生死攸关的大事。这里的第一个问题是:你是否希望将“选择具体实现类”的决策推迟到部署阶段。如果是,那么你需要使用插入技术。使用了插入技术之后,插件的装配原则上是与应用程序的其余部分分开的,这样你就可以轻松地针对不同的部署替换不同的配置。这种配置机制可以通过服务定位器来实现(Service Locator 模式),也可以借助依赖注入直接完成(Dependency Injection 模式)。
Some further issues(更多的问题)
In this article, I've concentrated on the basic issues of service configuration using Dependency Injection and Service Locator. There are some more topics that play into this which also deserve attention, but I haven't had time yet to dig into. In particular there is the issue of life-cycle behavior. Some components have distinct life-cycle events: stop and starts for instance. Another issue is the growing interest in using aspect oriented ideas with these containers. Although I haven't considered this material in the article at the moment, I do hope to write more about this either by extending this article or by writing another.
在本文中,我关注的焦点是使用 Dependency Injection 模式和 Service Locator 模式进行服务配置的基本问题。还有一些与之相关的话题值得关注,但我已经没有时间继续申发下去了。特别值得注意的是生命周期行为的问题:某些组件具有特定的生命周期事件,例如“停止”、“开始”等等。另一个值得注意的问题是:越来越多的人对“如何在这些容器中运用面向方面(aspect oriented)的思想”产生了兴趣。尽管目前还没有认真准备过这方面的材料,但我也很希望以后能在这个话题上写一些东西。
You can find out a lot more about these ideas by looking at the web sites devoted to the lightweight containers. Surfing from the picocontainer and spring web sites will lead to you into much more discussion of these issues and a start on some of the further issues.
关于这些问题,你在专注于轻量级容器的网站上可以找到很多资料。浏览 PicoContainer(http://www.picocontainer.org)或者Spring(http://www.springframework.org)的网站,你可以找到大量相关的讨论,并由此引申出更多的话题。
Concluding Thoughts(结论和思考)
The current rush of lightweight containers all have a common underlying pattern to how they do service assembly - the dependency injector pattern. Dependency Injection is a useful alternative to Service Locator. When building application classes the two are roughly equivalent, but I think Service Locator has a slight edge due to its more straightforward behavior. However if you are building classes to be used in multiple applications then Dependency Injection is a better choice.
在时下流行的轻量级容器都使用了一个共同的模式来组装应用程序所需的服务,我把这个模式称为 Dependency Injection,它可以有效地替代Service Locator 模式。在开发应用程序时,两者不相上下,但我认为 Service Locator 模式略有优势,因为它的行为方式更为直观。但是,如果你开发的组件要交给多个应用程序去使用,那么 Dependency Injection 模式会是更好的选择。
If you use Dependency Injection there are a number of styles to choose between. I would suggest you follow constructor injection unless you run into one of the specific problems with that approach, in which case switch to setter injection. If you are choosing to build or obtain a container, look for one that supports both constructor and setter injection.
如果你决定使用 Dependency Injection 模式,这里还有几种不同的风格可供选择。我建议你首先考虑构造子注入;如果遇到了某些特定的问题,再改用设值方法注入。如果你要选择一个容器,在其之上进行开发,我建议你选择同时支持这两种注入方式的容器。
The choice between Service Locator and Dependency Injection is less important than the principle of separating service configuration from the use of services within an application.
Service Locator 模式和Dependency Injection 模式之间的选择并是最重要的,更重要的是:应该将服务的配置和应用程序内部对服务的使用分离开。
参考资料
- Inversion of Control Containers and the Dependency Injection pattern
- Wiki 控制反转
- PicoContainer
- NanoContainer
下载 Demo 1(使用 PicoContainer 2.14.1)
下载 Demo 2(使用 PicoContainer 1.2 和 nanocontainer-1.1.2)