依赖注入容器有什么好处?

我明白依赖注入本身的好处。让我们以 Spring 为例。我也理解其他 Spring 特性的好处,比如 AOP、不同类型的助手等等。我只是想知道,XML 配置的好处是什么,比如:

<bean id="Mary" class="foo.bar.Female">
<property name="age" value="23"/>
</bean>
<bean id="John" class="foo.bar.Male">
<property name="girlfriend" ref="Mary"/>
</bean>

与普通的旧 Java 代码相比,例如:

Female mary = new Female();
mary.setAge(23);
Male john = new Male();
john.setGirlfriend(mary);

这是更容易调试,编译时间检查,可以理解的任何人谁只知道 Java。 那么,依赖注入框架的主要目的是什么呢


更新:
以防万一

IService myService;// ...
public void doSomething() {
myService.fetchData();
}

如果 myService 有多个实现,IoC 框架如何猜测我想要注入的是哪个实现?如果给定接口只有一个实现,并且我让 IoC 容器自动决定使用它,那么在出现第二个实现之后,它将被中断。如果故意只有一个接口的可能实现,那么就不需要注入它。

看到 IoC 的一小部分配置显示了它的好处,这将是非常有趣的。我已经使用 Spring 有一段时间了,我不能提供这样的例子。我可以展示单行代码,它们演示了 hibernate、 dwr 和我使用的其他框架的优点。


更新2:
我意识到 IoC 配置可以在不重新编译的情况下进行更改。这真的是个好主意吗?我可以理解,当有人想更改数据库凭据而不重新编译-他可能不是开发人员。在您的实践中,除了开发人员以外的其他人多久更改一次 IoC 配置?我认为对于开发人员来说,没有必要重新编译那个特定的类,而是改变配置。对于非开发人员,您可能希望让他的生活更轻松,并提供一些更简单的配置文件。


更新3:

接口及其具体实现之间映射的外部配置

有什么好处可以让它扩展呢?你没有把所有的代码都放在外部,但是你肯定可以——只要把它放在 ClassName.java.txt 文件中,在飞行中手动读取和编译——哇,你避免了重新编译。为什么要避免编译? !

您节省了编码时间,因为您以声明方式提供映射,而不是在过程代码中提供映射

我知道有时声明式方法可以节省时间。例如,我只声明一次 bean 属性和 DB 列之间的映射,并且 hibernate 在加载、保存、基于 HSQL 构建 SQL 等时使用这种映射。这就是声明式方法的工作原理。对于 Spring (在我的示例中) ,声明有更多的行,并且具有与对应代码相同的表达能力。如果有一个例子,当这样的声明是短于代码-我想看到它。

控制反转原则允许简单的单元测试,因为你可以用假的实现替换真的实现(比如用内存中的数据库替换 SQL 数据库)

我确实理解控制反转的好处(我更愿意把这里讨论的设计模式称为依赖注入,因为 IoC 更通用——有很多种控制,而我们只颠倒了其中的一种——初始化控制)。我在问为什么有人需要编程语言以外的东西来做这件事。我完全可以用代码来替换真实的实现。这段代码将表达与配置相同的内容——它只是用假值初始化字段。

mary = new FakeFemale();

我明白 DI 的好处。我不明白与配置执行相同操作的代码相比,外部 XML 配置有什么好处。我不认为应该避免编译——我每天都在编译,而且我还活着。我认为 DI 的配置是声明性方法的坏例子。如果以不同的方式多次使用 AND 声明,则声明可能非常有用——比如 hibernate cfg,其中 bean 属性和 DB 列之间的映射用于保存、加载、构建搜索查询等。SpringDI 配置可以很容易地转换为配置代码,就像在这个问题的开头,难道不是吗?它只用于 bean 初始化,不是吗?这意味着声明式方法在这里没有添加任何内容,对吗?

当我声明 hibernate 映射时,我只是给 hibernate 一些信息,并且它基于这些信息工作——我不告诉它要做什么。对于春天,我的宣言告诉春天该做什么——那么为什么要宣布,为什么不直接去做呢?


最后更新:
伙计们很多答案都在告诉我依赖注入我知道这很好。 问题在于 DI 配置的目的而不是初始化代码——我倾向于认为初始化代码更短更清晰。 到目前为止,我得到的唯一答案是,当配置发生变化时,它避免了重新编译。我想我应该发布另一个问题,因为它是我的一个大秘密,为什么编译应该避免在这种情况下。

31819 次浏览

In the .NET world, most of IoC frameworks provide both XML and Code configuration.

StructureMap and Ninject, for example, use fluent interfaces to configure containers. You are no longer constrained to use XML configuration files. Spring, which also exists in .NET, heavily relies on XML files since it is his historical main configuration interface, but it is still possible to configure containers programmatically.

You don't need to recompile your code each time you change something in configuration. It will simplify program deployment and maintenance. For example you can swap one component with another with just 1 change in config file.

This is a bit of a loaded question, but I tend to agree that huge amounts of xml configuration doesn't really amount to much benefit. I like my applications to be as light on dependencies as possible, including the hefty frameworks.

They simplify the code a lot of the times, but they also have an overhead in complexity that makes tracking down problems rather difficult (I have seen such problems first hand, and straight Java I would be a lot more comfortable dealing with).

I guess it depends on style a bit, and what you are comfortable with... do you like to fly your own solution and have the benefit of knowing it inside out, or bank on existing solutions that may prove difficult when the configuration isn't just right? It's all a tradeoff.

However, XML configuration is a bit of a pet peeve of mine... I try to avoid it at all costs.

Spring also has a properties loader. We use this method to set variables that are dependant on the environment (e.g. development, testing, acceptance, production, ...). This could be for example the queue to listen to.

If there is no reason why the property would change, there is also no reason to configure it in this way.

Your case is very simple and therefore doesn't need an IoC (Inversion of Control) container like Spring. On the other hand, when you "program to interfaces, not implementations" (which is a good practice in OOP), you can have code like this:

IService myService;
// ...
public void doSomething() {
myService.fetchData();
}

(note that the type of myService is IService -- an interface, not a concrete implementation). Now it can be handy to let your IoC container automatically provide the correct concrete instance of IService during initialization - when you have many interfaces and many implementations, it can be cumbersome to do that by hand. Main benefits of an IoC container (dependency injection framework) are:

  • External configuration of mapping between interfaces and their concrete implementations
  • IoC container handles some tricky issues like resolving complicated dependency graphs, managing component's lifetime etc.
  • You save coding time because you provide mappings declaratively, not in a procedural code
  • Inversion of Control principle allows for easy unit testing because you can replace real implementations with fake ones (like replacing SQL database with an in-memory one)

You can slot in a new implementation for girlfriend. So new female can be injected without recompiling your code.

<bean id="jane" class="foo.bar.HotFemale">
<property name="age" value="19"/>
</bean>
<bean id="mary" class="foo.bar.Female">
<property name="age" value="23"/>
</bean>
<bean id="john" class="foo.bar.Male">
<property name="girlfriend" ref="jane"/>
</bean>

(The above assumes Female and HotFemale implement the same GirlfFriend interface)

Dependency injection is a coding style that has its roots in the observation that object delegation is usually a more useful design pattern than object inheritance (i.e., the object has-a relationship is more useful than the object is-a relationship). One other ingredient is necessary however for DI to work, that of creating object interfaces. Combining these two powerful design patterns software engineers quickly realized that they could create flexible loosely coupled code and thus the concept of Dependency Injection was born. However it wasn't until object reflection became available in certain high level languages that DI really took off. The reflection component is core to most of today's DI systems today because the really cool aspects of DI require the ability to programmatically select objects and configure and inject them into other objects using a system external and independent to the objects themselves.

A language must provide good support for both normal Object Oriented programming techniques as well as support for object interfaces and object reflection (for example Java and C#). While you can build programs using DI patterns in C++ systems its lack of reflection support within the language proper prevents it from supporting application servers and other DI platforms and hence limits the expressiveness of the DI patterns.

Strengths of a system built using DI patterns:

  1. DI code is much easier to reuse as the 'depended' functionality is extrapolated into well defined interfaces, allowing separate objects whose configuration is handled by a suitable application platform to be plugged into other objects at will.
  2. DI code is much easier to test. The functionality expressed by the object can be tested in a black box by building 'mock' objects implementing the interfaces expected by your application logic.
  3. DI code is more flexible. It is innately loosely coupled code -- to an extreme. This allows the programmer to pick and choose how objects are connected based exclusively on their required interfaces on one end and their expressed interfaces on the other.
  4. External (Xml) configuration of DI objects means that others can customize your code in unforeseen directions.
  5. External configuration is also a separation of concern pattern in that all problems of object initialization and object interdependency management can be handled by the application server.
  6. Note that external configuration is not required to use the DI pattern, for simple interconnections a small builder object is often adequate. There is a tradeoff in flexibility between the two. A builder object is not as flexible an option as an externally visible configuration file. The developer of the DI system must weigh the advantages of flexibility over convenience, taking care that small scale, fine grain control over object construction as expressed in a configuration file may increase confusion and maintenance costs down the line.

Definitely DI code seems more cumbersome, the disadvantages of having all of those XML files that configure objects to be injected into other objects appears difficult. This is, however, the point of DI systems. Your ability to mix and match code objects as a series of configuration settings allows you to build complex systems using 3rd party code with minimal coding on your part.

The example provided in the question merely touches on the surface of the expressive power that a properly factored DI object library can provide. With some practice and a lot of self discipline most DI practitioners find that they can build systems that have 100% test coverage of application code. This one point alone is extraordinary. This is not 100% test coverage of a small application of a few hundred lines of code, but 100% test coverage of applications comprising hundreds of thousands of lines of code. I am at a loss of being able to describe any other design pattern that provides this level of testability.

You are correct in that an application of a mere 10s of lines of code is easier to understand than several objects plus a series of XML configuration files. However as with most powerful design patterns, the gains are found as you continue to add new features to the system.

In short, large scale DI based applications are both easier to debug and easier to understand. While the Xml configuration is not 'compile time checked' all application services that this author is aware of will provide the developer with error messages if they attempt to inject an object having an incompatible interface into another object. And most provide a 'check' feature that covers all known objects configurations. This is easily and quickly done by checking that the to-be-injected object A implements the interface required by object B for all configured object injections.

One of the most appealing reasons is the "Hollywood principle": don't call us, we'll call you. A component is not required to do the lookups to other components and services itself; instead they are provided to it automatically. In Java, this means that it is no longer necessary to do JNDI lookups inside the component.

It is also lots easier to unit test a component in isolation: instead of giving it an actual implementation of the components it needs, you simply use (possibly auto generated) mocks.

Ease of combining partial configurations into a final complete configuration.

For example, in web applications, the model, view and controllers are typically specified in separate configuration files. Use the declarative approach, you can load, for example:

UI-context.xml
Model-context.xml
Controller-context.xml

Or load with a different UI and a few extra controllers:

AlternateUI-context.xml
Model-context.xml
Controller-context.xml
ControllerAdditions-context.xml

To do the same in code requires an infrastructure for combining partial configurations. Not impossible to do in code, but certainly easier to do using an IoC framework.

The reason for using a DI container are that you don't have to have a billion properties pre-configured in your code that are simply getters and setters. Do you really want to hardcode all those with new X()? Sure, you can have a default, but the DI container allows the creation of singletons which is extremely easy and allows you to focus on the details of the code, not the miscellaneous task of initializing it.

For example, Spring allows you to implement the InitializingBean interface and add an afterPropertiesSet method (you may also specify an "init-method" to avoid coupling your code to Spring). These methods will allow you to ensure that any interface specified as a field in your class instance is configured correctly upon startup, and then you no longer have to null-check your getters and setters (assuming you do allow your singletons to remain thread-safe).

Furthermore, it is much easier to do complex initializations with a DI container instead of doing them yourself. For instance, I assist with using XFire (not CeltiXFire, we only use Java 1.4). The app used Spring, but it unfortunately used XFire's services.xml configuration mechanism. When a Collection of elements needed to declare that it had ZERO or more instances instead of ONE or more instances, I had to override some of the provided XFire code for this particular service.

There are certain XFire defaults defined in its Spring beans schema. So, if we were using Spring to configure the services, the beans could have been used. Instead, what happened was that I had to supply an instance of a specific class in the services.xml file instead of using the beans. To do this, I needed to provide the constructor and set up the references declared in the XFire configuration. The real change that I needed to make required that I overload a single class.

But, thanks to the services.xml file, I had to create four new classes, setting their defaults according to their defaults in the Spring configuration files in their constructors. If we had been able to use the Spring configuration, I could have just stated:

<bean id="base" parent="RootXFireBean">
<property name="secondProperty" ref="secondBean" />
</bean>


<bean id="secondBean" parent="secondaryXFireBean">
<property name="firstProperty" ref="thirdBean" />
</bean>


<bean id="thirdBean" parent="thirdXFireBean">
<property name="secondProperty" ref="myNewBean" />
</bean>


<bean id="myNewBean" class="WowItsActuallyTheCodeThatChanged" />

Instead, it looked more like this:

public class TheFirstPointlessClass extends SomeXFireClass {
public TheFirstPointlessClass() {
setFirstProperty(new TheSecondPointlessClass());
setSecondProperty(new TheThingThatWasHereBefore());
}
}


public class TheSecondPointlessClass extends YetAnotherXFireClass {
public TheSecondPointlessClass() {
setFirstProperty(TheThirdPointlessClass());
}
}


public class TheThirdPointlessClass extends GeeAnotherXFireClass {
public TheThirdPointlessClass() {
setFirstProperty(new AnotherThingThatWasHereBefore());
setSecondProperty(new WowItsActuallyTheCodeThatChanged());
}
}


public class WowItsActuallyTheCodeThatChanged extends TheXFireClassIActuallyCareAbout {
public WowItsActuallyTheCodeThatChanged() {
}


public overrideTheMethod(Object[] arguments) {
//Do overridden stuff
}
}

So the net result is that four additional, mostly pointless Java classes had to be added to the codebase to achieve the affect that one additional class and some simple dependency container information achieved. This isn't the "exception that proves the rule", this IS the rule...handling quirks in code is much cleaner when the properties are already provided in a DI container and you're simply changing them to suit a special situation, which happens more often than not.

For myself one of the main reasons to use an IoC (and make use of external configuration) is around the two areas of:

  • Testing
  • Production maintenance

Testing

If you split your testing into 3 scenarios (which is fairly normal in large scale development):

  1. Unit testing
  2. Integration testing
  3. Black box testing

What you will want to do is for the last two test scenarios (Integration & Black box), is not recompile any part of the application.

If any of your test scenarios require you to change the configuration (ie: use another component to mimic a banking integration, or do a performance load), this can be easily handled (this does come under the benefits of configuring the DI side of an IoC though.

Additionally if your app is used either at multiple sites (with different server and component configuration) or has a changing configuration on the live environment you can use the later stages of testing to verify that the app will handle those changes.

Production

As a developer you don't (and should not) have control of the production environment (in particular when your app is being distributed to multiple customers or seperate sites), this to me is the real benefit of using both an IoC and external configuration, as it is up to the infrastructure/production support to tweak and adjust the live environment without having to go back to developers and through test (higher cost when all they want to do is move a component).

Summary

The main benefits that external configuration of an IoC come from giving others (non-developers) the power to configure your application, in my experience this is only useful under a limited set of circumstances:

  • Application is distributed to multiple sites/clients where environments will differ.
  • Limited development control/input over the production environment and setup.
  • Testing scenarios.

In practice I've found that even when developing something that you do have control over the environment it will be run on, over time it is better to give someone else the capabilities to change the configuration:

  • When developing you don't know when it will change (the app is so useful your company sells it to someone else).
  • I don't want to be stuck with changing the code every time a slight change is requested that could have been handled by setting up and using a good configuration model.

Note: Application refers to the complete solution (not just the executable), so all files required for the application to run.

Any time you can change your code to data you're making a step in the right direction.

Coding anything as data means that your code itself is more general and reusable. It also means that your data may be specified in a language that fits it exactly.

Also, an XML file can be read into a GUI or some other tool and easily manipulated pragmatically. How would you do that with the code example?

I'm constantly factoring things most people would implement as code into data, it makes what code is left MUCH cleaner. I find it inconceivable that people will create a menu in code rather than as data--it should be obvious that doing it in code is just plain wrong because of the boilerplate.

Often, the important point is who is changing the configuration after the program was written. With configuration in code you implicitly assume that person changing it has the same skills and access to source code etc as the original author had.

In production systems it is very practical to extract some subset of settings (e.g. age in you example) to XML file and allow e.g. system administrator or support personal to change the value without giving them the full power over source code or other settings - or just to isolate them from complexities.

Initializing in an XML config file will simplify your debugging / adapting work with a client who has your app deployed on their computers. (Because it doesn't require recompilation + binary files replacement)

I have your answer

There are obviously trade offs in each approach, but externalized XML configuration files are useful for enterprise development in which build systems are used to compile the code and not your IDE. Using the build system, you may want to inject certain values into your code - for example the version of the build (which could be painful to have to update manually each time you compile). The pain is greater when your build system pulls code off of some version control system. Modifying simple values at compile time would require you to change a file, commit it, compile, and then revert each time for each change. These aren't changes that you want to commit into your version control.

Other useful use cases regarding the build system and external configs:

  • injecting styles/stylesheets for a single code base for different builds
  • injecting different sets of dynamic content (or references to them) for your single code base
  • injecting localization context for different builds/clients
  • changing a webservice URI to a backup server (when the main one goes down)

Update: All the above examples were on things that didn't necessarily require dependencies on classes. But you can easily build up cases where both a complex object and automation is necessary - for example:

  • Imagine you had a system in which it monitored the traffic of your website. Depending on the # of concurrent users, it turns on/off a logging mechanism. Perhaps while the mechanism is off, a stub object is put in its place.
  • Imagine you had a web conferencing system in which depending on the # of users, you want to switch out the ability to do P2P depending on # of participants

From a Spring perspecitve I can give you two answers.

First the XML configuration isn't the only way to define the configuration. Most things can be configured using annotations and the things that must be done with XML are configuration for code that you aren't writing anyways, like a connection pool that you are using from a library. Spring 3 includes a method for defining the DI configuration using Java similar to the hand rolled DI configuration in your example. So using Spring does not mean that you have to use an XML based configuration file.

Secondly Spring is a lot more than just a DI framework. It has lots of other features including transaction management and AOP. The Spring XML configuration mixes all these concepts together. Often in the same configuration file I'm specifying bean dependencies, transaction settings and adding session scoped beans that actually handled using AOP in the background. I find the XML configuration provides a better place to manage all these features. I also feel that the annotation based configuration and XML configuration scale up better than doing Java based configuration.

But I do see your point and there isn't anything wrong with defining the dependency injection configuration in Java. I normally do that myself in unit tests and when I'm working on a project small enough that I haven't added a DI framework. I don't normally specify configuration in Java because to me that's the kind plumbing code that I'm trying to get away from writing when I chose to use Spring. That's a preference though, it doesn't mean that XML configuration is superior to Java based configuration.