好了,我看过了写的最好的了,讲的很详细
Spring IoC有什么好处呢? - 知乎
但不以为着此贴终结啊。我觉得我这篇讲的很详细,虽然不如知乎上的通俗易懂,但是更显专业orz。
2019年11月7日更新
相信大家和我一样初学springIOC和DI时都有些迷糊吧,反正我是看了会资料了解了大概就继续看其他的内容了,想着多学点知识,好读书,不求甚解。以为多学点就能处理问题了。后来上班实习发现这些知识挺重要的,想着有时间看资料总结一下,为了自己更好的理解,也帮助大家,因为一是有些忙,二是想写好点。就是可能多次更新,写一写。+
分享一下牛人对于IOC的理解。地址:http://jinnianshilongnian.iteye.com/blog/1413846
以下是原文:
Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好Ioc呢?理解好Ioc的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:
●谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
●为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
用图例说明一下,传统程序设计如图,都是主动去创建相关对象然后再组合起来:
当有了IoC/DI的容器后,在客户端类中不再主动去创建这些对象了,如图所示
1.1.2 IoC能做什么
IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
2.1.3 IoC和DI
DI—Dependency Injection,即“依赖注入”:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
●谁依赖于谁:当然是应用程序依赖于IoC容器;
●为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
●谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
●注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
注:如果想要更加深入的了解IoC和DI,请参考大师级人物Martin Fowler的一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》,原文地址:http://www.martinfowler.com/articles/injection.html。
然后我就去读了这篇文章,把他大致总结一下:
Components and Services(组件和服务)
现有许多资料谈组件和服务都很繁琐和晦涩,我们应该用尽量清楚明白的语言来阐述。
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.)
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.------>我在本文中主要使用service,但是同样的逻辑也可以应用于本地组件。实际上,您经常需要某种本地组件框架来方便地访问远程服务。但是编写“组件或服务”的读写是很累人的,而且目前使用“服务“”要流行得多。
A Naive Example
接下来作者举了一个例子。
在本例中,我正在编写一个组件,它提供由特定导演执导的电影列表。这个非常有用的函数是由一个方法实现的。
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对象,特别是如何将lister对象与特定的finder对象连接起来。这很有趣的原因是,我想让我的美妙的影片通过方法来导演,完全独立于所有影片的存储方式。这个方法所做的就是引用finder,而这个finder所做的就是知道如何响应findAll方法。我可以通过为finder定义一个接口来实现这一点。
public interface MovieFinder {
List findAll();
}
现在,所有这些都很好地解耦了,但在某个时候,我必须想出一个具体的类来处理电影。在本例中,我将其代码放入lister类的构造函数中。
class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
The name of the implementation class comes from the fact that I'm getting my list from a colon delimited text file.用冒号分隔文件,现在,如果我只是为自己使用这个类,这一切都很好。但是,当我的朋友们被这种美妙的功能所吸引,想要复制我的程序时,会发生什么呢?如果他们还将电影列表存储在一个冒号分隔的文本文件中,该文本文件名为“movies1”.txt“那么一切都是美好的。如果它们的电影文件有不同的名称,那么很容易将文件的名称放入属性文件中。但是,如果他们有完全不同的存储电影列表的形式:SQL数据库、XML文件、web服务,或者只是另一种文本文件格式呢?在本例中,我们需要一个不同的类来获取数据。现在因为我已经定义了一个MovieFinder接口,这不会改变我的moviesDirectedBy方法。但是,我仍然需要一些方法来获得right finder实现的实例。
上图显示了这种情况的依赖关系。MovieLister类依赖于MovieFinder接口和实现。如果它只依赖于接口,我们会更喜欢它,但是我们如何创建一个实例来使用它。
在我的书《 P of EAA》(Patterns of Enterprise Application Architecture)中,我们将这种情况描述为插件。finder的实现类在编译时没有链接到程序中,因为我不知道我的朋友将使用什么。相反,我们希望我的lister能够与任何实现一起工作,并在稍后的某个时候插入该实现,而不受我的控制。问题是,我如何创建这个链接,使lister类不知道实现类,但仍然可以与实例对话来完成它的工作。
将其扩展到一个真实的系统中,我们可能有几十个这样的服务和组件。在每种情况下,我们都可以通过接口与这些组件进行通信来抽象我们对它们的使用(如果组件的设计没有考虑到接口,则使用适配器)。但是如果我们希望以不同的方式部署这个系统,我们需要使用插件来处理与这些服务的交互,这样我们就可以在不同的部署中使用不同的实现。
因此,核心问题是如何将这些插件组装到应用程序中?这是这种新型轻量级容器所面临的主要问题之一,而且它们通常都使用反转控制来实现这一点。
Inversion of Control
当这些容器谈到它们是如何如此有用,因为它们实现了“控制反转”时,我感到非常困惑。反转控制是框架的一个常见特性,所以说这些轻量级容器是特殊的,因为它们使用反转控制,就像说我的车是特殊的,因为它有轮子一样。问题是:“他们改变了控制的哪些方面?”当我第一次遇到控制反转时,它是在一个用户界面的主控制中。早期的用户界面由应用程序控制。你会有一系列的命令,比如“输入名字”,“输入地址”;您的程序将驱动提示并对每个提示进行响应。对于图形化(甚至基于屏幕的)UI, UI框架将包含这个主循环,而您的程序将为屏幕上的各个字段提供事件处理程序。程序的主控件被倒置,从您移到框架上。
对于这种新类型的容器,反转是关于它们如何查找插件实现的。在我的简单示例中,lister通过直接实例化查找finder实现。这将阻止finder成为插件。这些容器使用的方法是确保插件的任何用户都遵循某种约定,允许一个单独的组装模块将实现注入lister。因此,我认为我们需要为这种模式取一个更具体的名称。控制反转这个术语太笼统了,因此人们会感到困惑。因此,在与各种IoC支持者进行了大量讨论之后,我们确定了名称依赖注入。我将首先讨论各种形式的依赖注入,但现在我要指出,这不是将依赖项从应用程序类移到插件实现的唯一方法。您可以使用的另一种模式是Service Locator,我将在解释完依赖项注入之后讨论它。(个人观点:此点体现了低耦合,使得其扩展性更强)
Forms of Dependency Injection
依赖项注入的基本思想是拥有一个单独的对象(一个汇编程序),该对象使用finder接口的适当实现填充lister类中的字段,生成如下图所示的依赖关系图
依赖项注入有三种主要类型。我为它们使用的名称是构造函数注入、Setter注入和接口注入。如果您在当前关于控制反转的讨论中读到过这方面的内容,那么您将听到这些被称为类型1 IoC(接口注入)、类型2 IoC (setter注入)和类型3 IoC(构造函数注入)。我发现数字名称很难记住,这就是为什么我使用这里的名称。
Constructor Injection with PicoContainer
首先,我将展示如何使用一个名为PicoContainer的轻量级容器来完成这个注入。我之所以从这里开始,主要是因为我在ThoughtWorks的几位同事对PicoContainer的开发非常积极(是的,这是一种企业裙带关系)。PicoContainer使用构造函数来决定如何将查找器实现注入lister类。要实现这一点,movie lister类需要声明一个构造函数,其中包含它需要注入的所有内容。
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
finder本身也将由pico容器管理,因此将文本文件的文件名由容器注入其中。
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
然后需要告诉pico容器要与每个接口关联的实现类,以及要注入到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的朋友都可以在他们自己的setup类中编写适当的配置代码。当然,将这类配置信息保存在单独的配置文件中是很常见的。您可以编写一个类来读取配置文件并适当地设置容器。尽管PicoContainer本身不包含此功能,但是有一个密切相关的项目NanoContainer,它提供了适当的包装器,允许您拥有XML配置文件。这样的nano容器将解析XML,然后配置底层的pico容器。该项目的理念是将配置文件格式与底层机制分离。要使用容器,可以编写类似这样的代码。
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也支持setter注入,尽管它的开发人员更喜欢构造函数注入。
Setter Injection with Spring
Spring框架是一个用于企业Java开发的广泛框架。它包括事务抽象层、持久性框架、web应用程序开发和JDBC。与PicoContainer一样,它支持构造函数注入和setter注入,但是它的开发人员倾向于选择setter注入——这使得它成为本例中的一个合适选择。为了让我的movie lister接受注入,我为该服务定义了一个设置方法
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
Similarly I define a setter for the filename.定义了一个setter
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
第三步是设置文件的配置。Spring支持通过XML文件和代码进行配置,但XML是要按照预期的方式去做。如下
测试类是这样的
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
第三种注入技术是为注入定义和使用接口。Avalon就是这样在某些地方使用这种技术的框架的一个例子。稍后我会详细讨论这一点,但在本例中,我将使用一些简单的示例代码。使用这种技术,我首先定义一个接口,我将使用它来执行注入。下面是将影片查找器(finder)注入对象的接口。
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
这个接口将由提供MovieFinder接口的人定义。它需要由希望使用finder(如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);
}
当依赖项是为这个容器编写的类时,组件本身实现注入器接口是有意义的,就像我在这里使用movie finder所做的一样。对于泛型类,例如字符串,我在配置代码中使用一个内部类。
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");
}
}
然后测试使用容器。
class Tester…
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());
}
容器使用声明的注入接口来确定依赖项,并使用注入器注入正确的依赖项。(我在这里所做的特定容器实现对该技术并不重要,我不会展示它,因为您只会一笑置之。)
Using a Service Locator
依赖注入器的主要好处是,它消除了MovieLister类对具体MovieFinder实现的依赖。这允许我将listers提供给朋友,并让他们插入适合自己环境的实现。注入不是打破这种依赖关系的唯一方法,另一种方法是使用服务定位器。服务定位器背后的基本思想是拥有一个知道如何获取应用程序可能需要的所有服务的对象。因此,此应用程序的服务定位器将具有一个方法,该方法在需要时返回影片查找器。当然,这只是稍微转移了一些负担,我们仍然需要将定位器放入lister中,这导致了下图中的依赖关系
在本例中,我将使用ServiceLocator作为单例注册表。lister然后可以在实例化finder时使用它来获取finder。
class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
与注入方法一样,我们必须配置服务定位器。这里我是用代码来做的,但是使用从配置文件中读取适当数据的机制并不难。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。——————————————————————————————————————————————————————————————
由于篇幅实在太多,后面设计服务定位器(Service Locator)等知识,可以自行去查看。
到目前为止,我一直专注于解释我如何看待这些模式及其变化。现在,我可以开始讨论它们的优缺点,以帮助确定使用哪些方法以及何时使用。
Service Locator vs Dependency Injection
基本的选择是服务定位器和依赖项注入之间的选择。第一点是,这两种实现都提供了基本的解耦,而这正是朴素示例中所缺少的——在这两种情况下,应用程序代码都独立于服务接口的具体实现。这两种模式之间的重要区别在于如何将实现提供给应用程序类。使用服务定位器,应用程序类通过发送给定位器的消息显式地请求它。注入没有显式的请求,服务出现在应用程序类中——因此控制反转。控制反转是框架的一个常见特性,但它是有代价的。当您尝试调试时,它往往很难理解并导致问题。所以总的来说,我宁愿避免它,除非我需要它。这并不是说这是件坏事,只是我认为它需要证明自己,而不是更直接的选择。
关键的区别在于,使用服务定位器时,服务的每个用户都对定位器具有依赖关系。定位器可以隐藏对其他实现的依赖关系,但您确实需要查看定位器。所以定位器和注入器之间的决定取决于依赖关系是否存在问题。使用依赖项注入可以帮助更容易地查看组件依赖项是什么。使用依赖注入器,您可以只查看注入机制,比如构造函数,然后查看依赖项。使用服务定位器,您必须搜索对定位器的调用的源代码。带有find references功能的现代ide使这一点变得更容易,但它仍然不像查看构造函数或设置方法那么容易。
这在很大程度上取决于服务用户的性质。如果您正在使用使用服务的各种类构建应用程序,那么应用程序类与定位器之间的依赖关系不是什么大问题。在我为朋友提供电影列表的例子中,使用服务定位器工作得非常好。他们所需要做的就是配置定位器,以便通过一些配置代码或配置文件将正确的服务实现挂钩起来。在这种情况下,我不认为注入器的反转提供了任何令人信服的东西。如果lister是我提供给其他人正在编写的应用程序的组件,就会有不同。在这种情况下,我不太了解我的客户将要使用的服务定位器的api。每个客户可能都有自己不兼容的服务定位器。我可以通过使用隔离接口来解决一些问题。每个客户都可以编写一个适配器,使我的接口与他们的定位器匹配,但是在任何情况下,我仍然需要看到第一个定位器来查找我的特定接口。一旦适配器出现,直接连接到定位器的简单性就开始下滑。
由于使用注入器时,组件与注入器之间不存在依赖关系,因此一旦配置好注入器,组件就无法从注入器获得进一步的服务。人们更喜欢依赖注入的一个常见原因是,它使测试更容易。这里的重点是,要进行测试,您需要轻松地用存根或模拟替换真正的服务实现。然而,依赖项注入和服务定位器之间实际上没有区别:两者都非常适合存根。我怀疑这种观察来自于人们没有努力确保他们的服务定位器可以很容易地替换的项目。这就是持续测试的作用所在,如果您不能轻松地为测试存根服务,那么这就意味着您的设计存在严重的问题。
当然,测试问题由于组件环境(如Java的EJB框架)非常具有侵入性而加剧。我的观点是,这类框架应该尽量减少它们对应用程序代码的影响,特别是不应该做一些降低编辑执行周期的事情。使用插件来替代重量级组件对这个过程有很大帮助,这对于测试驱动开发之类的实践非常重要。所以,主要的问题是,对于那些编写代码的人来说,他们希望在作者控制之外的应用程序中使用这些代码。在这些情况下,即使是关于服务定位器的最小假设也是一个问题。
Constructor versus Setter Injection
对于服务组合,您总是必须有某种约定才能将内容连接在一起。注入的优势主要在于它需要非常简单的约定——至少对于构造函数和setter注入是这样。您不需要在组件中执行任何奇怪的操作,而且对于注入器来说,配置一切都非常简单。接口注入更具侵入性,因为您必须编写大量接口才能把所有事情都处理好。对于容器所需的一小组接口,比如Avalon的方法,这并不太糟糕。但是组装组件和依赖项需要大量的工作,这就是为什么当前的轻量级容器使用setter和构造函数注入。
setter和构造函数注入之间的选择很有趣,因为它反映了面向对象编程的一个更普遍的问题——应该在构造函数中填充字段还是用setter填充字段。我长期运行的默认对象是尽可能多地在构建时创建有效的对象。这个建议可追溯到Kent Beck的《Smalltalk最佳实践模式:构造函数方法和构造函数参数方法》。带有参数的构造函数可以清楚地说明在一个明显的位置创建一个有效对象意味着什么。如果有不止一种方法,创建多个显示不同组合的构造函数。
构造函数初始化的另一个优点是,通过不提供setter,可以清楚地隐藏任何不可变的字段。我认为这很重要——如果某些东西不应该改变,那么缺少setter可以很好地传达这一点。如果您使用setter进行初始化,那么这会成为一种痛苦。(事实上,在这些情况下,我更愿意避免通常的设置约定,我更愿意使用initFoo这样的方法,来强调这只是在出生时才应该做的事情。)但任何情况都有例外。如果有很多构造函数参数,事情就会变得很混乱,特别是在没有关键字参数的语言中。确实,长构造函数通常是应该分割的过于繁忙的对象的标志,但是在某些情况下,这正是您所需要的。
如果有多种构造有效对象的方法,则很难通过构造函数来展示这一点,因为构造函数只能根据参数的数量和类型而变化。这时工厂方法开始发挥作用,它们可以使用私有构造函数和setter的组合来实现它们的工作。用于组件组装的经典工厂方法的问题是,它们通常被视为静态方法,而不能在接口上使用这些方法。您可以创建一个工厂类,但是这样就变成了另一个服务实例。工厂服务通常是一种很好的策略,但是您仍然必须使用其中一种技术实例化工厂。如果有简单的参数,比如字符串,构造函数也会受到影响。通过setter注入,您可以为每个setter指定一个名称,以指示字符串应该做什么。对于构造函数,您只是依赖于位置,而位置更难跟踪
如果您有多个构造函数和继承,那么事情就会变得特别棘手。为了初始化所有内容,您必须提供构造函数来转发给每个超类构造函数,同时还要添加自己的参数。这可能导致构造函数的更大爆炸。尽管有这些缺点,但我更倾向于从构造函数注入开始,但是一旦上面列出的问题开始成为问题,就要准备切换到setter注入。这个问题在提供依赖注入器作为框架一部分的各个团队之间引发了很多争论。然而,构建这些框架的大多数人似乎已经意识到支持这两种机制很重要,即使偏爱其中一种。
Code or configuration files
另一个经常合并的问题是,是否使用配置文件或API上的代码来连接服务。对于大多数可能部署在许多地方的应用程序,单独的配置文件通常最有意义。几乎所有时候这都是一个XML文件,这是有意义的。然而,在某些情况下,使用程序代码进行组装更容易。一种情况是,您有一个简单的应用程序,它没有太多的部署变化。在这种情况下,一些代码比单独的XML文件更清晰。相反的情况是,程序集非常复杂,涉及条件步骤。一旦您开始接近编程语言,那么XML就开始分解,最好使用一种真正的语言,它具有编写清晰程序所需的所有语法。然后编写一个执行组装的构建器类。如果您有不同的构建器场景,您可以提供几个构建器类,并使用一个简单的配置文件在它们之间进行选择。我经常认为人们过于急于定义配置文件。通常,编程语言会创建一个简单而强大的配置机制。现代语言可以很容易地编译小型汇编程序,这些程序可用于为大型系统组装插件。如果编译是一个痛苦的过程,那么有些脚本语言也可以很好地工作。
经常有人说,配置文件不应该使用编程语言,因为它们需要由非程序员编辑。但这种情况多久发生一次呢?人们真的希望非程序员更改复杂服务器端应用程序的事务隔离级别吗?非语言配置文件只有在简单的情况下才能很好地工作。如果它们变得复杂,那么是时候考虑使用合适的编程语言了。目前我们在Java世界中看到的一件事是配置文件的不和谐,其中每个组件都有自己的配置文件,这些文件与其他组件的配置文件不同。如果您使用这些组件中的一打,那么您很容易得到一打配置文件来保持同步。我在这里的建议是,始终提供一种使用编程接口轻松完成所有配置的方法,然后将单独的配置文件视为可选特性。您可以轻松地构建配置文件处理来使用编程接口。如果正在编写组件,则将其留给用户决定是使用编程接口、配置文件格式,还是编写自己的自定义配置文件格式并将其绑定到编程接口。
Separating Configuration from Use
所有这些中的重要问题是确保服务的配置与它们的使用分离。实际上,这是一个基本的设计原则,它将接口与实现分离开来。在面向对象程序中,当条件逻辑决定要实例化哪个类,然后通过多态性(而不是通过重复的条件代码)对该条件进行未来的评估时,我们就会看到这种情况。如果这种分离在单个代码库中有用,那么在使用组件和服务等外部元素时尤其重要。第一个问题是,是否希望将实现类的选择推迟到特定的部署。如果是这样,你需要使用一些插件的实现。一旦您使用了插件,那么插件的组装就必须与应用程序的其他部分分开进行,这样您就可以轻松地为不同的部署替换不同的配置。如何做到这一点是次要的。这种配置机制既可以配置服务定位器,也可以使用注入直接配置对象。
Some further issues
在本文中,我主要讨论了使用依赖项注入和服务定位器进行服务配置的基本问题。还有其他一些话题也值得关注,但我还没有时间深入探讨。特别是生命周期行为的问题。有些组件具有不同的生命周期事件:例如,停止和启动。另一个问题是,人们对在这些容器中使用面向方面的思想越来越感兴趣。虽然我目前还没有在本文中考虑过这些材料,但是我确实希望通过扩展本文或编写另一篇文章来对此进行更多的讨论。通过查看致力于轻量级容器的web站点,您可以了解更多关于这些想法的信息。从picocontainer和spring web站点进行浏览将使您对这些问题进行更多的讨论,并开始讨论一些更深入的问题。
当前大量的轻量级容器都有一个共同的底层模式来进行服务组装——依赖注入器模式。依赖项注入是服务定位器的一个有用的替代方法。当构建应用程序类时,这两个类大致相当,但是我认为Service Locator有一点优势,因为它的行为更直接。但是,如果要构建用于多个应用程序的类,则依赖项注入是更好的选择。如果使用依赖项注入,则有许多样式可供选择。我建议您遵循构造函数注入,除非您遇到这种方法的特定问题之一,在这种情况下切换到setter注入。如果选择构建或获取容器,请寻找同时支持构造函数和setter注入的容器。服务定位器和依赖项注入之间的选择没有将服务配置与应用程序中的服务使用分离开来的原则重要。