IoC without the fluff
Things like dependency injection and IoC are cool, and they do decouple your code and make it easier to extend at test. But the problem is that the software community always tends to reinvent the same solutions to the same problems and then give them a totally different name. I mean, what are the real differences between Haskell Type Classes and the now defunct C++1x ‘concept’… er… concept. (Seriously guys, we’re running out of different synonyms for ‘thing’. We’ve got object, class, type, type class, structure…) Here’s what IoC and DI are, how they interrelate, without using silly words like ‘container’, which is just another word for ‘thing’ pretty much in this context. We have a function, let’s say, and I’m going to use python notation since it’s short and sweet.
x = 0
Everyone can figure out what that function does. But how will it react in a large system? Who the hell knows, it uses a global variable that can be modified at any time, from anywhere. Our function ‘func’ is now coupled to a global variable, as is, by associativity (or is it commutativity?) anyone who uses it. So we change it to this far more elegant version:
Voila! In DI terms, we’ve ‘injected’ the ‘dependency’ of func on the variable x. Now func is only dependent on two locally defined parameter variables, a far more elegant and simple way to do things. Let’s fast forward to OO for a second. Assume we have a class foo…
self.myFoo = foo()
def func(self, x):
return self.x + self.myFoo
This is OO, so it’s nice and decoupled right? Not really. If you unroll the object, thinking of it basically as a poor man’s closure, you realize that func is actually doing something like this (assuming no one changes self.myFoo)
return foo() + x
Global functions, when they’re pure, are nice to have. But you can still see the problem of the coupling between func and foo, now. If foo is not pure, then we have side effect issues, but moreover, we still have just plain maintainability/extensibility issues with the coupling between the two. The number one use case pointed to by dependency-injection types is that of testing. What if we want to test func, but we don’t want to test foo? Maybe foo takes a long time. Or maybe we haven’t even written foo yet! (Now you can see the connection between TDD, dependency injection, and why the two might change your style of design. You can test top level objects without even implementing ‘low-level’ ones yet.. But that’s tangential.) We can do the same thing we did in the first case though to break that coupling:
def func(x, foo_thing):
return x + foo_thing()
Oh hey, higher order programming. Who’d have thought that would show up here? Let’s roll our stuff back into familiar OO terms:
def __init__(self, foo_thing):
self.myFoo = foo_thing()
def func(self, x):
return self.myFoo + x
Dependency, consider yourself injected. Ok, fine, what the hell is IoC then? Why does it have to have such a crazy name, and what are these containers, and wah wah wah! Well, let’s go back to our familiar func and make it a little more complex:
x = 0
y = 0
z = 0
a = 0
b = 0
c = 0
return var + a + b + c + x + y + z
Gross! Global dependencies on like, a bajillion different things! Let’s inject those dependencies!
def func(var, x, y, z, a, b, c):
return var + a + b + c + x + y + z
Ah, much better. Er, wait… now my buddy, who had code like this:
def func_2(var1, var2):
return func(var1) + func(var2)
Has had his interface explode to:
def func_2(var1, var2, a, b, c, x, y ,z):
return func(var1, a, b, c, x, y, z) + func(var2, a, b, c, x, y, z)
And his callers have their interfaces explode, and their callers have theirs, etc… But wait, any of you OO gurus notice that a,b,c and x,y,z never seem to make any sense without each other? They always seem passed with each other, they seem semantically related, who knows what else. Why, I’ll bet our design could be more… what’s that nice word elegant designs are described as? Oh yes, cohesive!
def __init__(self, x, y, z):
self.x = x; self.y = y; self.z = z #just leave them as plain-old-data and public
Oh wait, and two points, well, we can call that a…
class distance: #er, maybe not the best name…
def __init__(self, pt1, pt2):
self.pt1 = pt1; self.pt2 = pt2
Now are interfaces shrink down, only needing one extra parameter, a ‘distance’, which I’m sure in our func, foo, bar whatever application we’re building, make good sense semantically and is drawn from our domain. Likewise, let’s say that we’re using dependency injection to make our objects a lot less coupled.
def __init__(self, obj1, obj2, obj3, obj4, obj5):
self.obj1 = obj1; self.obj2 = obj2; self.obj3 = obj3; self.obj4 = obj4; self.obj5 = obj5
Wow, those dependencies sure are injected all right. But that class has been injected so much, it’s beginning to look like a heroine addict. These objects all seem to have something to do with each other, why don’t we bundle them up in a cohesive object?
def __init__(self, obj_builder_thingie): #TODO: think of a better name
self.obj1 = obj_builder_thingie.obj1(); …
Ah, now our design is decoupled and cohesive. Hallmarks of good design. (Of course, simply bundling arbitrary objects together isn’t necessarily cohesive. We’re assuming in this case, whenever we’ve bundled something together, those objects have had something to do with each other semantically or in the domain.) For some reason, whenever we bundle these objects together, our Java buddies (and C# buddies) want to call this ‘Inversion of Control’, mostly because it’s now the caller/creator’s problem of figuring out how it wants to ‘configure’ the innards of our bar class. They sometimes will go even further and make obj_builder_thingie not just a normal object, but a framework for going out and reading XML files to dynamically figure out what obj1() and obj2() should return. That way, instead of editing a nice clean python unit test file to configure and modify your tests, you can edit a huge ugly xml file with lots of angled brackets to configure and modify your tests. Sounds much more enterprisey to me! Job security here I come!
So, let’s recap. Dependency injection is moving a ‘global’ to a ‘parameter’, which insofar as primitive types and values, has always seemed like a good idea. But for OO guys, moving global functions (or an object representing those functions) seems like a brand spankin’ new idea, when really functional guys have been moving global functions to parameters for years. C’est la vie, at least we’re learning something from each other. The specific case bandied about is always how this makes testing easier because you can ‘inject’ which objects you’d like to collaborate with in the constructor (or a setter, etc… but setter’s are evil.) But this solution can be generalized to ‘inject’ any configurable behavior you want in the form of variables, functions or classes themselves in a dynamic language like python. The side effect is your parameter lists get huge, and while your code is decoupled, its decoupled kind of like a box of legos with no instructions on how to build your starship. So you use your ole’ object-oriented smarts to start grouping some of those parameters together into objects themselves, and now your ‘injected class’ begins looking through the objects that are passed into it to find what it needs, rather than everything being unrolled on the parameter list. IoC is basically, then, dependency injection (or the voracious removal of anything global) on a large scale and must, then, solve the problems (explosive parameter lists) that come with that.
That’s it in a nutshell. Hopefully I haven’t confused you further. Dependency injection is just another step in the good practice of “make things as local as possible”, or decoupling. Parameter lists are more local than global, even for functions like class constructors. IoC is the counterbalance to massive decoupling, and that’s to make the program more ‘cohesive’ again by grouping up many of these new things being passed in parameter lists into objects and hierarchies themselves. Any questions?
No comments yet.