Skip to main content

Dependency Injection: A General Principle For All Software Developers

Introduction

Dependency injection, often shortened to the acronym DI, is a fundamental principle of software design. Every software developer makes decisions related dependency injection whether they are aware of it or not, and most, if not all developers, have had to deal with the consequences of poor DI-related decisions in an existing code base. Understanding it and deciding when and how to use it is crucial to building good software.

In this article, we define DI, discuss its purpose and benefits, and when and when not to use it. By learning these principles, we can write higher-quality, more maintainable code and even ship code faster. This article uses the Java programming language for examples. The general principles of DI, however, apply to any general-purpose programming language.

Definition of DI

Dependency injection is the act of providing dependencies to code. The opposite of dependency injection is to have code explicitly  access its dependencies via global variables. Let's break this definition down into its component pieces:
  1. Code. For the purposes of our discussion, the smallest unit of code is a Java method definition. 
  2. Code Dependency. Code depends on data and other code. In Java, we mean that our Java methods depend on other methods and objects to do its job.
  3. Injection: dependencies are injected into a method by passing them in as parameters.
  4. Explicit reference. Code does the opposite of DI by explicitly referencing its dependencies as global variables.
In Java, global variables include, but are not limited to:
  1. Static methods
  2. Static fields
  3. Constructors
  4. final instance methods
  5. Class, interface, and method declarations and definitions

Simple Code Dependency Example

We could spend an entire blog post talking about code dependencies, but we can start with one very simple example:

class Example {
   public static int doubleIt(IntSupplier supplier) {
        return supplier.getAsInt() * 2;
   }
}
We've written here a fairly useless method that takes in a unary int function, calls it, and doubles its result. However, even in this trivial example, our method has a global dependency it explicitly references, which is the IntSupplier class definition and its getAsInt method declaration in particular.

This dependency represent the 4th kind of global state mentioned above. There is only one IntSupplier type and, consequently, only one IntSupplier::getAsInt() method declaration in the entire program, This kind of global  dependency arises naturally from using a  nominally- and statically-typed language like Java, and cannot be avoided without using dynamic typing or similar features such as reflection.

On the other hand, the implementation of IntSupplier and its getAsInt method that is being depended on via the supplier parameter is injected. It is entirely up to the caller what instance is passed in and what it will do. This dependency is said to be injected.

Java-Specific Details We Glossed Over

We've tried to keep our definition as simple as possible. For instance, we don't distinguish between objects and primitive values in Java because, for the purpose of talking about DI, the distinction is meaningless. We also don't distinguish between constructors and static methods, since conceptually they are the same thing.

Instance methods can depend on fields of the instance object. There are two general ways we inject dependencies retrieved as instance fields:
  1. Constructor arguments. We take the argument and assign it to an instance field before we call the method that uses the field.
  2. Field Mutation. We can pass in a dependency via a setter method, or, direct field access if the field is non-private.
Finally, in Java, we can't pass in a method dependency directly because methods aren't first class objects that can be passed around. We can, however, pass in function objects that represent methods. These are called functional interfaces in Java, such as java.util.functions.Function, or the IntSupplier we used previously. For our discussion, we won't really distinguish between methods and function objects.

Purpose of DI

Now that we've defined DI, let's briefly mention why it is important. The primary purpose of DI is to defer decision-making to the caller of code rather than have code make a decision itself. By deferring decisions we can make code more modular, flexible, and easier to reason about, as well as more testable. On the other hand, we can overdo it. if we inject too many dependencies code becomes less useful and more unreadable as it makes too few decisions on its own.

Note that, based on our definition of DI and its stated purpose, it is neither possible or desirable to use dependency injection all the time. At some point, some code needs to make some decisions by directly accessing its dependencies. Part of the art of software design is to decide which dependencies to inject and which to make explicit, or, in other words, which decisions code should make for itself, and which decisions it should leave to callers.

DI Examples

DI's benefits are often best illustrated through examples. What follows are examples modeled off of
code I've personally had to deal with. Each of these examples will highlight how the author arguably made the wrong choice with respect as to whether or not to inject a specific dependency. We will talk about the negative consequences of the decision, and then show how to fix the issue with a better code design.

Example 1: Docx Library

Imagine a library that can parse and generate .docx files, for instance, so users can upload a .docx file that you parse and analyze for some purpose. Since .docx files are XML-formatted, this library needs to use an XML parser. One way to do so is to simply create one using the built-in XML libraries provided in Java:

public class DocxParser {
    public static Document parse(InputStream input) {
        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
        ...
    }
}

A real-world problem with this decision is that the XML parser provided by Java has one or more security holes enabled by default when it comes to parsing untrusted files. The library makes no attempt to disable the features to make itself secure, so the library itself is also insecure. The only way to work around this is to figure out a way to make the XML parser returned by SAXParserFactory to be secure. This is possible, but requires a hack to make work, and requires modifying the global state of the program.

It would be better if the XML parser were an injectable parameter, rather than it being explicitly created through the use of static factory method, which is a global variable. This allows the caller to make all the decisions it needs to about the nature of the XML parser without having to modify global state. If desired, we can even keep the original parse method around as a convenience.

public class DocxParser {
    public static Document parse(InputStream input) {
        return parse(input, defaultSAXParser());
    }

   /* this method overload lets us inject the SAXParser dependency */
    public static Document parse(InputStream input, SAXParser parser) {
         ...
    }
    private static SAXParser defaultSAXParser() {
        ...
    }
}

Example 2: User DAO

Imagine a DAO object that provides an object-oriented interface for manipulating a database table. The table needs a database connection. This particular class obtains its database connection from a thread-local variable. The idea behind this is that the thread-local variable can be installed at the start of handling a web request, and then used by all DAO objects needed to fulfill the web request.

public class UserDao {
    public User getById(long id) {
        return ThreadLocalDbConnection.get().findById(this.tableName(), id);
    }
}


Thread-locals are a close cousin to global variables, and directly accessing them has most of the same downsides as directly accessing a regular global variable. First of all, using this class correctly is far more difficult than using one where we pass in the connection object directly. When instantiating this DAO, users have no idea, unless they read docs or the source code, that there is a hidden, implicit dependency on the thread-local For instance, if we somehow accidentally use this DAO before the thread-local is setup, or after it is torn down, or in some context where the thread-local isn't automatically set, we will get an NPE or other exception. When unit-testing this class, we will similarly get NPE errors unless we remember to install a mock on the thread-local.

In worse cases, we may need to do something write some code that talks to a user table but using a different connection. For instance, we may decide that we need to grab a lock using another connection avoid a deadlock. The only way to accomplish this is to spawn a separate thread so we can install a different thread-local, then have this thread and our main thread coordinate.

Unfortunately, all of these issues are real ones encountered in production codebases. Forcing the use of the thread-local is a shortcut with negative consequences that outweigh the benefits. Instead, the class should allow users to inject the dependency as a constructor argument.

public class UserDao {
    private final DbConnection connection;

    public UserDao(DbConnection connection) {
        this.connection = connection;
    }

    public User getById(long id) {
        return connection.findById(this.tableName(), id);
    }
}

Example 3: Configuration File

Imagine a class that loads some configuration from a classpath resource that drives the behavior of the class, like the choice algorithm it uses for some operation:

public class ConfiguredByFile {
    private static final Config CONFIG;

    static {
        ClassLoader cl = Thread.currentThread.getContextClassLoader();
        try (InputStream input = cl.getResourceAsStream("resource")) {
            CONFIG = parseConfig(input);
        } catch (IOException e) {
            throw new Error(e);
        }
    }

    public ReturnValue doSomething() {
        CONFIG.getAlgorithm()....;
    }
}

This class does not allow to inject the configuration dependency, making it unnecessarily inflexible. What if  we need to create two instances of this class that are configured differently? This is currently  impossible.

The class is also not friendly to unit-testing nor is it friendly in general to users of the class, who have to deal with the fact there is a hidden, implicit dependency on a classpath resource that may or may not exist. Or, worse, it may be bundled with the jar but the user needs to be modify it to meet his or her needs.

All of these issues can be fixed by making a Config instance field that the instance methods of the class rely on, and to allow to pass this object in as a constructor argument. If we want, we can still provide a convenience factory method that lazily loads the config from a global variable like a classpath resource.
public class ConfiguredByFile {
    private final Config config;

    public ConfiguredByFile(Config config) {
        this.config = config;
    }

    public ConfiguredByFile() {
        this(CONFIG.get());
    }

    public ReturnValue doSomething() {
        config.getAlgorithm()....;
    }

private static final Supplier<Config> = Suppliers.memoize(() => getDefaultConfig());

    private static Config loadDefaultConfig() {
        ClassLoader cl = Thread.currentThread.getContextClassLoader();
        try (InputStream input = cl.getResourceAsStream("resource")) {
            returnparseConfig(input);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

Example Summary

All three of our negative examples have something in common. They all take a shortcut by having code explicitly reference a global variable as a dependency rather than allowing the dependency to be injected. This results in unnecessarily rigid code that can be more difficult to use due to both its inflexibility and its implicit contract that certain global state be set up correctly prior to use of the class.

In all three cases, it only took slightly more effort to make the dependency injectable. We were even able to provide multiple ways of constructing a class so that we could either inject a dependency ourselves, or use a default global dependency for convenience. Providing multiple options can give us the both the convenience of relying on global state with the flexibility of being able to inject our dependencies.

DI Frameworks: What Are They For?

So far we've studied DI's definition and purpose, and we've gone through several examples of showing how applying DI to code can yield real and practical benefits. So far, we've focused on one aspect of DI, but there are actually two aspects to consider:
  1. When writing a unit of code, decide what should be injectable and expose those dependencies as parameters. This is what we've focused on so far.
  2. When using a given unit of code, we must pass in the needed parameters.
Of these two aspects, the first is the far more important one. The first is where actual software design occurs, and where our decisions affect whether our code will be appropriately modular, testable,  maintainable, and usable or not. The second aspect generally involves writing simple code to instantiate objects and pass them around. In particular, when writing our application startup code, we may have to write some boilerplate code to essentially instantiate a graph of top-level objects that our application needs. In DI terminology, this is sometimes referred to as "wiring" together an application. This boilerplate is very straightforward to write and maintain and is usually not a source of bugs, design flaws, or maintainability problems.

DI frameworks like Spring do not help users at all with the first, most important aspect of DI. Deciding what to make injectable and how to make it injectable, and then building your class and object APIs based on those decisions, has absolutely nothing to do with any DI framework I am aware of. Instead, DI frameworks can only help you with the second aspect by replacing some or all of an application's startup code with a domain-specific language defined by the framework. In essence, this is nothing more than a language for defining global variables and wiring them together into an object graph.

It is important to restate: most or all popular DI frameworks are nothing but simple domain-specific languages used to define and use global variables. This feature, defining and using global variables,  can only assist us in removing some application startup boilerplate. On the other hand, these frameworks bring with them many major disadvantages, as outlined in our previous article dedicated to the subject of DI frameworks, so much that they are more likely to slow you down than help you.

It is therefore important to distinguish the actual software design principle of dependency injection from DI frameworks, which at best can touch on only the most minor aspects of dependency injection.

Conclusion

Because it is so fundamental to writing code, dependency injection is something every software developers thinks about, whether they are aware of it or not. By understanding DI's fundamental meaning and purpose, we can actively use this knowledge to build better software.

In my experience, correct use of DI can help developers ship code faster with higher quality and better maintainability. For this reason, I believe it is important that every software developer study the topic. I also believe it is important that people learn the role of DI frameworks and how they can actually hurt us more than help us.

Lastly, it is important to remember that DI applies to all general-purpose programming languages, from GoLang to Javascript, and regardless as to whether the language has a popular DI framework readily available. So, whether you are a backend java developer, a frontend React developer, a Gopher, a Pythonista, or a Rubyist, I hope you find the information in this article practical and useful in your day-to-day work.

Comments

Popular posts from this blog

Ditching DI Frameworks To Ship Software Faster

Introduction Software development velocity is a major concern for many teams and tech companies. Software is expensive to write and maintain. Businesses find themselves under siege by competition and feel pressure to move more quickly. In this article, I am going to discuss how we can move faster as developers by removing unnecessary dependencies, such as runtime DI frameworks like Spring. When designing a professional race car, one builds around the engine entirely. The engine is the heaviest component of the vehicle, but also the most important, as it provides all the power to propel the rest of the vehicle forward. All other components are designed to support the engine but otherwise get out of the way. By analogy, a service implements and executes some key business functionality, which we can compare to our race car's engine. Any frameworks, libraries, and infrastructure we use must support the core business functions of the service, but hopefully otherwise get out of ...