[archives]

Dependency Injection, Inversion of Control & The Dependency Inversion Principle


Dependency Injection, Inversion of Control, and The Dependency Inversion Principle are poorly understood and often conflated. This confusion is the result of several factors. The first is that they all have similar names. Another factor is that these concepts often work in conjunction with each other (though they are not requirements of each other). Also, early writers of DI frameworks, appropriated the term Inversion of Control, which had previously existed in a different form. Finally, especially in the case of the Dependency Inversion Principle, the original explanation is not clear.

Inversion of Control (IoC)

Inversion of Control is an Architectural Pattern where the flow control of a system is provided by a generic library that calls into custom business logic. The “inversion” is that in more traditional approaches the custom business logic would call into generic libraries.

Thus the traditional example would look like:

import my.foo.traditional.Network

def main(args: String*) {
	//call into a provided library
	val connection = Network.getConnection(url, port)

	//flow control in the "custom" code
	while(true) {
		val pack = connection.getNextPacket()
		doSomeStuff(pack)
	}
}

def doSomeStuff(pack: Packet) {
	//do some stuff with pack
}

And the IoC version:

import my.foo.ioc.Network

def main(args: String*) {
	//notice no flow control in this code
	//that is the network library is calling "into" custom logic via a call back
	Network.startSever(url, port)(doSomeStuff)
}

def doSomeStuff(pack: Packet) {
	//do some stuff with pack
}

The IoC pattern is a good fit for problems where the generic library solves a problem that is constrained and well known with touch points between the custom logic that are obvious and well defined. The more customization required of the library, the less likely it is that IoC is the correct choice.

Dependency Injection (DI)

Dependency Injection is a Code Pattern that severs the creation of stateful dependencies from the use of those dependencies. This is most often used as a way to accomplish compositional polymorphism.

Before applying DI:

import my.foo.di.Network

class Foo(url: String, port: Int) {
	val connection = Network.getConnection(url, port)

	def doFoo() {
		// do something with connection 
	}
}

After applying DI

import my.foo.di.Network

class Foo(connection: IConnection) {
	def doFoo() {
		// do something with connection
	}
}

In the first example the Foo class has an explicit coupling with the instantiation of the connection. There is also an implicit coupling between what a connection is and the Foo class. That is, the constructor of the Foo class implies that it works with connections that are based on a url and port.

There is a second form of Dependency Injection that uses setters instead of constructor injection. Do not use this form.

It should be noted that there are other code patterns that accomplish the same goal of Dependency Injection. The service locator pattern, functional composition, and parametric injection (either via implicits or not) are all options for achieving compositional polymorphism and de-coupling from instantiation of dependencies.

The DI (or IoC) Container ie The Devil

As DI started becoming more popular, especially in large enterprise java environments, the complexity of wiring up large dependent networks of classes became a difficult task. To meet this complexity a new class of software framework was created that encapsulated the instantiation of these large DI heavy dependent class networks. For some horrible reason, these frameworks called themselves IoC containers. Martin Fowler tried to rename them DI containers, a more correct name, but this only served to further conflate DI and IoC, as well as to associate both terms with large highly coupled class networks. Worsening the situation further was that several of these DI containers imposed onerous constraints on the code that used it (most glaringly requiring setter style DI).

Given all that, DI containers as a class of software have a very bad name. This is a backwards way of thinking about the problem. The issue is that complex graphs of dependent classes are an architectual smell. This is not a result of using the DI or IoC patterns as both can be used well outside of these contexts. So the DI container does not cause the problem, it is a symptom of it. That said, it is a very strong indicator of poor architecture if a DI container is a useful component to the system.

The Dependency Inversion Principle

Unlike DI and IoC which are patterns, that is emergent archetypes of solutions to common problems, DIP is a principle. A principle is used to guide design and is useful for determining a more correct solution amongst many choices. Occasionally, while applying the DIP it makes sense to use the DI or IoC patterns, but they are not fundamentally linked.

As originally written by Robert Martin, the DIP is:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions.

This is a problematic definition as ALL software implementations are abstractions. Due to this confusion many interpretations of this principle simply move dependencies from concrete implementations to abstract types that mimic the same method signatures as the original. This may be helpful in the limited case of using test only substitution of dependencies but it very rarely accomplishes what this principle is intended to accomplish.

Typical dependency chains flow linearly from client –> high level library abstraction –> low level library abstraction. This typical flow tightly couples the implementation of the high level library with the low level one, greatly reducing its reusability and tying it’s life cycle to that of its low level dependency. That is, if the low level dependency is to change the high level one does as well. For example:


import my.foo.dip.HighLevelFoo
import my.foo.dip.LowLevelFooFile

def main(args: String*) {
	val foo = new HighLevelFoo(LowLevelFooFile.get())
	foo.organizeFoos("Foo1", "Foo2", "Foo3")
}

-----

import my.foo.dip.LowLevelFooFile

class HighLevelFoo(lowLevel: LowLevelFooFile) {

	private def getFile(name: String) = {
		if(lowLevel.fileExists(name)) {
			if(!lowLevel.fileHasWritePermissions(name)) {
				lowLevel.makeFileWriteable(name)
			}
			lowLevel.getFile(name)
		} else {
			lowLevel.makeFile(name, "w")
		}
	}

	def organizeFoos(args: String*) {
		val fooFile1 = getFile("file1")
		val fooFile2 = getFile("file2")
		args.foreach { arg =>
			//complicated high level foo busines logic that uses those files	
			//for instance
			//fooFile1.write(resultA)
			//fooFile2.write(resultB)
		}
		fooFile1.save()
		fooFile2.save()
	}
}

--------

trait LowLevelFooFile {
	def fileExists(name: String) : Boolean
	def fileHasWritePermissions(name: String): Boolean
	def getFile(name: String): FooFile
	def makeFileWriteable(name: String)
	def makeFile(name: String, permissions: String): FooFile	
}

This example shows the coupling between HighLevelFoo and LowLevelFooFile, even though it uses Dependency Injection and only depends on a trait. Assumedly, HighLevelFoo does not actually care about the details of how file creation and permissions operate. Further, by including it in the HighLevelFoo we insure that HighLevelFoo can only work with result outputs that have these semantics (for instance, if we wanted to output the results to stdout those semantics don’t make sense). The abstraction has leaked from the lower level library to the higher level one. This is true of any low level abstraction we create here. The only way to prevent this is to invert the dependency. That is HighLevelFoo needs to control the interface/abstraction it interacts with. For example:

import my.foo.dip.highlevel.HighLevelFoo
import my.foo.dip.highlevel.HighLeveFooSync
import my.foo.dip.lowleve.LowLevelFooFile

def main(args: String*) {
	val foo = new HighLevelFoo(new FooWithFile(LowLevelFooFile.get()))
	foo.organizeFoos("Foo1", "Foo2", "Foo3")
}


class FooWithFile(fooFile: LowLevelFooFile) extends HighLevelFooOutputs[FooFile] {
	def get(name: String): FooFile = {
		if(lowLevel.fileExists(name)) {
			if(!lowLevel.fileHasWritePermissions(name)) {
				lowLevel.makeFileWriteable(name)
			}
			lowLevel.getFile(name)
		} else {
			lowLevel.makeFile(name, "w")
		}
	}

	def out(file: FooFile, result: String) {
		file.write(result)
	}

	def close(file: FooFile) {
		file.save()
	}
}

-----

trait HighLevelFooOutputs[T] {
	def get(name: String): T
	def out(t: T, result: String)
	def close(t: T)
}

class HighLevelFoo[T](sync: HighLevelFooOutputs[T]) {

	def organizeFoos(args: String*) {
		val fooFile1 = sync.get("file1")
		val fooFile2 = sync.get("file2")
		args.foreach { arg =>
			//complicated high level foo busines logic that uses those files	
			//for instance
			//sync.write(fooFile1, resultA)
			//sync.write(fooFile2, resultB)
		}
		sync.close(fooFile1)
		sync.close(fooFile2)
	}
}

--------

trait LowLevelFooFile {
	def fileExists(name: String) : Boolean
	def fileHasWritePermissions(name: String): Boolean
	def getFile(name: String): FooFile
	def makeFileWriteable(name: String)
	def makeFile(name: String, permissions: String): FooFile	
}

This second example completely removes the dependency in HighLevelFoo from LowLevelFooFile. Further, the abstraction for HighLevelFoo is controlled by HighLevelFoo. So any change to the high level abstraction does not touch changes to the low level abstraction, nor vice versa. Only the specific client that knows it wants to marry the 2 dependencies does.

This priciniple applies fractally across every level of a software system. This example was at the class level, but similar examples can be made for library, service, or system level.