控制反转容器&依赖注入模式(转)
本文内容
- 组件&服务
- 简单示例
- 控制反转
- 依赖注入的形式
- 使用PicoContainer进行构造器注入
- 使用Spring框架进行属性注入
- 接口注入
- 使用服务定位器
- 为Locator使用分离接口
- 动态服务定位器
- Avalon:Locator Injection双管齐下
- 选择
- 服务定位 VS 依赖注入
- 构造器注入VS属性注入
- 代码配置还是配置文件
- 将配置从应用中分离
- 更多话题
- 结论
在Java世界的企业级应用中有一个有趣的现象:很多人都在尝试做主流J2EE技术的替代品,这多出现在开源社区。这一方面很大程度上反映了主流J2EE技术的沉重和复杂,另一方面这其中诚然也有很多另辟蹊径极富创意。一个常见的问题就是如何将各种元素组织装配起来:Web控制层和数据接口由不同的团队开发而且团队间彼此知之甚少,你如何从中斡旋使其配合工作?很多框架都曾尝试这个问题,一些还在这个方向做了分支,致力于提供通用的各层组件组装解决方案。这些框架通常被称为轻量级容器,例如:PicoContainer Spring .
这些容器的背后有一些有趣的设计原则做支持,这些原则是不拘泥于具体容器和Java平台的。这里我将对这些原则进行探索,例子是java的但我相信同我的大部分文章一样这些原则也适用于其它面向对象环境,特别是.NET.
组件&服务
将元素装配在一起,这个话题一开始就让我陷入棘手的术语问题:"Service""component"对于这两个概念的定义,你轻而易举找到长篇累牍而观点截然相反的文章。所以我先将二者的使用意图进行澄清。
我这里的“component”组件是指一个软件单元,它可以应用程序被使用但是不能被改变,组件的作者对这个应用程序没有控制权。不能修改我是指不能修改组件的源代码,但我们可以通过作者允许的方式对组件行为进行扩展。(译者注:比如NLog组件的扩展LayoutRenderer)
服务Service的概念和组件类似它是由外部应用程序调用。组件和服务主要的区别在于我认为是:组件是可以本地调用的(可以是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()]);
}
这个功能的实现真的是简单至极了,它通过finder对象(后面会提及)来返回finder所能找到所有的电影列表,然后遍历列表,并返回特定导演的作品。很简单是吧,我不会去改进它,在本文它只是用来说明问题的,仅此而已。
本文真正关心的是finder对象,或者说如何将lister对象和finder对象联系起来。这个问题有趣的原因在于我要期望那个漂亮的movieDirectedBy要独立于影片的存储方式。所以所有方法都会引用一个finder,finder对象可以完成findeAll的功能。我们可以把这个抽取出来做成接口
public interface MovieFinder {
List findAll();
}
现在已经完成了很好的对象解耦,但是我需要用一个实体类来完成电影的查找工作时就要涉及到一个具体类;这里我把代码放在lister的构造器中
//class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
这个实体类的名字就能表达这样一个事实:我们需要从一个逗号分隔的文本文件列表中获得影片列表。具体的实现细节我省略掉了,只要知道这是一种实现方式就可以了。
如果这个类只要我自己用一点问题都没有。但是如果我的一位朋友也惊叹于这个精彩的功能并想使用它,那会怎样呢?如果他们也把影片列表存放在文本文件中并使用逗号分隔,而且把文件名改成“movies1.txt”那么一点问题也没有。如果仅仅是电影列表的名字不同那也没有问题,我可以从配置文件中读取。但是如果他们使用完全不同的存储介质呢?比如SQL数据库,xml文件,Web Service,哪怕只是另外一种格式规则存储的文本文件呢?这样我们就需要一个新的类来获取数据。由于已经抽取了一个MovieFinder接口,我可以不修改moviesDirectedBy 方法,我还是希望通过别的途径获得合适的movieFinder实现类的实例。
图1:在MoiveLister类中简单创建MoiveFinder实例时的依赖关系
上图展现了这种情况下的依赖关系:MovieLister类同时依赖于MoiveFinder接口及其实现。我们当然更期望MoiveLister只依赖于接口,但是我们如何得到一个获得一个MoiveFinder的实例呢?
在我的《企业级应用模式》一书中,我们把这种情况称为插件Plugin:MoiveFinder不是在编译时就加入程序的,因为我不知道我的朋友会怎么用什么样的finder。我想让我的MoiveLister类能与任何MoiveFinder实现配合工作,并且允许在运行时加载完全不用我的控制。现在的问题就是如何设计这个连接使MoiveLister类在不知道实现类具体细节的前提下与其协作。
将这种情况推广到真实系统中,我们或许又数十个服务和组件。每种情况我们都可以把使用组件的形式加以抽象,抽取接口并通过接口与组件进行交互(如果组件没有提供一个接口那么可以通过适配暗度陈仓);但是我们希望用不同的方式部署系统,就需要使用插件方式来处理服务间的交互,这样我们才有可能在不同的部署方案中使用不同的实现。
所以现在核心的问题就是我们如何将这些插件集结在一个应用程序中?这恰恰是新生代轻量级容器面对的主要障碍,而它们都无一例外的选择了控制反转。
控制反转
当这些容器的设计者谈话时会说这些容器是如此的有用,因为他们实现了“控制反转”。而我却深感迷惑,控制反转是框架的共有特征,如果说一个框架以实现了控制反转为特点相当于说我的汽车有轮子。
问题是它们反转了什么?我第一次接触控制反转它关注的是对用户界面的控制。早期的用户界面全由程序控制。你会设计一系列的命令:类似于“请输入姓名”“请输入地址”你的程序会显示提示信息并取得用户响应。在图形化(甚至或者是基于触摸屏)用户界面中,用户界面框架会维护一个主循环,你的应用程序只需要为不同的区域设计事件和处理函数就可以了。这里程序的主要控制就发生了反转:从应用程序转移到了框架。
因而对于新生代的容器,它们要反转的就是如何定位插件的具体实现。在我简单的例子中,MovieLister类负责MovieFinder的定位:它直接就实例化了一个子类。这样依赖,MoiveFinder也就不是插件了,因为它不是在运行时加载的。这些容器的方法是:只要插件遵守一定转化规则那么一个独立的程序集模块便可以注入到lister。
结果就是我需要给这个模式一个更具体的名字。控制反转太宽泛了,因而常常让人迷惑。通过和一些IoC爱好者商讨之后我们命名为依赖注入.
我将开始讨论各式各样的依赖注入,但是要先指出的一点是:要消解应用程序对插件的依赖,依赖注入绝不是不二法门。你也可以通过使用Service Locator模式做到这一点,讨论依赖注入之后之后我们会谈到Service Locator服务定位模式。
依赖注入的形式
依赖注入的基本思想是:有一个独立的对象--一个装配器,它获得实现了Finder接口合适的实现类并赋值给MovieLister类的一个字段。现在的依赖情况如下图所示:
图2依赖注入器的依赖关系
依赖注入的形式主要又三种:构造器注入,属性注入,接口注入。如果你关注最近关于依赖注入的讨论资料你就会发现这就是其中提到的类型1IoC 类型2IoC 类型3IoC。数字往往比较难记,所以我这里使用了名称命名。
使用PicoContainer进行构造器注入
首先我通过使用一个轻量级的容器PicoContainer来实现构造器注入。之所以要从这里开始是因为我在ThoughtWorks公司的几个同事在PicoContainer的开发社区很活跃,是的,可是说是一种偏爱。
PicoContainer使用构造器来决定如何把一个finder的实现类注入到lister类中。要达到这个目的MoiveLister类必须要生命一个构造器而且要包含足够的注入信息。
//class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
finder自身也是由PicoContainer管理的所以文本文件的名字也可以通过容器注入来实现。
//class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
下面要做的就是告诉PicoContainer哪些接口和哪些类实现关联,哪些字符串注入到finder组件。
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;
}
这段配置代码通常位于另外一个类。我们这个例子,每一个使用我的lister类只要在自己的配置类编写适当的配置代码就可以了。当然通常的做法是把这些配置信息放在一个单独的配置文件中。你可以写一个类来读取配置文件来设置容器,尽管PicoContainer不包含这个功能在另一个有紧密关系的项目NanoContainer 中提供了一些包装,允许你使用XML配置文件。NanoContainer 解析并对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一样它也支持构造器注入和属性注入,但是它的开发者推荐是使用属性注入--用到这个例子里正合适。
为了让我的Moive Lister能接收注入,我要给它添加一个赋值方法:
//class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
类似的我们为filename也添加这样一个方法
//class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
第三步就是设置配置文件。Spring支持多种配置方式,你可以通过XML文件配置或者通过代码,XML配置文件是比较理想的方式。
<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>
测试代码大致是这样的:
2 ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
3 MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
4 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
5 assertEquals("Once Upon a Time in the West", movies[0].getTitle());
6 }
7
接口注入
第三种注入技术是在接口中定义注入需要的信息并通过接口完成注入。 Avalon 就是使用了这种技术的框架的典型例子。稍后我会讨论更多相关内容,现在这个例子先用简单的代码来实现。
使用这个技术我先要定义一个接口,并用这个完接口成注入。这个接口的作用就是把finder实例注入到实现了该接口的对象中。
void injectFinder(MovieFinder finder);
}
这个接口由提供MovieFinder的类提供。任何要使用MovieFinder的实体类都必须实现这个接口,比如lister。
//class MovieLister implements InjectFinder
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
我使用类似的方法将文件名注入到finder实现中:
public interface InjectFinderFilename {
void injectFilename (String filename);
}
//class ColonMovieFinder implements MovieFinder, //InjectFinderFilename
public void injectFilename(String filename) {
this.filename = filename;
}
接下来我还需要一些配置代码将这些组件的实现包装起来,简单起见我就在代码里完成了:
//class Tester
private Container container;
private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
配置分成了两个阶段,通过定位关键字来注册组件,这和其它的例子一样:
//class Tester
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
下一步就是注册要依赖组件的注入器,每一个注入接口都需要一些代码来注入到依赖的对象。这里我使用容器来完成注入器的注册。每一个注入器对象都实现了注入接口。
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);
}
当依赖设计成是一个为容器而写的类那么它对组件自身实现接口注入是有意义的,就像这里我对moive finder做的修改一样。对于普通类,比如string,我使用内部类来完成配置代码。
//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());
}
容器使用声明了注入器的接口来指明注入器和实体类之间的依赖关系。(具体用什么容器实现不重要,我也不会做展示这只会让你笑我)
使用服务定位器
依赖注入的关键优势是它消除了lister类对具体finder类实现的依赖。这样就可以让我的朋友方便的将一个特定的实现插入到他们的应用环境中了。注入绝不是唯一出路,另外一个方案就是使用Service Locator:服务定位器模式。
服务定位器的基本思想是一个对象知道如何获得应用程序所需要的所有服务。在我们的例子里服务定位器就应该有一个方法返回所需的finder实例。这只是减少了一点负担,从下图的依赖关系可以看出我们还是需要在lister中获得定位器。
图3服务定位器的依赖关系
这里我把服务定位器做成一个单件注册表。lister可以在实例化时通过服务定位器获取一个finder的实例。
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;
}
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());
}
我常听到这样的抱怨服务定位器不是什么好东西因为你无法替换服务定位器返回的实例也就无法进行测试。当然要是你设计很糟糕遇到了这些麻烦在所难免。在这个例子服务定位器就是一个简单的数据容器,可以简单修改就可以创建适用于测试的服务。
对于更复杂的情况我可以从服务定位器派生子类并将子类传递给注册表类变量。另外我可以让服务定位器暴露出来一个静态方法而不是直接访问该类实例的变量。我还可以使用特定的线程存储提供特定线程的服务定位器。所有这些变化都不会修改使用服务定位器的Client.
一种对服务定位器的改进思路是不设计成单件形式。单件的确是实现注册表的简单方法,但这这只是实现决定很容易改变它。
为定位器使用分离接口
上面的简单方案有一个问题,lister类依赖于整个服务定位器哪怕它仅仅使用一个服务。我们可以通过使用分离的接口来改变这种对服务定位器的依赖,lister使用的是它想要使用的那部分接口。
相应的lister类的提供者也应该提供这样一个定位器接口,使用者可以通过这个接口获得finder。
public interface MovieFinderLocator {
public MovieFinder movieFinder();
The locator then needs to implement this interface to provide access to a finder.
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;
注意这里由于使用了接口我们就不能再使用静态方法直接访问服务,我们必须在类中获得定位器实例进而使用各种需要的服务。
动态服务定位器
上面的例子是静态的,服务定位器拥有你想要的各种服务。这还不是唯一的路子,你可以使用动态服务定位器,它允许你在其中注册需要的任何服务并在运行时按需获取服务。
下面的例子中,服务定位器使用map来保存服务信息,而不是放在字段中,并提供了一个通用的方法来获取服务。
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:Locator Injection双管齐下
依赖注入和服务定位器不是水火不容的概念。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对象注入到Lister对象中。而ServiceMananger则是一个服务定位器的例子。这个例子中,MyMoiveLister并不是要把ServiceManager保存在字段里面,而是借助它获得finder的实例,并将finder保存起来。
选择
到目前为止我已经集中阐述了我对这两个模式及其变化形式的看法。下面就要细数它们的优缺点了,这样有助于判断什么时候用哪一个。
服务定位器VS依赖注入
首先是服务定位器和依赖注入的选择。第一点要注意的虽然上面两个过于简单的例子没有表现出来实际上这两种模式都提供了基本的解耦功能。两种情况下应用程序代码都不依赖于具体服务接口的实现。二者的区别在于怎样把具体的实现提供给应用程序类。使用服务定位器是通过应用程序发送消息明确要哪一个服务的实现。使用依赖注入不明确发请求,服务的实现会自然的出现在应用程序的类中,这就是控制反转。
控制反转是所有框架的共同特征,但是你要付出一点代价。它会难于理解并在调试时带来一些问题。所以我整体来讲我尽量避免使用它。这并不是说它不好,只是我更倾向于一个直观的方案。
二者关键的区别在于:使用服务定位器每一个服务的使用者都会依赖于服务定位器。定位器隐藏了使用者对其它服务具体实现的依赖,而必须依赖服务定位器本身。所以选择注入还是定位器就在于你是否在意对定位器的依赖!
使用依赖注入你可以很清楚的看到组件之间的依赖关系。你只需要观察使用了什么样的依赖注入机制,比如构造器注入,就可以看清其中的依赖关系。
而在服务定位器中你必须在源代码中搜索对服务定位器的调用,具备引用分析功能的IDE可以简化这个操作,但还不如直接观察构造器和属性简单。
关于这二者的选择很大程度上取决于我么服务使用者的性质。如果你的应用程序中有不同的类使用同一个服务,那么应用程序使用服务构造器就不是什么大的问题。上面影片列表的例子使用服务定位器就很好我的朋友只需要对定位器做一点配置(配置代码或者配置文件均可)就可以获得适当的服务实现。而在这种应用场景中,注入器的反转没有什么引人注目的地方。
要是把lister看成一个组件提供给别的应用程序使用情况就不同了。这种情况下我不知道使用者会使用什么样的服务定位器API,每一个使用者都有可能都使用自己的服务定位器,而这些服务定位器彼此之间并不兼容。我可以使用分离接口的方法来解决这个问题,每一个使用者都可以开发自己的服务定位器的适配器。但即使是这样我还是希望第一个服务定位器中寻找特定的接口。而且一旦适配器出现定位器的简单直接就开始削弱了。
使用注入器不会产生组件和注入器之间的依赖关系,可是注入器一旦配置确定了组件也无法获得更多的服务。
人们倾向选择依赖注入的原因是进行测试更容易了。关键之处是为了进行测试你可以使用真实服务或者使用Stubs桩或者mocks方法等创建的测试用服务。但是实际上这里使用依赖注入和服务定位器是没有区别的:两者都很好的支持“桩”。我猜想之所以有这么多人又依赖注入更容易进行测试的印象,是因为他们并没有努力做到定位器可替换。这就是持续测试的意义所在,如果你不能很容易的使用桩进行测试,这就意味着你的设计出现了严重问题。
当然,如果组件环境又很强的侵入性那么测试的问题会更加严重,例如Java EJB框架。我的观点是这些框架应该尽量减小框架本身对应用程序代码的影响,特别是不要做那些可能会让编辑-执行过程变慢的操作。用插件来取代重量级的组件会对测试过程有很大帮助。这也是TDD--测试驱动开发实践的关键。
所以,主要的问题还是在于作者是否愿意组件脱离自己控制被应用到其它程序中。如果作者是这样想的,那么它就不能对服务定位器做任何假设--哪怕是最小的假设也会带来麻烦。
构造器注入vs属性注入
服务整合的时候,你要遵循一定规则来将所有的东西整合在一起。依赖注入的有点在于:只要非常简单的约定--至少对构造器注入和属性注入来说是这样的。你不必在你的组件中做什么特殊的处理就是直接做一下配置就可以了。
接口注入的侵入性要强的多,因为你要按照需要写很多接口。用容器提供一个小型的接口集合,这个主意不错,Avalon就是这么做的。
整合和依赖方面还有很多工作要做,这就是为什么很多轻量级容器依然致力于研究属性注入和构造器注入的原因。
属性注入和构造器注入反映了一个有趣的问题,所有面向对象系统都会遇到:应该在那里填充对象字段:构造器还是属性?
一直以来我的实践都是尽量把对象的创建放在构造阶段,也就是构造器中填充对象字段。这个建议可以追溯到Kent Beck's Smalltalk Best Practice Patterns: Constructor Method and Constructor Parameter Method.带参构造器可以明确创建意图,如果不知一种构造方式,就要提供多个构造函数。
构造器的另外一个好处就是允许你隐藏一些不变字段只要没有setter就可以了。我认为这个很重要如果一个字段被认为是不应该被改变的,那么属性赋值锁定就能很好的表明这一点。如果你使用属性初始化那就痛苦了,实际上我倾向于避免使用设置方法,而是使用哪个类似于initFoo之类的方法,来强调这个方法应该在对象创建之初进行调用。
不过什么事情都有个例外,如果构造函数的参数过多就显得有点混乱了,特别是对于哪些不支持关键字参数的语言更是如此。一个冗长的参数列表说明这个类过于繁忙了,应该进行拆分,确实需要很多参数的情况除外。
如果又多种方式构造一个有效的对象,很难通过构造函数的个数和类型来描述构造意图。这就是Factory Method模式的用武之地了,工厂方法可以借助多个私有构造函数和Setter方法组合来完成自己的任务。经典Factory Method模式的问题就在于它往往通过静态方法的形式实现,你无法在接口声明它们。你可以创建一个工厂类,但是那又变成另一个服务实体类了。工厂服务是一种不错的技巧,但是你需要某种方式实例化工厂对象,问题还在。
如果要传入像字符串之类的简单类型,构造器注入也会又问题:使用属性注入你可以属性方法的名字中说明参数的用途;而构造器注入你只能靠参数的位置决定每一个参数的作用,而这实践起来难很多。
如果你的对象又有很多构造器并且对象间存在继承关系,事情就会变糟糕了。为了能够正确初始化你必须将子类的构造器调用转到父类构造器然后处理自己的参数。这可能导致构造器爆炸式膨胀。
尽管诸多缺陷我还是首推构造器注入,不过我们已经做好准备一旦遇到上述的问题就转向属性注入。
在以依赖注入作为框架核心的几个开发团队之间,属性注入还是构造器注入这个话题上争论不休。不过多数人都已经意识到即使有所偏重还是要兼顾两种注入方式。
代码配置还是配置文件
另外一个相对独立却也纠结的话题是:使用配置文件还是使用代码来完成配置服务的组装。对于大多数要在多处部署的应用程序来说,配置文件的方式更合适些。配置文件多为XML,XML也确实胜任。但也有一些情况使用代码做配置更简单。一种情况就是你要做一个简单的应用没有部署的变化要求,这时使用代码配置就比单独的XMl简洁明了。
相对的情况是应用程序装配复杂的情况,这涉及大量的条件步骤。一旦一种编程语言的XML配置逻辑变得复杂,你就应该使用合适的语言来描述配置信息使得程序逻辑变得清晰。你可以写一个Builder来完成组装,如果使用Builder的场景不止一处你可以提供多个Builder类并用配置文件分离。
我经常发现人们急于去定义配置文件。编程语言通常会提供简单的方式进行配置管理并可以插入到大型系统中。如果编译过程是个麻烦,脚本语言可以提供很好的帮助。
通常的看法是配置文件不应该使用编程语言编写,因为他们能够被非技术人员编辑。可是这情况会有多少呢?我们真的希望一个非技术人员来改变一个复杂服务器端应用程序的事务隔离等级么?只有在简单的场景中,非编程语言的配置文件才效果最好。如果配置文件变得复杂了就应该考虑换一种合适的编程语言了。
Java世界里关于配置文件有这样一种不和谐的情况:每一个组件都有自己的配置文件,而且格式各不相同。你要使用一堆组件就要维护一堆配置文件,很快同步配置文件问题就会让你你不胜其烦。
我的建议是:所有的配置文件都可以通过一个统一的编程接口提供,这样把配置文件分开就是可选选项了。借助这个接口你可以轻松的管理配置文件。如果你写了一个组件,则可以由使用者决定是否使用你的编程接口,配置文件的格式,或者定义他们自己的配置文件格式并和你的编程接口紧密结合。
将配置从应用中分离
这个话题的重中之重是保证服务的配置和应用分离。实际上这一个基本的设计原则:分离接口和具体实现。在面向对象的程序中我们在一个地方使用条件逻辑来决定初始化那个类之后的条件逻辑通过多态来实现而不是重复前面的条件逻辑。
如果对于一段代码接口和具体实现分离是有用的,当你使用外部元素例如组件和服务时,它就至关重要了。第一个问题是你是否希望将具体实现的选择推迟到部署阶段。如果是的话,你就需要使用实现插入方式了。使用插入方式,插件的装配和应用程序的其它部分隔离开了,这样你就可以轻松的针对不同的部署来调整配置。怎么做到这一点倒是次要的,使用服务定位器或者注入技术都可以。
更多话题
本文我关注核心话题是使用依赖注入和服务定位器进行服务配置。还有一些和这些容器相关的话题需要注意,我已经没有时间继续深入挖掘了下去了。特别注意的是生命周期的问题:某些组件具有特定的生命周期事件,例如停止 开始。还有一个话题是人们对如何在这些容器中应用面向方面的思想兴趣浓厚。尽管目前没有准备相关的资料,我的确希望在这些话题上多写一点,无论是扩展本文或是另起炉灶。
你可以在轻量级容器的网站上找到更多的资料:
在 picocontainer 和 spring 你可以找到大量的讨论内容可以做进一步的研究。
结论
当下流行的轻量级容器底层都使用了相同的模式来整合服务--依赖注入模式。还有一个有效替代的模式是服务定位器模式。在开发应用程序的过程中二者旗鼓相当,但我认为服务定位器模式略占上风,因为它的行为更直观。要是你开发的组件要被多个程序使用,那么依赖注入是个更好的选择。
如果你使用依赖注入模式还有一些实现方式可选。我建议你使用构造器注入,遇到一些特定的问题再转向属性注入。如果你需要选择容器找一个支持同时两中注入方式的。
服务定位器模式和依赖注入模式之间的选择不是那么重要,重要的是这样一个原则:应该将服务配置从应用程序内部分离出来。