IoC: What is it and why do I care?
It finally sank into my melon why Dependency Injection, Inversion of Control, and IoC Containers exist. The ‘a-ha’ moment came, and I wanted to share it. Here’s my “explain it to your mom” definition of IoC Containers, and the back-story to make it make sense. First, we start with bugs …
Bugs: These are when a program malfunctions. They aren’t good. An abundance of bugs means the program is broken, and users are grumpy.
Trying to solve bugs more proactively leads us to …
Unit Testing: Automated testing of software leads to catching and solving bugs faster. Unit testing specifically involves testing a small piece of the program in isolation – away from all other parts of the program. More generally, the term “testing” is used to mean testing the application, its parts, its functions, interaction between parts, access to external resources (a database, a web service, etc). Testing code frequently and automatically leads to better code because bugs are exposed by tests, not by users.
Unit Testing highlights inconsistent object initialization. When I create a new transaction, I need to set the date, set the customer, set the product, and verify all these exist. That seems to fundamentally go against the nature of Object Oriented development: Polymorphism, Inheritance, and Encapsulation. The other two aren’t important to this discussion, but Encapsulation is.
Encapsulation: Encapsulation is hiding the grunt of the task behind a simpler skin. “Yes, I know to wash the car, you need to get it wet, rub soap on it, scrub, rinse, dry, wax, etc. I just want to call Car.Wash() and you figure out the details.” This is encapsulation.
There are many ways to encapsulate awkward code, but for this discussion we’ll discuss encapsulating object creation. Encapsulation of object initialization is solved via …
Factory Pattern: In the Factory Pattern, object initialization is done in a central place for each type of object. When you want a Car object, you call the CarFactory, pass it parameters, and it returns a fully initialized car. Initialization is done centrally in the Factory method, so object initialization is always uniform and complete.
During testing, you have to test lots of permutations of sequential behaviors that may depend on external resources that are slow. For example, consider a test to see if a recent transaction query only grabs transactions within 7 days. To test this, you need some transactions older than 7 days ago and some newer than 7 days ago. If your TransactionFactory is coded to create all transactions as “right now”, you can’t exactly wait around 7 days for your unit test to finish. In a less obscure example, say you are trying to verify a calculation, but the database query used to populate the calculation is slow. What you need is the ability to alter these dependencies during a test. This leads to …
Dependency Injection: Dependency Injection is the process of telling an object what its dependencies are. If you’re creating transactions on a variety of dates, and the Factory marks every new transaction as “today”, you may want to specify what “today” means to the Factory during a test. In the real program, “today” will always mean “right now”. But during the test, “today” needs to match the scenario we’re testing to make sure the program functions correctly.
Dependency Injection is also really handy if the unit you’re testing depends on other things which are slow – for example a database or a web service. If these things take seconds or minutes to return the requested resource, a single test takes at least as long. If we have many such tests – or groups or suites of these tests – the tests will be said to “take too long to run”, so they won’t be run often. A test that isn’t run doesn’t find bugs, thus doesn’t serve its purpose. This dilemma leads us to …
Mocking: Creating a mock (fake) object that behaves in a predictable way is a great way to “fake-out” an external resource with a faster “work-alike” counterpart. If the GeoLocation service takes too long to return, and we’ve proven (through another test) that it always returns the same answer when we ask for a specific location, we may not want to flex that resource to calculate the distance between a customer and a delivery location. Instead, we can create a mock web service that always answers correctly instantaneously. This mock object stands in for the real, expensive resource during a test. The mock object is injected into place via Dependency Injection.
When you’ve got mock objects in the mix, the Factory’s object initialization parameters can get pretty intense. Not only do I need to specify the make & model of the car I want built, I also need to specify which database you get it from (the real one or the mock object), maybe what “today” means for initializing the waranty, and maybe I also need to specify other external resources such as the GeoLocation service strategy for how to find the car. This initialization puts a heavy burden on the other parts of the program that use this code. Mocking via Dependency Injection and hiding details of execution through Encapsulation seem to be fundamentally at odds, which leads us to …
IoC Container: Inversion of Control (IoC) – is a methodology of injecting dependencies through a centralized Factory for all objects in the application. The IoC container typically has a method like Resolve() or Create() or Get(). You pass it details of what you want, it makes an object in its centralized factory, and hands it back. The benefit to being centralized is not only does it know how to make your object, but it knows how to make many objects. If your object requires external dependencies, it can find and create those, and inject them in as it creates your object. You need not worry about these details.
The IoC Container is an uber object Factory that centrally creates all objects, and recursively resolves Dependency Injection requirements of those objects while encapsulating this complexity from consumers of these objects.
If you’re in the middle of a unit test, and you need to adjust what “today” means, you configure the IoC Container, and all objects that need it get it. But the code that wants the object doesn’t need to know any of this. It doesn’t need to know if it’s in the middle of a test or not. It doesn’t even need to know if the database is real or the data its working on is real. It just says to the IoC container, “Give me an object that is …” and poof, there it is.
IoC Container methodology is awesome, as it empowers:
- consistent initialization through a central factory
- dependency injection to allow mock objects to stand in for computationally expensive objects during tests
- consuming classes need not resolve dependencies to use objects IoC is awesome. It is hardly the magic bullet though. The nature of the IoC Container is it must know how to resolve all the initialization of all the objects in the application. To do this effectively, it must be configured to know all the dependencies it needs to inject, to know which object to return when asked for an interface, how to instantiate and initialize everything, and all this in a generic, maintainable way. This configuration is key. You’re specifying, “When you’re asked for a ‘car’ that is ‘red’, go create it like this, initialize its dependencies like this, etc.” Beefy IoC container libraries will allow you to return the Singleton instance of an object created previously, a new object, a cached object (if it exists), etc.
Traditionally, IoC configuration comes in two ways. I haven’t found any compelling reason to choose one over the other as both are not that great: create the map in code or in a configuration file.
Code is great as you get compile time support for it. If you change the name of a class, the IoC configuration breaks until you also change it. (Or the refactor tool does it for you.) But on the other hand, it takes a recompile to change your mind.
A configuration file is easily changed, but it’s fragile. Generally, you have to specify the full object namespace and type name, maybe even the library name and/or path it came from. And all this without intellisense, compile-time checking, or even spell checking.
So, that’s what IoC is. It isn’t the magic bullet, but it can sure make unit testing much more effective. To sum it up, my ‘a-ha’ “to my mom” definition of IoC is:
The IoC Container is an uber object Factory that centrally creates all objects, and recursively resolves Dependency Injection requirements of those objects while encapsulating this complexity from consumers of these objects. (no, my mom didn’t get it either.)