翻译:控制容器的反转IoC和依赖注入模式DIP 概念发源地 Martin Fowler

说明

Martin Fowler 写于 2004年1月23日,为了更好了解对控制容器的反转和依赖注入模式的历史,所以翻译。

1. 控制容器的反转和依赖注入模式

Inversion of Control Containers and the Dependency Injection pattern
在Java社区中,涌现了许多轻量级的容器,这些容器可帮助将来自不同项目的组件组装成一个内聚的应用程序。这些容器的底层是它们执行接线方式的常见模式,它们以非常通用的名称“控制反转”引用。在本文中,我将以更具体的名称“依赖关系注入”深入研究此模式的工作原理,并将其与Service Locator替代方法进行对比。它们之间的选择不如将配置与使用分开的原理重要。

关于企业Java世界的有趣的事情之一是,为构建主流J2EE技术​​的替代品而进行了大量的活动,其中许多活动是在开源中进行的。这在很大程度上是对主流J2EE世界中重量级复杂性的一种反应,但其中很大一部分还在探索替代方案并提出创意。一个常见的问题是如何将不同的元素组合在一起:当Web控制器体系结构由数据库团队支持并且由彼此之间几乎不了解的不同团队构建时,如何将它们组合在一起。许多框架都在解决这个问题,并且正在扩展一些框架,以提供从不同层组装组件的一般能力。这些通常称为轻量级容器,示例包括PicoContainerSpring

这些容器的基础是许多有趣的设计原理,这些原理超出了这些特定的容器以及Java平台的范围。在这里,我想开始探讨其中一些原则。我使用的示例是在Java中编写的,但是像我的大多数写作一样,这些原理同样适用于其他OO环境,尤其是.NET。

2. 组件和服务

布线元素的主题几乎使我立即陷入围绕服务和组件一词的棘手的术语问题。您可以轻松找到关于这些事物的定义的长篇文章和相互矛盾的文章。出于我的目的,这是这些重载术语的当前用法。

我使用组件的意思是一整套软件,该软件原本打算由不受组件编写者控制的应用程序使用而无需更改。“无更改”是指使用的应用程序不会更改组件的源代码,尽管它们可能会通过以组件编写者允许的方式扩展组件来更改组件的行为。

服务与组件相似,供外部应用程序使用。主要区别在于,我希望组件在本地使用(例如jar文件,程序集,dll或源导入)。服务将通过同步或异步的某个远程接口(例如,Web服务,消息系统,RPC或套接字)远程使用。

我在本文中主要使用服务,但是许多相同的逻辑也可以应用于本地组件。实际上,通常您需要某种本地组件框架来轻松访问远程服务。但是编写“组件或服务”很容易引起阅读和书写的困扰,并且服务现在更加流行。

3. 一个天真的例子

为了使所有这些更加具体,我将使用一个正在运行的示例来讨论所有这些。像我所有的示例一样,它也是那些超级简单的示例之一。足够小以至于不真实,但希望足以让您直观地了解正在发生的情况而不会陷入真实示例的泥潭。

在此示例中,我正在编写一个组件,该组件提供由特定导演执导的电影列表。这个惊人的有用功能是通过一种方法实现的。

class MovieLister...

  public Movie[] moviesDirectedBy(String arg) {
      List allMovies = finder.findAll();
      for (Iterator it = allMovies.iterator(); it.hasNext();) {
          Movie movie = (Movie) it.next();
          if (!movie.getDirector().equals(arg)) it.remove();
      }
      return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
  }

这个功能的实现极端幼稚,它要求一个取景器对象(稍后我们将介绍)返回它知道的每部电影。然后,它只是在此列表中搜寻以返回由特定导演指挥的那些。我不会解决这个天真的问题,因为这只是本文的重点。

本文的重点是该查找程序对象,或者特别是我们如何将列表器对象与特定查找程序对象连接。之所以如此有趣,是因为我希望我的奇妙 moviesDirectedBy方法完全独立于所有电影的存储方式。因此,该方法所做的只是引用一个查找程序,而查找程序所做的全部就是知道如何响应该 findAll方法。我可以通过为查找器定义一个接口来实现这一点。

public interface MovieFinder {
    List findAll();
}

现在,所有这些都已经很好地分离了,但是在某些时候,我必须提出一个具体的课程来实际提出电影。在这种情况下,我将其代码放入lister类的构造函数中。

class MovieLister...

  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

实现类的名称来自以下事实:我从冒号分隔的文本文件中获取列表。我将为您保留所有细节,毕竟所有这些只是一些实现。

现在,如果我只为自己使用该类,那么一切都很好。但是,当我的朋友对这种出色功能的渴望不知所措,并且想要我的程序的副本时,会发生什么?如果他们还将电影列表存储在以冒号分隔的文本文件“ movies1.txt”中,那么一切都很好。如果他们的电影文件具有不同的名称,则可以很容易地将文件名称放入属性文件中。但是,如果他们以完全不同的方式存储电影列表的形式:SQL数据库,XML文件,Web服务或只是另一种格式的文本文件怎么办?在这种情况下,我们需要一个不同的类来获取该数据。现在,因为我已经定义了一个MovieFinder接口,所以不会改变我的moviesDirectedBy方法。但是我仍然需要某种方法来使正确的finder实现实例生效。
在这里插入图片描述
图1:在lister类中使用简单创建的依赖项

图1显示了这种情况的依赖性。该MovieLister班是取决于两个上 MovieFinder取决于实施方案接口和。如果它仅依赖于接口,我们会更喜欢它,但是我们如何使一个实例可以使用呢?

在我的书EAA中,我们将这种情况描述为插件。查找器的实现类在编译时未链接到程序中,因为我不知道我的朋友将使用什么。取而代之的是,我们希望我的列表器可以与任何实现一起使用,并在以后的某个时间点将其插入我的手中。问题是如何建立链接,以便我的列表器类不了解实现类,但仍然可以与实例对话以完成其工作。

将其扩展到一个真实的系统中,我们可能会有数十个这样的服务和组件。在每种情况下,我们都可以通过通过接口与这些组件进行通信来抽象化这些组件的使用(如果该组件在设计时并未考虑接口,则可以使用适配器)。但是,如果我们希望以不同的方式部署此系统,则需要使用插件来处理与这些服务的交互,以便我们可以在不同的部署中使用不同的实现。

因此,核心问题是如何将这些插件组装到应用程序中?这是这种新型的轻量级容器面临的主要问题之一,并且普遍地,它们都使用控制反转来解决。

4. 控制反转

当这些容器谈论它们因实现“控制反转”而如此有用时,我最终感到非常困惑。控制反转是框架的常见特征,因此说这些轻量级容器之所以特别是因为它们使用控制反转,就好像说我的汽车很特别,因为它带有4个轮子。

问题是:“控制权在什么方面倒置?” 当我第一次遇到控件反转时,它位于用户界面的主控件中。早期的用户界面由应用程序控制。您将有一系列命令,例如“输入名称”,“输入地址”;您的程序将驱动提示并获取对每个提示的响应。使用图形(甚至基于屏幕)UI时,UI框架将包含此主循环,而您的程序将为屏幕上的各个字段提供事件处理程序。程序的主控件被反转,从您移至框架。

对于这种新型容器,反转是关于它们如何查找插件实现的。在我幼稚的示例中,列表器通过直接实例化查找器实现。这将阻止查找程序成为插件。这些容器使用的方法是确保插件的任何用户都遵循某种约定,该约定允许单独的汇编器模块将实现注入到列表器中。

因此,我认为我们需要为该模式指定一个更具体的名称。控制反转是一个过于笼统的术语,因此人们会感到困惑。与IoC倡导者进行了大量讨论之后,我们决定使用依赖注入这个名称 。

我将首先讨论各种形式的依赖注入,但是现在我要指出,这并不是从应用程序类到插件实现中删除依赖的唯一方法。可以用来执行此操作的另一种模式是Service Locator,在解释完依赖注入之后,我将讨论它。

5. 依赖注入的形式

Dependency Injection的基本思想是拥有一个单独的对象,即一个汇编器(Assembler),该汇编器使用finder接口的适当实现填充lister类中的字段,从而产生图2所示的依赖关系图。

在这里插入图片描述

图2:依赖注入器的依赖

依赖项注入有三种主要样式。我为它们使用的名称是构造函数注入,Setter注入和接口注入。如果在当前有关控制反转的讨论中读到这些内容,您会听到这些信息分别称为1型IoC(接口注入),2型IoC(设定者注入)和3型IoC(构造函数注入)。我发现很难记住数字名称,这就是为什么我使用这里的名称的原因。

5.1 PicoContainer的构造方法注入

我将首先展示如何使用名为PicoContainer的轻型容器完成此注入。我从这里开始的主要原因是,ThoughtWorks的几个同事对PicoContainer的开发非常活跃(是的,这是一种公司裙带关系。)

PicoContainer使用构造函数来决定如何将finder实现注入到lister类中。为此,电影列表器类需要声明一个构造函数,其中包括需要注入的所有内容。

class MovieLister...

  public MovieLister(MovieFinder finder) {
      this.finder = finder;       
  }

取景器本身也将由pico容器管理,因此,容器将注入文本文件的文件名。

class ColonMovieFinder...

  public ColonMovieFinder(String filename) {
      this.filename = filename;
  }

然后,需要告知pico容器与每个接口关联的实现类,以及将哪些字符串注入到查找器中。

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;
}

此配置代码通常在其他类中设置。对于我们的示例,使用我的列表器的每个朋友都可以在自己的某些安装程序类中编写相应的配置代码。当然,通常将这种配置信息保存在单独的配置文件中。您可以编写一个类来读取配置文件并适当地设置容器。尽管PicoContainer本身不包含此功能,但是有一个密切相关的项目NanoContainer,该项目提供适当的包装以允许您拥有XML配置文件。这样的纳米容器将解析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注入,尽管其开发人员确实更喜欢构造函数注入。

5.2 Spring框架注入

Spring框架是企业Java开发一个广泛的框架。它包括事务,持久性框架,Web应用程序开发和JDBC的抽象层。像PicoContainer的它同时支持构造函数和setter注入,但它的开发者倾向于更喜欢setter注入-这使得它在这个例子中一个合适的选择。

为了让我的电影列表管理员接受注入,我为该服务定义了一种设置方法

class MovieLister...

  private MovieFinder finder;
public void setFinder(MovieFinder finder) {
  this.finder = finder;
}


同样,我为文件名定义了一个setter。

class ColonMovieFinder...

  public void setFilename(String filename) {
      this.filename = filename;
  }

第三步是设置文件的配置。Spring支持通过XML文件以及通过代码进行配置,但是XML是实现它的预期方式。

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

然后测试看起来像这样。

public void testWithSpring() throws Exception {
    ApplicationContext ctx = 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());
}

5.3 接口注入

第三种注入技术是定义和使用接口进行注入。Avalon是在某些地方使用此技术的框架的示例。稍后我将详细讨论,但是在这种情况下,我将通过一些简单的示例代码来使用它。

通过这项技术,我首先定义了一个用于执行注入的接口。这是将电影查找器注入对象的界面。

public interface InjectFinder {
    void injectFinder(MovieFinder finder);
}

该界面由提供MovieFinder界面的人定义。任何想要使用查找器的类(例如列表器)都需要实现它。

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);

}

当依赖项是为此容器编写的类时,对于组件来说,实现注入器接口本身是有意义的,就像我在电影查找器中所做的那样。对于通用类,例如字符串,我在配置代码中使用内部类。

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 Testerpublic 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());
  }

容器使用声明的注入接口找出依赖关系,并使用注入器注入正确的依赖关系。(我在这里执行的特定容器实现对该技术并不重要,并且我不会展示它,因为您只会笑。)

6 使用Service Locator

依赖注入器的主要好处是,它消除了MovieLister类对具体 MovieFinder实现的依赖。这使我可以将列表提供给朋友,并让他们为自己的环境插入合适的实现。注入不是打破这种依赖关系的唯一方法,另一种方法是使用Service Locator

服务定位器的基本思想是拥有一个对象,该对象知道如何掌握应用程序可能需要的所有服务。因此,此应用程序的服务定位器将具有一种在需要时返回电影查找器的方法。当然,这只是转移了一点负担,我们仍然必须将定位器放入列表器中,从而产生图3的依赖项。
在这里插入图片描述
图3:服务定位器的依赖关系

在这种情况下,我将ServiceLocator用作单例注册表。然后,列表器可以在实例化时使用它来获取查找器。

class MovieLister...

  MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...

  public static MovieFinder movieFinder() {
      return soleInstance.movieFinder;
  }
  private static ServiceLocator soleInstance;
  private MovieFinder movieFinder;

与注入方法一样,我们必须配置Service Locator。在这里,我正在用代码进行操作,但是使用从配置文件中读取适当数据的机制并不难。

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;
  }

这是测试代码。

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());
  }

我经常听到抱怨,这些服务定位器是一件坏事,因为它们不可测试,因为您不能用它们替代实现。当然,您可以对它们进行糟糕的设计,以免遇到此类麻烦,但是您不必这样做。在这种情况下,服务定位器实例只是一个简单的数据持有者。我可以使用服务的测试实现轻松地创建定位器。

对于更复杂的定位器,我可以对服务定位器进行子类化,然后将该子类传递到注册表的类变量中。我可以更改静态方法以在实例上调用方法,而不是直接访问实例变量。我可以通过使用特定于线程的存储来提供特定于线程的定位器。所有这些都可以在不更改服务定位器客户的情况下完成。

想到这一点的一种方法是,服务定位器是注册表而不是单例。单例提供了实现注册表的简单方法,但是该实现决策很容易更改。

6.1 对定位器使用隔离的接口

上面的简单方法的问题之一是,即使MovieLister仅使用一项服务,它也依赖于完整的服务定位器类。我们可以通过使用角色接口来减少这种情况 。这样,列表器可以仅声明它需要的接口位,而不必使用完整的服务定位器接口。

在这种情况下,列表提供者的提供者还将提供一个定位器接口,它需要掌握查找器。

public interface MovieFinderLocator {
    public MovieFinder 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;

您会注意到,由于我们要使用接口,因此不能再通过静态方法访问服务了。我们必须使用该类来获取定位器实例,然后使用该类来获取所需的对象。

6.2 动态服务定位器

上面的示例是静态的,因为服务定位器类具有用于您需要的每个服务的方法。这不是唯一的方法,您还可以创建一个动态服务定位器,使您可以将所需的任何服务存储在其中,并在运行时进行选择。

在这种情况下,服务定位器使用映射代替每个服务的字段,并提供获取和加载服务的通用方法。

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");

总的来说,我不喜欢这种方法。尽管它肯定是灵活的,但不是很明确。我唯一能找到服务的方法是通过文本键。我更喜欢显式方法,因为通过查看接口定义可以更轻松地找到它们的位置。

6.3 同时使用定位器和Avalon进行注射

依赖注入和服务定位器不一定是互斥的概念。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");
    } 

服务方法是接口注入的一个示例,它允许容器将服务管理器注入MyMovieLister。服务管理器是服务定位器的一个示例。在此示例中,列表器未将管理器存储在字段中,而是立即使用它来查找它存储的查找器。

7. 决定使用哪个选项

到目前为止,我一直专注于解释如何看待这些模式及其变化。现在,我可以开始讨论它们的优缺点,以帮助确定使用哪些以及何时使用。

7.1 服务定位器与依赖注入

基本选择是在服务定位器和依赖注入之间。第一点是,这两种实现都提供了天真的示例中缺少的基本解耦-在两种情况下,应用程序代码都独立于服务接口的具体实现。两种模式之间的重要区别在于如何将实现提供给应用程序类。使用服务定位器,应用程序类通过向定位器的消息显式地请求它。使用注入时,没有显式请求,该服务将出现在应用程序类中-因此控制权反转。

控制反转是框架的共同特征,但这是有代价的。当您尝试调试时,它往往难以理解并导致问题。因此,总的来说,我更喜欢避免它,除非我需要它。这并不是说这是一件坏事,只是我认为它需要证明自己比更直接的选择更合理。

关键区别在于,使用服务定位器时,服务的每个用户都对定位器具有依赖性。定位器可以隐藏对其他实现的依赖关系,但是您确实需要查看定位器。因此,定位器和注入器之间的决定取决于该依赖关系是否成问题。

使用依赖注入可以帮助您更轻松地了解组件的依赖关系。使用依赖注入器,您可以仅查看注入机制,例如构造函数,并查看依赖关系。使用服务定位器,您必须在源代码中搜索对定位器的调用。具有查找引用功能的现代IDE使此操作变得更容易,但仍不像查看构造函数或设置方法那样容易。

这在很大程度上取决于服务用户的性质。如果要使用使用服务的各种类来构建应用程序,则从应用程序类到定位器的依赖关系不大。在我的示例中,将电影列表提供给我的朋友,然后使用服务定位器非常有效。他们所需要做的就是通过一些配置代码或配置文件将定位器配置为挂接正确的服务实现。在这种情况下,我认为喷油器的倒置并不能提供任何令人信服的东西。

如果列表器是我提供给其他人正在编写的应用程序的组件,则会有区别。在这种情况下,我对客户要使用的服务定位器的API不太了解。每个客户可能都有自己不兼容的服务定位器。我可以通过使用隔离的接口来解决一些问题。每个客户都可以编写一个使我的界面与他们的定位器匹配的适配器,但是无论如何,我仍然需要看到第一个定位器来查找我的特定接口。一旦适配器出现,与定位器直接连接的简便性便开始下滑。

由于使用注入器,您没有组件到注入器的依赖关系,因此一旦配置,该组件就无法从注入器获得更多服务。

人们偏爱依赖注入的一个普遍原因是它使测试更加容易。这里的重点是要进行测试,您需要轻松地用存根或模拟替换真实的服务实现。但是,依赖注入和服务定位器之间实际上并没有什么区别:两者都非常适合存根。我怀疑这种观察来自于人们没有努力确保可以轻松替换其服务定位器的项目。这是持续测试的地方,如果您无法轻松地对服务进行存根测试,那么这意味着您的设计存在严重问题。

当然,非常麻烦的组件环境(例如Java的EJB框架)会加剧测试问题。我的观点是,这类框架应将其对应用程序代码的影响降至最低,尤其是不应做会减慢编辑-执行周期的事情。使用插件替代重量级组件在很大程度上帮助了此过程,这对于诸如测试驱动开发之类的实践至关重要。

因此,主要问题是编写代码的人员希望在编写者无法控制的应用程序中使用它们。在这些情况下,即使是关于服务定位器的最小假设也是一个问题。

7.2 构造函数注入与setter注入

对于服务组合,您始终必须有一些约定才能将它们连接在一起。注入的优点主要是它需要非常简单的约定-至少对于构造函数和setter注入而言。您不必在组件中做任何奇怪的事情,并且对于进样器而言,配置所有组件非常简单。

接口注入更具侵入性,因为您必须编写许多接口才能使所有事情都得到解决。对于容器所需的一小部分接口,例如Avalon的方法,这还不错。但是组装组件和依赖项需要做很多工作,这就是为什么当前的轻量级容器使用setter和构造函数注入的原因。

在setter和构造函数注入之间进行选择很有趣,因为它反映了面向对象编程的一个更普遍的问题-应该在构造函数中还是使用setter填充字段。

我对对象的长期默认设置是尽可能多的,以便在构造时创建有效的对象。该建议可以追溯到Kent Beck的Smalltalk最佳实践模式:构造方法和构造参数方法。带参数的构造函数使您清楚地说明了在明显的位置创建有效对象的含义。如果有多种方法可以实现,请创建多个构造函数以显示不同的组合。

构造函数初始化的另一个优点是,您可以通过不提供设置器来清楚地隐藏任何不可变的字段。我认为这很重要-如果某些事情不应该更改,那么缺少二传手就可以很好地传达这一点。如果使用setter进行初始化,则可能会很痛苦。(实际上,在这种情况下,我宁愿避免使用常规的设置约定,而宁愿使用像之类的方法initFoo来强调它是您只应在出生时才做的事情。)

但是在任何情况下都有例外。如果您有很多构造函数参数,则看起来会很混乱,尤其是在没有关键字参数的语言中。的确,一个长构造函数通常是一个忙碌对象的信号,应将其拆分,但是在某些情况下,这就是您所需要的。

如果您有多种构造有效对象的方法,则可能很难通过构造函数来显示该对象,因为构造函数只能在参数的数量和类型上有所不同。这是工厂方法发挥作用的时候,它们可以结合使用私有构造函数和setter方法来实现其工作。经典的用于组件组装的工厂方法的问题在于它们通常被视为静态方法,而您不能在接口上使用它们。您可以创建一个工厂类,但这将成为另一个服务实例。工厂服务通常是一个很好的策略,但是您仍然必须使用此处的一种技术来实例化工厂。

如果您具有简单的参数(例如字符串),构造函数也会受到影响。使用setter注入,您可以为每个setter命名,以指示该字符串应该执行的操作。对于构造函数,您只是依靠位置,这很难遵循。

如果您有多个构造函数和继承关系,那么事情可能会变得特别尴尬。为了初始化所有内容,您必须提供构造函数以转发到每个超类构造函数,同时还要添加自己的参数。这可能导致构造函数的爆炸式增长。

尽管有很多缺点,我还是希望从构造函数注入开始,但是一旦我上面概述的问题开​​始成为问题,就可以准备切换到setter注入。

这个问题在提供依赖注入器作为其框架一部分的各个团队之间引起了很多争论。但是,似乎大多数构建这些框架的人已经意识到,即使偏爱其中一种机制,支持这两种机制也很重要。

7.3 代码或配置文件

一个单独但经常混淆的问题是是否使用配置文件或API上的代码来连接服务。对于大多数可能部署在许多地方的应用程序,通常最有意义的是使用单独的配置文件。几乎所有时间都将是一个XML文件,这很有意义。但是,在某些情况下,使用程序代码进行汇编会更容易。一种情况是,您有一个简单的应用程序,并且没有太多的部署差异。在这种情况下,一些代码可以比单独的XML文件更清晰。

一个相反的情况是组装非常复杂,涉及条件步骤。一旦开始接近编程语言,XML就会开始崩溃,最好使用具有所有语法的真实语言编写清晰的程序。然后,您编写一个进行组装的构建器类。如果您有不同的构建器方案,则可以提供多个构建器类,并使用简单的配置文件在它们之间进行选择。

我经常认为人们过于渴望定义配置文件。通常,编程语言会提供一种简单而强大的配置机制。现代语言可以轻松地编译小型汇编程序,这些汇编程序可用于为大型系统汇编插件。如果编译很麻烦,那么有些脚本语言也可以很好地工作。

人们常说配置文件不应使用编程语言,因为它们需要由非程序员编辑。但是,这种情况多久发生一次呢?人们真的希望非程序员会改变复杂服务器端应用程序的事务隔离级别吗?非语言配置文件仅在简单的范围内才能正常工作。如果它们变得复杂,那么该考虑使用适当的编程语言了。

目前,我们在Java世界中看到的一件事是配置文件混乱,每个组件都有自己的配置文件,这些文件与其他人的文件不同。如果使用这些组件中的许多组件,则可以轻松获得十二个配置文件来保持同步。

我的建议是始终提供一种通过编程界面轻松进行所有配置的方法,然后将单独的配置文件视为可选功能。您可以轻松构建配置文件处理以使用编程界面。如果要编写组件,则由用户决定是使用编程接口,配置文件格式,还是编写自己的自定义配置文件格式并将其绑定到编程接口中

7.4 将配置与使用分开

所有这一切中的重要问题是确保服务的配置与使用分开。确实,这是一个基本的设计原则,它是接口与实现的分离。当条件逻辑决定实例化哪个类,然后通过多态性而不是通过重复的条件代码完成对该条件的将来评估时,便会在面向对象程序中看到这种情况。

如果这种分离在单个代码库中有用,那么在使用诸如组件和服务之类的外部元素时,这一点尤为重要。第一个问题是您是否希望将实现类的选择推迟到特定的部署。如果是这样,则需要使用一些插件实现。一旦使用了插件,那么至关重要的是,插件的组装必须与应用程序的其余部分分开完成,以便您可以轻松地将不同的配置替换为不同的部署。您如何实现这一目标是次要的。此配置机制可以配置服务定位器,也可以使用注入直接配置对象。

8 一些其他问题

在本文中,我集中讨论了使用依赖注入和服务定位器进行服务配置的基本问题。还有更多的话题也值得关注,但是我还没有时间去探讨。特别是存在生命周期行为的问题。一些组件具有不同的生命周期事件:例如停止和启动。另一个问题是在这些容器中使用面向方面的思想的兴趣日益浓厚。尽管目前我还没有在本文中考虑此材料,但我还是希望通过扩展本文或撰写另一篇文章来撰写更多有关此材料的文章。

通过查看专门针对轻量级容器的网站,您可以找到有关这些想法的更多信息。从picocontainerspring网站上冲浪 将使您对这些问题进行更多的讨论,并开始一些其他问题。

10. 结论思想

当前的轻量级容器热潮都具有它们如何进行组装的基本模式-依赖注入器模式。依赖注入是服务定位器的有用替代方法。在构建应用程序类时,这两个类大致相同,但是我认为Service Locator由于其更直接的行为而略有优势。但是,如果要构建要在多个应用程序中使用的类,则依赖注入是一个更好的选择。

如果使用依赖注入,则有多种样式可供选择。我建议您遵循构造函数注入,除非您遇到该方法的特定问题之一,在这种情况下,请切换到setter注入。如果选择构建或获取容器,请寻找一个既支持构造函数注入又支持setter注入的容器。

在服务定位器和依赖注入之间进行选择比将应用程序中的服务配置与服务的使用分开的原理没有那么重要。

翻译自

https://martinfowler.com/articles/injection.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值