Ayende’s “IoC and Average Programmers”
Ayende and Eli, among several others, are having a little back and forth about testability and how that is impacted by design choices. Here’s some more background.
I think the question that Eli poses at the end of his post merits a little more attention. Here it is for simplicity’s sake
Quiz: Which is easier to maintain:
File.WriteAllText(filename, text);or
using(TextWriter writer = IoC.Resolve<ifilewriterfactory>().Create(filename)) writer.Write(text);
The first approach represents maintainability while the second approach represents “testability” insofar as it has been constructed to allow the injection of stub or mock objects, while the first approach, using a sealed static method, cannot easily have its behavior modified at test execution time.
There are more differences to the two approaches than are generally being discussed, and I think it might be valuable to talk about them a little bit more.
Here’s a quick rundown of the differences.
The File class doesn’t provide deterministic disposability in the same way that the second example’s using statement does. So let’s remove the using statement from the second example.
The File class isn’t injecting any dependencies, so let’s remove the container resolution bit.
After those two steps, we’re left with this (I’ve named the concrete factory on my own):
FileWriterFactory.Create(filename).Write(text);
The signature is a little more verbose, but otherwise, we’re not dealing with a significantly different idiom here. I think it’s safe to say that the fundamental behavior of the two approaches are pretty similar.
Reducing the two approaches to the lowest common denominator reveals that the question itself may be spurious, or at least poorly framed. What value is there in comparing the testability or maintainability of two approaches that in both concrete and abstract terms fulfill two different sets of requirements?
In my opinion, the maintenance-based critique of the second approach as needless complexity is a straw man. You may as well ask whether it’s easier to maintain an algorithm that performs basic arithmetic or one that implements Runge-Kutta approximation of differential equations.
A more productive comparison of would involve setting out a couple of basic requirements and implementing them using dependency injection and without using it. In that case, we might see that the ability to automatically resolve dependencies against services such as authentication (of course we need permissions to write the file, right?) is actually a win in the long term. But then again, we might not.
The example Eli gave of a development team implementing an unnecessary feature and then being burned by it down the road is a great cautionary tale about why good product management is important, but I am not convinced that it holds much water when it comes to a discussion about whether or not designing your code for testability is a good practice.
I think that framing the discussion to be more about whether or not your approach to designing applications takes into account that various types of software that are out there and what their expectations are in terms of lifecycle, staffing, and maintenance might help to better address the core issue here: is it ever acceptable to put the big guns away and just hack out something that works?