34

I work in a project that uses (Spring) Dependency Injection for literally everything that is a dependency of a class. We are at a point where the Spring configuration file has grown to about 4000 lines. Not long ago I watched one of Uncle Bob’s talks on YouTube (unfortunately, I could not find the link) in which he recommends injecting only a couple of central dependencies (e.g. factories, database, …) into the Main component from which they will then be distributed.

The advantages of this approach is the decoupling of the DI framework from the most part of the application and also it makes the Spring config cleaner as the factories will contain a lot more of what was in the config before. On the contrary, this will result in spreading creation logic among many factory classes and testing might become more difficult.

So my question really is what other advantages or disadvantages you see in the one or the other approach. Are there any best practices? Thanks a lot for your answers!

  • 3
    Having a 4000 line configuration file isn't the fault of dependency injection... there's plenty of ways to fix it: modularise the file into multiple smaller files, switch to annotation based injection, use JavaConfig files instead of xml. Basically, the problem you are experiencing is a failure to manage the complexity and size of your application's dependency injection needs. – Maybe_Factor Sep 4 '17 at 23:35
  • @Maybe_Factor TL;DR split the project into smaller components. – Walfrat Sep 5 '17 at 7:11
40

As always, It Depends™. The answer depends on the problem one is trying to solve. In this answer, I'll try to address some common motivating forces:

Favour smaller code bases

If you have 4,000 lines of Spring configuration code, I suppose that the code base has thousands of classes.

It's hardly an issue that you can address after the fact, but as a rule of thumb, I tend to prefer smaller applications, with smaller code bases. If you're into Domain-Driven Design, you could, for instance, make a code base per bounded context.

I'm basing this advice on my limited experience, since I've written web-based line-of-business code for most of my career. I could imagine that if you're developing a desktop application, or an embedded system, or other, that things are harder to pull apart.

While I do realise that this first advice is easily the least practical, I also believe that it's the most important, and that's why I include it. Complexity of code varies non-linearly (possibly exponentially) with the size of the code base.

Favour Pure DI

While I still realise that this question presents an existing situation, I recommend Pure DI. Don't use a DI Container, but if you do, at least use it to implement convention-based composition.

I don't have any practical experience with Spring, but I'm assuming that by configuration file, an XML file is implied.

Configuring dependencies using XML is the worst of both worlds. First, you lose compile-time type safety, but you don't gain anything. An XML configuration file can easily be as big as the code it tries to replace.

Compared to the problem it purports to address, dependency injection configuration files occupy the wrong place on the configuration complexity clock.

The case for coarse-grained dependency injection

I can make a case for coarse-grained dependency injection. I can also make a case for fine-grained dependency injection (see next section).

If you only inject a few 'central' dependencies, then most classes might look like this:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

This is still fits Design Patterns's favor object composition over class inheritance, because Foo composes Bar. From a maintainability perspective, this could still be considered maintainable, because if you need to change the composition, you simply edit the source code for Foo.

This is hardly less maintainable than dependency injection. In fact, I'd say that it's easier to directly edit the class that uses Bar, instead of having to follow the indirection inherent with dependency injection.

In the first edition of my book on Dependency Injection, I make the distinction between volatile and stable dependencies.

Volatile dependencies are those dependencies that you should consider injecting. They include

  • Dependencies that must be re-configurable after compilation
  • Dependencies developed in parallel by another team
  • Dependencies with non-deterministic behaviour, or behaviour with side-effects

Stable dependencies, on the other hand, are dependencies that behave in well defined manner. In a sense, you could argue that this distinction makes the case for coarse-grained dependency injection, although I must admit that I didn't entirely realise that when I wrote the book.

From a testing perspective, however, this makes unit testing harder. You can no longer unit test Foo independent of Bar. As J.B. Rainsberger explains, integration tests suffer from a combinatoric explosion of complexity. You'll literally have to write tens of thousands of test cases if you want to cover all paths through an integration of even 4-5 classes.

The counter-argument to that is that often, your task isn't to program a class. Your task is to develop a system that solves some specific problems. This is the motivation behind Behaviour-Driven Development (BDD).

Another view on this is presented by DHH, who claims that TDD leads to test-induced design damage. He also favours coarse-grained integration testing.

If you take this perspective on software development, then coarse-grained dependency injection makes sense.

The case for fine-grained dependency injection

Fine-grained dependency injection, on the other hand, could be described as inject all the things!

My main concern regarding coarse-grained dependency injection is the criticism expressed by J.B. Rainsberger. You can't cover all code paths with integration tests, because you need to write literally thousands, or tens of thousands, of test cases to cover all code paths.

The proponents of BDD will counter with the argument that you don't need to cover all code paths with tests. You only need to cover those that produce business value.

In my experience, however, all the 'exotic' code paths will also execute in a high-volume deployment, and if not tested, many of those will have defects and cause run-time exceptions (often null-reference exceptions).

This has caused me to favour fine-grained dependency injection, because it enables me to test the invariants of all objects in isolation.

Favour functional programming

While I lean towards fine-grained dependency injection, I've shifted my emphasis towards functional programming, among other reasons because it's intrinsically testable.

The more you move towards SOLID code, the more functional it becomes. Sooner or later, you may as well take the plunge. Functional architecture is Ports and Adapters architecture, and dependency injection is also an attempt and Ports and Adapters. The difference, however, is that a language like Haskell enforces that architecture via its type system.

Favour statically typed functional programming

At this point, I've essentially given up on object-oriented programming (OOP), although many of the problems of OOP are intrinsically coupled to mainstream languages like Java and C# more than the concept itself.

The problem with mainstream OOP languages is that it's close to impossible to avoid the combinatoric explosion problem, which, untested, leads to run-time exceptions. Statically typed languages like Haskell and F#, on the other hand, enable you to encode many decision points in the type system. This means that instead of having to write thousands of tests, the compiler will simply tell you whether you've dealt with all possible code paths (to an extent; it's no silver bullet).

Also, dependency injection isn't functional. True functional programming must reject the entire notion of dependencies. The result is simpler code.

Summary

If forced to work with C#, I prefer fine-grained dependency injection because it enables me to cover the entire code base with a manageable number of test cases.

In the end, my motivation is rapid feedback. Still, unit testing isn't the only way to get feedback.

  • I really like this answer, and especially this statement used in the context of Spring: "Configuring dependencies using XML is the worst of both worlds. First, you lose compile-time type safety, but you don't gain anything. An XML configuration file can easily be as big as the code it tries to replace." – Thomas Carlisle Sep 7 '17 at 13:17
1

Huge DI setup classes are a problem. But think of the code it replaces.

You need to instantiate all those services and whatnot. The code to do so would either be in the app.main() starting point and manually injected, or tightly coupled as this.myService = new MyService(); within the classes.

Reduce the size of your setup class by splitting it out into multiple setup classes and call them from the program start point. ie.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

You don't need to reference service1 or 2 except to pass them into other ci setup methods.

This is better than attempting to get them from the container as it enforces the order of calling the setups.

  • 1
    For those who want to look into this concept further (building your class tree at the program start point), the best term to search for is "composition root" – e_i_pi Sep 4 '17 at 23:00
  • @Ewan what's that ci variable? – superjos Sep 6 '17 at 0:19
  • your ci setup class with static methods – Ewan Sep 6 '17 at 0:20
  • urg should be di. i keep thinking 'container' – Ewan Sep 6 '17 at 0:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.