Delight

Aug 2009, Thomas Leonard

Dependency Injection

The basic idea of dependency injection is this:

  • When an object needs access to another existing object, it should accept it as an input.

Here is some code that is not using dependency injection:

# Not using dependency injection (not Delight code)
class HttpProxy:
	static HttpProxy singleton
	string[string] settings

	HttpProxy getSingleton():
		if singleton is null:
			singleton = new HttpProxy()
		return singleton

class Foo:
	void doSomething():
		HttpProxy proxy = HttpProxy.getSingleton()
		proxy.get(...)

class Main:
	void main():
		Foo foo = new Foo()
		foo.doSomething()

The idea here is to simplify the API of Foo. The user doesn't need to tell foo how to find the HTTP proxy object with the proxy configuration; it finds it itself.

The problems with this are:

  • Actually, it's really useful to know that Foo uses the HttpProxy object.
  • We might sometimes want to create multiple instances of Foo with different proxies (especially in unit tests, but also at other times).

In dependency injection, the proxy must be passed explicitly:

# Using dependency injection
class HttpProxy:
	string[string] settings

class Foo:
	in HttpProxy proxy

	void doSomething():
		proxy.get(...)

class Main:
	void main():
		HttpProxy proxy = new HttpProxy()
		Foo foo = new Foo(proxy)
		foo.doSomething()

Now it's clear that Foo uses an HTTP proxy, and we can create different objects with different settings.

Essentially, the Main class becomes a configuration file, instantiating, configuring and connecting together the components used in the program.

The Java Spring framework uses this style extensively to great effect (it refers to it as inversion of control).

Delight is unusual in that it enforces this style, rather than simply permitting it (as other languages do), by removing global state from the language. The first example isn't valid Delight code, due to the static class variable.

I did this because I have found that dependency injection works very well when used everywhere, but connecting dependency injected code with non-dependency injected code is painful. Delight is an experiment to see what happens when the whole language and standard library uses this method.

Also, it can eliminate some possibilities when trying to debug a program if you know that it can't be using global state, rather than thinking that it probably doesn't.

Hints

If you haven't used this style before, you might be tempted to create a huge GlobalState object in Main and pass it around everywhere. Doing that will lose a lot of the benefits.

Another trap is changing your library API to accept all the arguments it needs to build some object. For example, say you have:

# Not delight code
class WebBrowser:
	void browse():
		HttpProxy proxy = HttpProxy.getSingleton()
		...

Now you can't call getSingleton, there is a temptation to do this:

# Bad style
class WebBrowser:
	in Network network
	in Configuration config

	void browse():
		HttpProxy proxy = new HttpProxy(network, config)
		...

After all, you need a proxy, and to make a proxy you need a network and a configuration, so you should take a network and a configuration as inputs, right?

The correct solution is probably more like this:

class WebBrowser:
	in HttpProxy proxy

	void browse():
		...

As far as possible, try to avoid constructing things that you could have taken as inputs. This way:

  • Your web-browser doesn't depend on a particular implementation of HttpProxy, which can now be an interface.
  • Before, it wasn't clear what the WebBrowser wanted the configuration for. For example, it might have used it to change the user's configuration. The HttpProxy interface is more specific.
  • When testing, it's easier to provide a mock HttpProxy than to provide a mock Network and a mock Configuration.