One thing I’ve battled with in my OO, TDD adventures is useless abstractions. I find it very easy to churn out class like the PaddedSingleWidgetList
described below in an attempt to follow the Single Responsibility Principle and write testable code, but in doing so I end up with lots of classes that don’t really make much sense in isolation, and that are not inherently reusable. (Just me?)
Instead of my usual approach of splitting a problem in terms of somewhat arbitrarily chosen responsibilities, what if we divided it into the parts that need knowledge of specific types, and the parts that can have the specific types abstracted away?
A confusing responsibility
I was looking at some code that produced a list of Widget
based on a single Widget
. This list had to have certain properties in order to be used by another class. There had been some trouble naming the class that housed this responsibility, and it had ended up as an uncomfortable PaddedSingleWidgetList
with the following interface.
From the types and names used it would be reasonable to guess this code produces a list containing the given widget
and pads that list out to length
by appending default(Widget)
values. Reasonable, but wrong:
This implementation creates a modified version of the single widget it uses as the first element, and pads the list with a specific EmptyWidget
rather than a default(Widget)
.
Our guess was incorrect because the types we were using were too specific. BuildFrom
knows it has a Widget
and so can inspect its fields and use that to change behaviour, create new Widget
instances, and use Widget.EmptyWidget
as a default.
Improving our guessing
If instead the type signature was IEnumerable<T> SomeMethod<T>(T value, int length)
, and we ban reflection and exceptions and the like, we could make a more reliable guess. In this case SomeMethod
knows nothing specific about the type T
, so it can’t modify value
, nor can it arbitrarily create instances of T
. Our guess becomes that the output contains length
copies of the given value
(i.e. Enumerable.Repeat<T>
).1
Our attempt to guess is aided by the type signature (and the restrictions we placed on reflection, exceptions etc.) letting us definitively rule out certain implementations. For example, we can tell that any values of T
that appear in the output are the same value
instance passed in. Our implementation can’t create a new instance of T
without reflection as we don’t know which type T
will be. Nor can it modify a property of value
or call a method on it (again, no reflection, so we can’t know what properties or methods T
will have). C# won’t even let us yield null
without putting constraints on the generic parameter.
Abstracting away the specifics of a type with a type parameter (a.k.a. parametric polymorphism) in this way is very useful. It limits the number of possible implementations (including the number of buggy ones), which makes it easier for us to guess what the code does just from the type.2 3
Abstracting responsibilities
Let’s look at the process which led to our original IPaddedSingleWidgetList
abstraction, and then see how abstracting type details can help us split responsibilities differently.
The Thingoe
class’ responsibility is to configure a snarfblat with correct widgets based on a single widget. In the original design this has been split into two responsibilities: producing the list from a widget using an IPaddedSingleWidgetList
, and the IWidgetBasedSnarfblat
which uses these widgets.
Here is the Thingoe
class alongside the PaddedSingleWidgetList
implementation.
This seems a reasonable way of breaking the problem down into testable parts, but given the advantages of generalising we’ve seen, let’s try breaking it into the code that needs to know specifics about a Widget
, and the code that can be generalised for any type T
.
The StandaloneWidget()
method needs to know how to transform a Widget
, and something needs to know how to create the Widget.EmptyWidget
, but the padding logic can be generalised to work for any type T
. Following this division of responsibilities leads us to this implementation:
We’ve moved the Widget
specific logic from the PaddedSingleWidgetList
into Thingoe
, and extracted the more general code into ToEnumerable
and Pad
functions:
Rather than the PaddedSingleWidgetList
type that we are unlikely to use again (it is quite specific to configuring snarfblats), we now have two potentially reusable functions that will work for any type T
. We’ve also moved the specific details of configuring widgets right up to where they are used, so we don’t need to follow trails of indirection to figure out what the code is doing. And everything is still nice and testable. Previously we may have mocked a IPaddedSingleWidgetList
to return a specific list to protect against changes in that logic, but now we can test the logic directly. We can rely on Pad
and ToEnumerable
not changing, as we know there are only a couple of reasonable implementations based on that type signature.
Conclusion
Abstracting away specific types can restrict the number of possible implementations of a function. This helps make code easier to understand because we can make reasonable guesses about what it does from the type, and less compilable implementations means we have less ways to write buggy ones.
It also gives us a useful way of dividing a class’ responsibilities into the parts that need to know type specifics from those that don’t. In doing so we can get genuinely reusable code, keep related logic together, and still have testable code.
I’m not sure how broadly applicable this approach is, but it seems a useful piece of design guidance to explore.
Other possibilities include it returning
null
, an emptyIEnumerable<T>
, or some arbitrary number ofvalue
elements (constant or some function oflength
). This still gives us quite a bit of room for incorrect guesses and buggy implementations, but less room than we had with a specificWidget
type.↩In some cases we can restrict the number of reasonable implementations to one, which means if it compiles it is almost guaranteed to work. One of my favourites is this:
Func<A,C> X<A,B,C>(Func<B,C> f, Func<A,B> g)
. If you getX
compiling and don’t deliberately break it by throwing an exception or going in to an infinite loop, then yourX
will be the correct implementation!↩For more on inferring properties of types with generic parameters, have a look at Tony Morris’ slide deck on “Parametricity. Types are documentation” [PDF].↩