简体   繁体   中英

Understanding the need for a DI framework

This might be a naive question. I'm currently learning the Spring framework and dependency injection . While the basic principle of DI is rather easy to grasp, it's not immediately obvious why you need an elaborate framework to implement it.

Consider the following:

public abstract class Saw
{
    public abstract void cut(String wood);
}

public class HandSaw extends Saw
{
    public void cut(String wood)
    {
        // chop it up
    }
}

public class ChainSaw extends Saw
{
    public void cut(String wood)
    {
        // chop it a lot faster
    }
}

public class SawMill
{
    private Saw saw;

    public void setSaw(Saw saw)
    {
        this.saw = saw;
    }

    public void run(String wood)
    {
        saw.cut("some wood");
    }
}

Then you could simply do:

Saw saw = new HandSaw();
SawMill sawMill = new SawMill();
sawMill.setSaw(saw);
sawMill.run();

Which would be equivalent to:

<bean id="saw" class="HandSaw"/>

<bean id="sawMill" class="SawMill">
   <property name="saw" ref="saw"/>
</bean>

plus:

ApplicationContext context = new ClassPathXmlApplicationContext("sawmill.xml");
SawMill springSawMill = (SawMill)context.getBean("sawMill");
springSawMill.run();

Granted, this is a contrieved example, and with more complex object relationships it might be more efficient to stash up an XML file than writing it programmatically, but surely there must be more to it than that?

(I know the Spring framework is more than that, but I'm thinking of the need for a DI container.)

In the first example it would also be trivial to change dependencies midstream:

// gotta chop it faster
saw = new ChainSaw();
sawMill.setSaw(saw);
sawMill.run();

I've had the exact same question, and it was answered by this:
Granted, you could do what you've described in "Then you could simply do:..." (let's call that "class A"). However, that would couple class A to HandSaw, or to all dependencies needed from class SawMill. Why should A be coupled to HandSaw - or, if you take a more realistic scenario, why should my business logic be coupled to the JDBC connection implementation needed for the DAO layer?
The solution I proposed then was "then move the dependencies one step further" - ok, so now I've got my view coupled to JDBC connection, where I should only deal with HTML (or Swing, pick your flavor).

The DI framework, configured by an XML (or JavaConfig) solves this by letting you just "get the needed service". You don't care how it's initialized, what it needs to work - you just get the service object and activate it.

Also, you have a misconception there regarding the "plus:" (where you do SawMill springSawMill = (SawMill)context.getBean("sawMill"); springSawMill.run(); ) - you don't need to get the sawMill bean from the context - the sawMill bean should've been injected into your object (class A) by the DI framework. so instead of ...getBean(...), you'd just go "sawMill.run()", not caring where it came from, who initialized it and how. For all you care, it could go straight to /dev/null, or test output, or real CnC engine... The point is - you don't care. All you care is your tiny little class A which should do what it's contracted to do - activate a saw mill.

Dependency injection is a degenerate form of implicit parameter passing , and the purpose is essentially the same, to solve what's called The Configurations Problem :

The configurations problem is to propagate run-time preferences throughout a program, allowing multiple concurrent configuration sets to coexist safely under statically guaranteed separation.

Dependency Injection frameworks compensate for the lack of implicit parameters , Curried functions , and convenient facilities for monads in the language.

Spring has three features that are equally important:

  1. dependency injection
  2. aspect oriented programming
  3. library of framework classes to help with persistence, remoting, web mvc, etc.

I'll agree that it's hard to see an advantage to dependency injection when you compare that to a single call to new. In that case, the latter will certainly look simpler, because it's a single line of code. Spring's configuration will always increase the lines of code, so it's not a winning argument.

It starts looking a lot better when you can take a cross-cutting concern like transactions out of your classes and use aspects to set them up in a declarative way. The comparison to a single "new" call isn't what Spring was created for.

Perhaps the best outcome from using Spring is the way its recommended idiom uses interfaces, layering, and good principles like DRY. It's really just the distillation of object-oriented best practices that Rod Johnson used in his consulting gigs. He found that the code he built up over time helped him make a buck delivering better software for his clients. He summarized his experience in "Expert 1:1 J2EE" and ended up open sourcing the code as Spring.

I'd say buy into the framework to the degree that you think his experience can help you write better code, too.

I don't think you can get the full value of Spring until you combine all three of those features.

Granted, this is a contrieved example, and with more complex object relationships it might be more efficient to stash up an XML file than writing it programmatically, but surely there must be more to it than that?

I think it makes more sense to put the "wiring up" in a configuration file rather than doing it manually in code for several reasons:

  1. The configuration is external to your code.
  2. Changes to the wiring up (to tell your sawmill to use a different instance of Saw ) can simply be made to the external (XML) file and do not require changing code, re-compiling, re-deploying, etc.
  3. When you have dozens of classes, and several layers of injection (for example: you have a web Controller class which gets a Service class which contains your business logic, which uses a DAO to obtain Saw s from the database, which gets a DataSource injected into it, etc.), manually wiring up the collaborators is tedious and requires a few dozen lines of code that do nothing but wiring up.
  4. This is somewhat less of a clear-cut "benefit", but by having all of the 'wiring up' external to the code, I think it helps reenforce to developers the ideas that are at the core of dependency injection, specifically the idea of coding to the interface, not the implementation. With manual wiring up, it can be easy to slip back into old ways.

I generally don't care about XML or Reflection based DI because, in my use cases, it adds unnecessary complexity. Instead, I usually go for some form of manual DI that, to me, seems more natural and has most of the benefits.

public class SawDI{
    public Saw CreateSaw(){
        return new HandSaw();
    }

    public SawMill CreateSawMill(){
        SawMill mill = new SawMill();
        mill.SetSaw(CreateSaw());
        return mill;
    }
}

// later on

SawDI di = new SawDI();
SawMill mill = di.CreateSawMill();

This means I still centralize the coupling and have all the advantages of that, without the dependency on more complex DI framework or the XML configuration files.

Don't forget one major disadvantage of dependency injection: you loose the ability to easily find out from where something is initialized using Find Usages of your powerful Java IDE. This might be a very serious point if you refactor a lot and want to avoid that the test code is 10 times bigger than the application code.

大多数(如果不是全部)DI容器/库为您带来的一件事是拦截通过DI创建的所有实例的方法的可能性。

If you hard-code the inserted class, you need that class be available at compile-time. With a configuration-file you could change the used saw (in your case) at runtime without recompiling and even use a saw taken from a new jar you just placed in the classpath. If it is worth the extra complexity is dependent on the task you have to solve.

Lately there has been very much emphasis on DI frameworks , even so that the DI pattern is being forgotten. DI's principles as summarized by JB Rainsberger :

It's simple: make dependencies explicit by requiring collaborators as parameters in the constructor. Repeat until you've pushed all decisions about which objects to create into the entry point. Of course, this only applies to Services (in the DDD sense). Done.

As you've noticed, there is not much difference between configuring the dependencies manually vs. using a framework. With constructor injection, as shown below, your code example would have even less boilerplate and the compiler would force you to provide all required dependencies (and your IDE will probably type them for you).

SawMill sawMill = new SawMill(new HandSaw());
sawMill.run();

A DI framework can reduce the boilerplate of creating factories and wiring the objects together, but at the same time it can also make it harder to find out that where each dependency is coming from - the DI framework's configuration is one more layer of abstraction to dig through, and your IDE might not be able to tell you where a particular constructor is being called from.

An indirect disadvantage of DI frameworks is that they can make wiring the dependencies too easy . When you can't anymore feel pain from having lots of dependencies, you may just keep on adding more dependencies instead of rethinking the design of the application to reduce the coupling between the classes. Wiring the dependencies manually - especially in test code - makes it easier to notice when you have too many dependencies as the test setup becomes longer and it becomes harder to write unit tests.

Some advantages of DI frameworks come from their support of advanced features such as AOP (for example Spring's @Transactional), scoping (though many times scoping with plain code will be enough) and pluggability (if a plugin framework is really needed).

Recently I made an experiment of manual DI vs. framework-based DI. The progress and results are shown as screencasts in Let's Code Dimdwarf episodes 42 through 47 . The project in question had a Guice -based plugin system for creating actors , which I then rewrote using manual DI, without Guice. The result was a much simpler and clearer implementation, and only a little bit more boilerplate.

Synopsis: First try using just manual DI (preferably constructor injection). If there becomes lots of boilerplate, try to rethink the design to reduce the dependencies. Use a DI framework if some of its features provide value for you.

It's important to understand that Spring is fundamentally two things, one built on top of the other:

  1. A lightweight DI/IoC framework and the classes to implement that (eg XML application contexts, etc); and
  2. It is a lightweight container .

(2) is the bulk of the Spring code. Basically pick a Java technology and you'll probably find Spring has helper classes for it. This is so you can use ActiveMQ, Sun One MQ or whatever and abstract them being a Spring JmsTemplate and the same goes for data access technologies, Web services, etc.

All of these helpers use (1) to wire them together.

One of the biggest benefits of using Dependency Injection is that it makes it much easier to provide mocks or stubs of a class's dependencies when creating a unit test for that class. This allows you to test the class in isolation without depending on its collaborators.

In your example, there is no way to mock or stub out Saw or SawMill in the class that where those are being instantiated. If Saw and SawMill were set via setters or the constructor, then you could pass in your own mock Saw and mock SawMill when running the unit test.

Spring helps you understand and even encourages DI models. However, I don't believe you have to have Spring.

You can have configuration files in Java which you can debug, refactor and perform code analysis on.

I suggest you put these java configuration files in another package, but they are otherwise equivalent to XML configuration files. You can even load these file dynamically if that's important.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM