摘自:https://martinfowler.com/articles/injection.html
控制容器的反转和依赖注入模式
在Java社区中,出现了一些轻量级容器,这些容器有助于将来自不同项目的组件组装成一个有凝聚力的应用程序。这些容器的基础是他们如何执行布线的常见模式,这是他们在非常通用的名称“控制反转”下引用的概念。在本文中,我将根据更具体的名称“依赖注入”深入研究这种模式的工作原理,并将其与服务定位器替代方案进行对比。它们之间的选择不如将配置与使用分离的原则重要。
2004年1月23日
关于企业Java世界的一个有趣的事情是构建主流J2EE技术??替代品的大量活动,其中大部分都是在开源中发生的。其中很多是对主流J2EE世界中重量级复杂性的反应,但其中很大一部分也在探索替代方案并提出创意。要处理的一个常见问题是如何将不同的元素连接在一起:当这些Web控制器体系结构由不同的团队构建时,如何将这个Web控制器体系结构与数据库接口支持结合在一起,彼此之间几乎没有相互了 许多框架已经解决了这个问题,并且有几个框架正在扩展以提供从不同层组装组件的一般能力。这些通常被称为轻量级容器,例子包括PicoContainer和Spring。
这些容器的基础是许多有趣的设计原则,这些原则超出了这些特定的容器和Java平台。在这里,我想开始探索其中的一些原则。我使用的示例是Java,但与我的大多数写作一样,这些原则同样适用于其他OO环境,尤其是.NET。
组件和服务
布线元素的主题让我几乎立即陷入围绕术语服务和组件的棘手术语问题。你可以轻松地找到关于这些事情定义的冗长而矛盾的文章。出于我的目的,这里是我目前使用这些重载的术语。
我使用组件来表示一组软件,该软件旨在由不受组件编写者控制的应用程序使用而无需更改。通过‘无变化‘,我的意思是使用应用程序不会更改组件的源代码,尽管它们可能通过以组件编写者允许的方式扩展组件的行为来改变组件的行为。
服务类似于外部应用程序使用的组件。主要的区别是我希望在本地使用一个组件(想想jar文件,汇编,dll或源代码导入)。服务将通过某些远程接口远程使用,无论是同步还是异步(例如,Web服务,消息传递系统,RPC或套接字)。
我主要使用本文中的服务,但许多相同的逻辑也可以应用于本地组件。实际上,您经常需要某种本地组件框架来轻松访问远程服务。但是,编写“组件或服务”令人厌倦阅读和写作,而服务目前更加时尚。
一个天真的例子
为了使所有这些更加具体,我将使用一个运行示例来讨论所有这些。像我的所有例子一样,这是一个超级简单的例子; 小到可以变得不真实,但希望足以让你可视化正在发生的事情,而不会陷入真实范例的沼泽。
在这个例子中,我正在编写一个组件,提供由特定导演指导的电影列表。这种惊人有用的功能通过单一方法实现。
class MovieLister ...
public Movie [] moviesDirectedBy(String arg){ 列出allMovies = finder.findAll(); for(Iterator it = allMovies.iterator(); it.hasNext();){ 电影电影=(电影)it.next(); if(!movie.getDirector()。equals(arg))it.remove(); } return(Movie [])allMovies.toArray(new Movie [allMovies.size()]); }
这个函数的实现极端天真,它要求一个finder对象(我们将在稍后得到)返回它所知道的每一部电影。然后它只是搜索这个列表以返回由特定导演指导的那些。这件特别的天真我不打算解决,因为它只是本文真正意义上的脚手架。
本文的真正意义在于此finder对象,或者特别是我们如何将lister对象与特定的finder对象连接起来。这很有趣的原因是我希望我的精彩 moviesDirectedBy
方法完全独立于所有电影的存储方式。因此,所有方法都是指查找器,所有查找程序都知道如何响应该 findAll
方法。我可以通过为finder定义一个接口来解决这个问题。
公共界面MovieFinder { 列出findAll(); }
现在所有这一切都很好地解耦了,但在某些时候我必须拿出一个具体的课程来实际拍摄电影。在这种情况下,我将此代码放在我的lister类的构造函数中。
class MovieLister ...
私人MovieFinder发现者; public MovieLister(){ finder = new ColonDelimitedMovieFinder(“movies1.txt”); }
实现类的名称来自于我从冒号分隔的文本文件中获取列表的事实。我会告诉你细节,毕竟这只是有一些实现。
现在,如果我只为自己使用这个课程,这一切都很好,花花公子。但是当我的朋友被这种奇妙功能的欲望所淹没并想要我的程序副本时会发生什么呢?如果他们还将他们的电影列表存储在名为“movies1.txt”的冒号分隔的文本文件中,那么一切都很棒。如果他们的电影文件名称不同,则可以很容易地将文件名放在属性文件中。但是,如果他们有一个完全不同的存储电影列表的形式:sql数据库,XML文件,Web服务,或只是另一种格式的文本文件?在这种情况下,我们需要一个不同的类来获取数据。现在因为我已经定义了一个MovieFinder
界面,这不会改变我的界面moviesDirectedBy
方法。但是我仍然需要一些方法来获得正确的finder实现的实例。
图1:使用lister类中的简单创建的依赖项
图1显示了这种情况的依赖关系。该MovieLister
班是取决于双方的 MovieFinder
接口和取决于实施方案。我们更喜欢它,如果它只依赖于接口,那么我们如何使一个实例工作?
在我的EAA书中,我们将这种情况描述为一个插件。查找程序的实现类在编译时没有链接到程序中,因为我不知道我的朋友将使用什么。相反,我们希望我的列表能够使用任何实现,并且在稍后的时间点插入该实现,不在我的手中。问题是我如何建立这个链接,以便我的列表类不知道实现类,但仍然可以与实例通信来完成它的工作。
将其扩展为真实系统,我们可能会有许多此类服务和组件。在每种情况下,我们都可以通过接口与它们交谈来抽象我们对这些组件的使用(如果组件没有考虑到接口,则使用适配器)。但是如果我们希望以不同的方式部署这个系统,我们需要使用插件来处理与这些服务的交互,这样我们就可以在不同的部署中使用不同的实现。
所以核心问题是我们如何将这些插件组装到一个应用程序中?这是这种新型轻质容器所面临的主要问题之一,而且通常它们都是使用Inversion of Control来实现的。
控制反转
当这些容器谈到它们如何如此有用时,因为它们实现了“控制反转”,我最终感到非常困惑。控制反转是框架的一个共同特征,所以说这些轻量级容器是特殊的,因为它们使用控制反转就像说我的汽车是特殊的,因为它有轮子。
问题是:“他们反转的控制方面是什么?” 当我第一次遇到控制反转时,它就是用户界面的主要控制。早期用户界面由应用程序控制。您将拥有一系列命令,例如“输入名称”,“输入地址”; 你的程序会驱动提示并获取每个提示的响应。使用图形(甚至基于屏幕)的UI,UI框架将包含此主循环,而您的程序则为屏幕上的各个字段提供事件处理程序。该程序的主要控制被颠倒过来,从您移到框架。
对于这种新的容器,反转是关于他们如何查找插件实现。在我的天真例子中,lister通过直接实例化它来查找finder实现。这会阻止取景器成为插件。这些容器使用的方法是确保插件的任何用户遵循一些允许单独的汇编程序模块将实现注入列表器的约定。
因此,我认为我们需要一个更具体的名称来模式。控制反转过于笼统,因此人们会发现它令人困惑。结果与各种IoC倡导者进行了大量讨论,我们选择了Dependency Injection这个名称 。
我将首先讨论各种形式的依赖注入,但我现在要指出,这不是从应用程序类中删除依赖项到插件实现的唯一方法。您可以使用的另一种模式是Service Locator,我将在完成解释依赖注入之后再讨论。
依赖注入的形式
依赖注入的基本思想是拥有一个单独的对象,一个汇编程序,它使用适当的finder接口实现填充lister类中的字段,从而产生一个依赖于图2的依赖关系图。
图2:依赖注入器的依赖关系
依赖注入有三种主要风格。我正在使用的名称是Constructor Injection,Setter Injection和Interface Injection。如果您在当前关于控制反转的讨论中读到这些内容,您会听到这些内容称为类型1 IoC(接口注入),类型2 IoC(setter注入)和类型3 IoC(构造函数注入)。我发现数字名称很难记住,这就是为什么我使用了我在这里的名字。
使用picocontainer进行构造函数注入
我将首先展示如何使用名为PicoContainer的轻量级容器完成此注入。我从这里开始主要是因为我在ThoughtWorks的几位同事非常积极地开发picocontainer(是的,这是一种公司裙带关系。)
picocontainer使用构造函数来决定如何将finder实现注入lister类。为此,电影lister类需要声明一个包含它需要注入的所有内容的构造函数。
class MovieLister ...
public MovieLister(MovieFinder finder){ this.finder = finder; }
查找器本身也将由pico容器管理,因此将具有由容器注入其中的文本文件的文件名。
class ColonMovieFinder ...
public ColonMovieFinder(String filename){ this.filename = filename; }
然后需要告诉pico容器将哪个实现类与每个接口关联,以及将哪个字符串注入finder。
private Mutablepicocontainer configureContainer(){ Mutablepicocontainer pico = new Defaultpicocontainer(); 参数[] finderParams = {new ConstantParameter(“movies1.txt”)}; pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder.class,finderParams); pico.registerComponentImplementation(MovieLister.class); 返回微微; }
此配置代码通常设置在不同的类中。对于我们的示例,使用我的lister的每个朋友可能会在他们自己的某个安装类中编写适当的配置代码。当然,将这种配置信息保存在单独的配置文件中是很常见的。您可以编写一个类来读取配置文件并适当地设置容器。虽然picocontainer本身不包含此功能,但有一个名为NanoContainer的密切相关的项目,它提供了适当的包装器,允许您拥有XML配置文件。这样的nano容器将解析XML,然后配置基础pico容器。该项目的理念是将配置文件格式与底层机制分开。
要使用容器,您可以编写类似这样的代码。
public void testWithPico(){ Mutablepicocontainer pico = configureContainer(); MovieLister lister =(MovieLister)pico.getComponentInstance(MovieLister.class); 电影[] movies = lister.movi??esDirectedBy(“Sergio Leone”); assertEquals(“西方的黄飞鸿”,电影[0] .getTitle()); }
虽然在这个例子中我使用了构造函数注入,但picocontainer也支持setter注入,尽管它的开发人员更喜欢构造函数注入。
与春天的塞特犬注射
在Spring框架是企业Java开发一个广泛的框架。它包括事务的抽象层,持久性框架,Web应用程序开发和JDBC。与picocontainer一样,它支持构造函数和setter注入,但它的开发人员倾向于更喜欢setter注入 - 这使得它成为这个示例的合适选择。
为了让我的电影列表接受注入,我定义了该服务的设置方法
class MovieLister ...
私人MovieFinder发现者; public void setFinder(MovieFinder finder){ this.finder = finder; }
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”/> </属性> </豆> <bean id =“MovieFinder”class =“spring.ColonMovieFinder”> <property name =“filename”> <值> movies1.txt </值> </属性> </豆> </豆>
然后测试看起来像这样。
public void testWithSpring()抛出异常{ ApplicationContext ctx = new FileSystemXmlApplicationContext(“spring.xml”); MovieLister lister =(MovieLister)ctx.getBean(“MovieLister”); 电影[] movies = lister.movi??esDirectedBy(“Sergio Leone”); assertEquals(“西方的黄飞鸿”,电影[0] .getTitle()); }
接口注入
第三种注射技术是定义和使用注射接口。Avalon是一个在某些地方使用这种技术的框架的例子。稍后我将详细讨论,但在这种情况下,我将使用一些简单的示例代码。
通过这种技术,我首先定义一个我将用于执行注入的接口。这是将电影查找器注入对象的界面。
public interface InjectFinder { void injectFinder(MovieFinder finder); }
此接口将由提供MovieFinder接口的任何人定义。它需要由任何想要使用finder的类实现,例如lister。
MovieLister类实现InjectFinder
public void injectFinder(MovieFinder finder){ this.finder = finder; }
public interface InjectFinderFilename { void injectFilename(String filename); }
class ColonMovieFinder实现MovieFinder,InjectFinderFilename ......
public void injectFilename(String filename){ this.filename = filename; }
然后,像往常一样,我需要一些配置代码来连接实现。为简单起见,我将在代码中执行此操作。
班级测试员......
私人集装箱; private void configureContainer(){ container = new Container(); registerComponents(); registerInjectors(); container.start(); }
此配置有两个阶段,通过查找键注册组件与其他示例非常相似。
班级测试员......
private void registerComponents(){ container.registerComponent(“MovieLister”,MovieLister.class); container.registerComponent(“MovieFinder”,ColonMovieFinder.class); }
一个新步骤是注册将注入相关组件的注射器。每个注入接口都需要一些代码来注入依赖对象。在这里,我通过向容器注册注入器对象来完成此操作。每个喷射器对象实现喷射器接口。
班级测试员......
private void registerInjectors(){ container.registerInjector(InjectFinder.class,container.lookup(“MovieFinder”)); container.registerInjector(InjectFinderFilename.class,new FinderFilenameInjector()); }
公共接口Injector { public void inject(Object target); }
当依赖项是为此容器编写的类时,组件实现注入器接口本身是有意义的,就像我在这里使用电影查找器一样。对于泛型类,例如字符串,我在配置代码中使用内部类。
class ColonMovieFinder实现Injector ...
public void inject(Object target){ ((InjectFinder)target).injectFinder(this); }
班级测试员......
公共静态类FinderFilenameInjector实现Injector { public void inject(Object target){ ((InjectFinderFilename)目标).injectFilename( “movies1.txt”); } }
然后测试使用容器。
班级测试员......
public void testIface(){ configureContainer(); MovieLister lister =(MovieLister)container.lookup(“MovieLister”); 电影[] movies = lister.movi??esDirectedBy(“Sergio Leone”); assertEquals(“西方的黄飞鸿”,电影[0] .getTitle()); }
容器使用声明的注入接口来确定依赖关系,并使用注入器来注入正确的依赖项。(我在这里做的具体容器实现对于这项技术并不重要,我不会展示它,因为你只会笑。)
使用服务定位器
Dependency Injector的主要好处是它消除了MovieLister
类对具体 MovieFinder
实现的依赖性。这允许我给朋友们提供给他们的信息,并让他们为自己的环境插入合适的实现。注入不是打破这种依赖的唯一方法,另一种是使用服务定位器。
服务定位器背后的基本思想是拥有一个知道如何获取应用程序可能需要的所有服务的对象。因此,此应用程序的服务定位器将具有在需要时返回电影查找器的方法。当然这只会减轻负担,我们仍然需要将定位器放入列表器中,从而产生图3的依赖关系
图3:服务定位器的依赖关系
在这种情况下,我将使用ServiceLocator作为单例注册表。然后,lister可以在实例化时使用它来获取finder。
class MovieLister ...
MovieFinder finder = ServiceLocator.movi??eFinder();
class ServiceLocator ...
public static MovieFinder movieFinder(){ return soleInstance.movi??eFinder; } private static ServiceLocator soleInstance; 私人MovieFinder movieFinder;
与注入方法一样,我们必须配置服务定位器。这里我是在代码中进行的,但是使用一种从配置文件中读取适当数据的机制并不难。
班级测试员......
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.movi??eFinder = movieFinder; }
这是测试代码。
班级测试员......
public void testSimple(){ 配置(); MovieLister lister = new MovieLister(); 电影[] movies = lister.movi??esDirectedBy(“Sergio Leone”); assertEquals(“西方的黄飞鸿”,电影[0] .getTitle()); }
我经常听到这样的抱怨:这些服务定位器是一件坏事,因为它们不可测试,因为你不能替代它们的实现。当然,你可以设计它们以解决这种麻烦,但你不必这样做。在这种情况下,服务定位器实例只是一个简单的数据持有者。我可以使用我的服务的测试实现轻松创建定位器。
对于更复杂的定位器,我可以子类化服务定位器并将该子类传递给注册表的类变量。我可以更改静态方法来调用实例上的方法,而不是直接访问实例变量。我可以通过使用特定于线程的存储来提供特定于线程的定位器。所有这一切都可以在不更改服务定位器的客户端的情况下完成。
想到这一点的一种方法是服务定位器是注册表而不是单例。单例提供了一种实现注册表的简单方法,但该实现决策很容易改变。
为定位器使用隔离接口
上述简单方法的一个问题是,MovieLister依赖于完整的服务定位器类,即使它只使用一个服务。我们可以通过使用角色界面来减少这种情况 。这样,列表器可以只声明它需要的接口,而不是使用完整的服务定位器接口。
在这种情况下,列表器的提供者还将提供一个定位器接口,它需要抓住取景器。
public interface MovieFinderLocator { public MovieFinder movieFinder();
然后,定位器需要实现此接口以提供对查找程序的访问。
MovieFinderLocator locator = ServiceLocator.locator(); MovieFinder finder = locator.movi??eFinder();
public static ServiceLocator locator(){ 返回soleInstance; } public MovieFinder movieFinder(){ return movieFinder; } private static ServiceLocator soleInstance; 私人MovieFinder movieFinder;
您会注意到,由于我们想要使用接口,我们不能再通过静态方法访问服务了。我们必须使用该类来获取定位器实例,然后使用它来获取我们需要的内容。
动态服务定位器
上面的示例是静态的,因为服务定位器类具有您需要的每个服务的方法。这不是执行此操作的唯一方法,您还可以创建一个动态服务定位器,允许您将所需的任何服务存储到其中并在运行时进行选择。
在这种情况下,服务定位器使用映射而不是每个服务的字段,并提供获取和加载服务的通用方法。
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(密钥,服务); }
配置涉及使用适当的密钥加载服务。
班级测试员......
private void configure(){ ServiceLocator locator = new ServiceLocator(); locator.loadService(“MovieFinder”,新的ColonMovieFinder(“movies1.txt”)); ServiceLocator.load(定位器); }
我使用相同的密钥字符串来使用该服务。
class MovieLister ...
MovieFinder finder =(MovieFinder)ServiceLocator.getService(“MovieFinder”);
总的来说,我不喜欢这种方法。虽然它确实很灵活,但它并不是很明确。我能找到如何获得服务的唯一方法是通过文本键。我更喜欢显式方法,因为通过查看接口定义可以更容易地找到它们的位置。
使用定位器和注射Avalon
依赖注入和服务定位器不一定是相互排斥的概念。将两者结合使用的一个很好的例子是Avalon框架。Avalon使用服务定位器,但使用注入来告诉组件在哪里找到定位器。
Berin Loritsch使用Avalon向我发送了这个简单版本的运行示例。
公共类MyMovieLister实现MovieLister,Serviceable { 私人MovieFinder发现者; public void service(ServiceManager manager)抛出ServiceException { finder =(MovieFinder)manager.lookup(“finder”); }
服务方法是接口注入的示例,允许容器将服务管理器注入MyMovieLister。服务管理器是服务定位器的示例。在此示例中,列表器不会将管理器存储在字段中,而是立即使用它来查找它存储的查找器。
决定使用哪个选项
到目前为止,我专注于解释我如何看待这些模式及其变化。现在我可以开始讨论它们的优点和缺点,以帮助确定使用哪些以及何时使用。
服务定位器与依赖注入
基本选择是服务定位器和依赖注入之间。第一点是两个实现都提供了天真示例中缺少的基本解耦 - 在这两种情况下,应用程序代码都独立于服务接口的具体实现。这两种模式之间的重要区别在于如何为应用程序类提供该实现。使用服务定位器,应用程序类通过发送给定位器的消息明确地请求它。使用注入没有明确的请求,服务出现在应用程序类中 - 因此控制反转。
控制反转是框架的一个共同特征,但它是有代价的。当您尝试调试时,它往往很难理解并导致问题。所以总的来说,我宁愿避免它,除非我需要它。这并不是说这是一件坏事,只是因为我认为它需要通过更简单的替代方案来证明自己的合理性。
关键的区别在于,使用服务定位器,服务的每个用户都对定位器具有依赖性。定位器可以隐藏其他实现的依赖关系,但您确实需要查看定位器。因此,定位器和注入器之间的决定取决于该依赖性是否是一个问题。
使用依赖项注入可以帮助您更轻松地查看组件依赖项。使用依赖注入器,您可以查看注入机制,例如构造函数,并查看依赖项。使用服务定位器,您必须在源代码中搜索定位器的调用。具有查找引用功能的现代IDE使这更容易,但它仍然不像查看构造函数或设置方法那么容易。
这很大程度上取决于服务用户的性质。如果要使用各种使用服务的类构建应用程序,那么从应用程序类到定位器的依赖关系不是什么大问题。在我向朋友提供电影列表的示例中,使用服务定位器的效果非常好。他们所需要做的就是将定位器配置为通过一些配置代码或通过配置文件挂钩正确的服务实现。在这种情况下,我没有看到喷射器的反转提供任何引人注目的东西。
如果列表程序是我提供给其他人正在编写的应用程序的组件,则会产生差异。在这种情况下,我不太了解客户将要使用的服务定位器的API。每个客户可能都有自己不兼容的服务定位器。我可以通过使用隔离接口解决其中一些问题。每个客户都可以编写一个匹配我的接口到其定位器的适配器,但无论如何我仍然需要查看第一个定位器来查找我的特定接口。一旦适配器出现,那么直接连接到定位器的简单性就开始下滑。
由于使用进样器,您没有从组件到进样器的依赖关系,因此组件一旦配置就无法从进样器获得进一步的服务。
人们喜欢依赖注入的一个常见原因是它使测试更容易。这里的要点是,要进行测试,您需要使用存根或模拟轻松替换实际服务实现。然而,依赖注入和服务定位器之间确实没有区别:两者都非常适合存根。我怀疑这个观察来自项目,人们没有努力确保他们的服务定位器可以很容易地被替换。这是持续测试有用的地方,如果您不能轻松地存储测试服务,那么这意味着您的设计存在严重问题。
当然,测试问题因非常具有侵入性的组件环境而恶化,例如Java的EJB框架。我的观点是,这些类型的框架应该最小化它们对应用程序代码的影响,特别是不应该做的事情会减慢编辑 - 执行周期。使用插件替代重量级组件可以帮助完成此过程,这对于测试驱动开发等实践至关重要。
因此,主要问题是那些编写代码的人希望在编写器控制之外的应用程序中使用。在这些情况下,即使是关于服务定位器的最小假设也是一个问题。
构造函数与Setter注入
对于服务组合,您总是必须有一些约定才能将事物连接在一起。注入的优势主要在于它需要非常简单的约定 - 至少对于构造函数和setter注入。您不必在组件中执行任何奇怪操作,并且注入器可以非常简单地完成所有配置。
接口注入更具侵入性,因为您必须编写大量接口才能将所有内容整理出来。对于容器所需的一小组接口,例如在Avalon的方法中,这并不算太糟糕。但是组装组件和依赖项需要做很多工作,这就是为什么当前的轻量级容器需要使用setter和构造函数注入。
setter和构造函数注入之间的选择很有意思,因为它反映了面向对象编程的一个更普遍的问题 - 你应该在构造函数中填充字段还是用setter填充字段。
我的长期默认对象是尽可能在构造时创建有效对象。这个建议可以追溯到Kent Beck的Smalltalk最佳实践模式:构造方法和构造函数参数方法。带参数的构造函数可以清楚地说明在显而易见的位置创建有效对象的含义。如果有多种方法可以执行此操作,请创建显示不同组合的多个构造函数。
构造函数初始化的另一个优点是它允许您通过简单地不提供setter来清楚地隐藏任何不可变的字段。我认为这很重要 - 如果某些东西不应该改变,那么缺乏一个setter就可以很好地传达这一点。如果你使用setter进行初始化,那么这可能会变得很痛苦。(事实上??在这些情况下,我宁愿避免通常的设置惯例,我更喜欢一种类似的方法initFoo
,强调这是你应该只在出生时做的事情。)
但在任何情况下都有例外。如果你有很多构造函数参数,那么事情看起来会很混乱,特别是在没有关键字参数的语言中。确实,长构造函数通常是应该被拆分的过度繁忙对象的标志,但有时候这就是你所需要的。
如果您有多种方法来构造有效对象,则很难通过构造函数显示它,因为构造函数只能根据参数的数量和类型而变化。这就是工厂方法发挥作用时,它们可以使用私有构造函数和setter的组合来实现它们的工作。组件组装的经典工厂方法的问题在于它们通常被视为静态方法,并且您不能在接口上使用它们。你可以创建一个工厂类,但那后它就成了另一个服务实例。工厂服务通常是一种很好的策略,但您仍然需要使用其中一种技术来实例化工厂。
如果您有简单的参数(如字符串),构造函数也会受到影响。使用setter注入,您可以为每个setter指定一个名称,以指示该字符串应该执行的操作。对于构造函数,您只是依赖于位置,这更难以遵循。
如果你有多个构造函数和继承,那么事情就会变得特别尴尬。为了初始化你必须提供的构造函数转发到每个超类构造函数,同时还添加自己的参数。这可能会导致更大的构造者爆炸。
尽管存在缺点,但我首选的是从构造函数注入开始,但是一旦我上面列出的问题开始成为一个问题就准备好切换到setter注入。
这个问题引发了各种团队之间的争论,他们提供依赖注入器作为其框架的一部分。然而,似乎构建这些框架的大多数人已经意识到支持这两种机制很重要,即使对其中一种机制有偏好。
代码或配置文件
一个单独但经常混淆的问题是,是否在API上使用配置文件或代码来连接服务。对于可能在许多地方部署的大多数应用程序,单独的配置文件通常最有意义。几乎所有时间这都是一个XML文件,这是有道理的。但是,在某些情况下,使用程序代码进行组装会更容易。一个案例是你有一个简单的应用程序,没有很多部署变化。在这种情况下,一些代码可以比单独的XML文件更清晰。
一个对比的案例是装配非常复杂,涉及条件步骤。一旦你开始接近编程语言,那么XML开始崩溃,最好使用具有所有语法的真实语言来编写清晰的程序。然后编写一个执行程序集的构建器类。如果您有不同的构建器方案,则可以提供多个构建器类,并使用简单的配置文件在它们之间进行选择。
我经常认为人们过于渴望定义配置文件。编程语言通常是一种简单而强大的配置机制。现代语言可以轻松编译可用于组装大型系统插件的小型汇编程序。如果编译很麻烦,那么脚本语言也可以很好地工作。
通常说配置文件不应该使用编程语言,因为它们需要由非程序员编辑。但这种情况多久发生一次?人们真的希望非程序员改变复杂服务器端应用程序的事务隔离级别吗?非语言配置文件只在它们很简单的情况下才能正常工作。如果它们变得复杂,那么就该考虑使用适当的编程语言了。
我们目前在Java世界中看到的一件事是配置文件的混乱,其中每个组件都有自己的配置文件,这些文件与其他人不同。如果您使用这些组件中的十几个,则可以轻松地使用十几个配置文件来保持同步。
我的建议是始终提供一种使用编程接口轻松完成所有配置的方法,然后将单独的配置文件视为可选功能。您可以轻松构建配置文件处理以使用编程接口。如果您正在编写组件,则将其留给您的用户,无论是使用编程接口,配置文件格式,还是编写自己的自定义配置文件格式并将其绑定到编程接口
将配置与使用分开
所有这一切中的重要问题是确保将服务配置与其使用分开。实际上,这是一个基本的设计原则,它将接口与实现分离开来。当条件逻辑决定实例化哪个类时,我们在面向对象程序中看到这一点,然后对该条件的未来评估是通过多态而不是通过重复的条件代码完成的。
如果这种分离在单个代码库中有用,那么当您使用组件和服务等外来元素时,这一点尤其重要。第一个问题是您是否希望将实现类的选择推迟到特定部署。如果是这样,你需要使用一些插件实现。一旦使用插件,插件的组装必须与应用程序的其余部分分开完成,这样您就可以轻松地替换不同的配置以进行不同的部署。你如何实现这一点是次要的。此配置机制可以配置服务定位器,也可以使用注入直接配置对象。
一些其他问题
在本文中,我主要关注使用依赖注入和服务定位器的服务配置的基本问题。还有更多的主题可以引起注意,但我还没来得及深入研究。特别是存在生命周期行为的问题。某些组件具有不同的生命周期事件:例如停止和启动。另一个问题是对这些容器使用面向方面的想法越来越感兴趣。虽然我目前没有在文章中考虑过这些内容,但我希望通过扩展本文或撰写另一篇文章来写更多相关内容。
通过查看专门针对轻量级容器的网站,您可以了解有关这些想法的更多信息。从picocontainer和spring网站进行冲浪 将引导您对这些问题进行更多讨论,并开始讨论其他一些问题。
结论性思考
当前的轻量级容器都有一个共同的基础模式,它们如何进行服务组装 - 依赖注入器模式。依赖注入是Service Locator的有用替代方案。在构建应用程序类时,两者大致相同,但我认为Service Locator由于其更直接的行为而略有优势。但是,如果要构建要在多个应用程序中使用的类,那么依赖注入是更好的选择。
如果使用依赖注入,则可以选择多种样式。我建议您遵循构造函数注入,除非您遇到该方法的一个特定问题,在这种情况下切换到setter注入。如果您选择构建或获取容器,请查找同时支持构造函数和setter注入的容器。
服务定位器和依赖注入之间的选择不如将服务配置与应用程序中的服务使用分离的原则重要。