In simple terms and/or in high-level pseudo-code, how does a DI container work and how is it used?
You configure a DI container so it knows about your interfaces and types - how each interface maps to a type.
When you call Resolve
on it, it looks at the mapping and returns the mapped object you have requested.
Some DI containers use conventions over configuration, where for example, if you define an interface ISomething
, it will look for a concrete Something
type to instantiate and return.
"It's nothing more than a fancy hash table of objects."
While the above is a massive understatement that's the easy way of thinking about them. Given the collection, if you ask for the same instance of an class - the DI container will decide whether to give you a cached version or a new one, or so on.
Their usage makes it easier and cleaner when it comes to wiring up dependencies. Imagine you have the following pseudo classes.
class House(Kitchen, Bedroom)
// Use kitchen and bedroom.
end
class Kitchen()
// Code...
end
class Bedroom()
// Code...
end
Constructing a house is a pain without a DI container, you would need to create an instance of a bedroom, followed by an instance of a kitchen. If those objects had dependencies too, you would need to wire them up. In turn, you can spend many lines of code just wiring up objects. Only then could you create a valid house. Using a DI/IOC (Inversion of Control) container you say you want a house object, the DI container will recursively create each of its dependencies and return you a house.
Without DI/IOC Container:
house = new House(new Kitchen(), new Bedroom());
With DI/IOC Container:
house = // some method of getting the house
At the end of the day they make code easy to follow, easier to write and shift the responsibility of wiring objects together away from the problem at hand.
At its core a DI Container creates objects based on mappings between interfaces and concrete types.
This will allow you to request an abstract type from the container:
IFoo f = container.Resolve<IFoo>();
This requires that you have previously configured the container to map from IFoo to a concrete class that implements IFoo (for example Foo).
This in itself would not be particularly impressive, but DI Containers do more:
- They use Auto-Wiring which means that they can automatically figure out that if IFoo maps to Foo and IBar maps to Bar, but Foo has a dependency on IBar, it will create a Foo instance with a Bar when you request IFoo.
- They manage the lifetime of components. You many want a new instance of Foo every time, but in other cases you might want the same instance. You may even want new instances of Foo every time, but the injected Bar should remain the same instance.
Once you start trying to manually manage composition and lifetimes you should start appreciating the services provided by a DI Container :)
Many DI Containers can do much more than the above, but those are the core services. Most containers offer options for configuring via either code or XML.
When it comes to proper usage of containers, Krzysztof Kozmic just published a good overview.