The term ‘Inversion of Control’ is typically used in relation to DI/IoC frameworks. However, that same idea works at the micro scale too. I for one encounter a lot of what I could call Local Inversion of Control (LIoC) all the time, and this is what this short post is all about.
Examples
Let’s start with the simplest example. Say you want to add an item to the collection: this is typically written as
myCollection.Add(x)
If you pause for a moment and think about the above statement and voice it in English, what you’ll get is myCollection
should add to itself item x
which is very much unreadable. This is precisely the case where IoC is useful, and here’s how: let us define an AddTo()
extension method:
public static T AddTo<T>(this T self, ICollection<T> collection)
{
collection.Add(self);
return self;
}
Now, instead of calling the Add()
method on the collection, control is inverted, so the AddTo()
method is called on the collection:
var aList = new List<int>();
2.AddTo(aList);
Translated to English, the above now reads 2 should be added to aList
. Makes more sense, doesn’t it? Also, as an added bonus, the AddTo()
operation is fluent because it returns the original argument. This means that a value can be added to more than one list at once, e.g.:
2.AddTo(someList).AddTo(someOtherList);
Let’s try something else. Suppose you want to check that a collection is empty. Typically, you’d write
if (myClass.Fields.Count == 0) { ... }
if (!myClass.Fields.Any()) { ... }
Both of these approaches are okay, I guess, but the code reads strange. The first example says if myClass
’s fields count is equal to zero whereas the second reads if not myClass
’s fields are any in number, which is even worse.
So here’s a pair of extension methods:
public static bool HasSome<TSubject, T>(this TSubject subject,
Func<TSubject, IEnumerable<T>> propertyToCheck)
{
return propertyToCheck(subject).Any();
}
public static bool HasNo<TSubject, T>(this TSubject subject,
Func<TSubject, IEnumerable<T>> propertyToCheck)
{
return !HasSome(subject, propertyToCheck);
}
Now, a check for the presence of fields turns into
if (myClass.HasNo(c => c.Fields)) {}
which reads as myClass
has no fields, which is precisely what we’re checking here.
Okay, one more example. Say you’re checking a string value, and you want to compare it with a set of values. This is typically represented as
if (myOp == "AND" || myOp == "OR" || myOp == "XOR") { ... }
This reads in a kind of OK, but fragmented, fashion. A much better way would be to group the variants into an array but, in C#, this just looks ugly:
if (new[]{"AND", "OR", "XOR"}.Contains(myOp)) { ... }
This is even less semantic. What if there was an IsOneOf()
function instead?
public static bool IsOneOf<T>(this T self, params T[] variants)
{
return variants.Contains(self);
}
Now, the above check would turn to
if (myOp.IsOneOf("AND", "OR", "XOR")) { ... }
The great thing about the above approach is that the arguments are declared as params T[]
, so we don’t have to initialize any arrays with new[]
before using them as a test.
Summary
So here’s the basic premise of LIoC: given a function f(x,y...)
, it may in certain cases be more benefitial from the semantic and usability perspectives to instead define an extension method on x
that applies the function f
using the argument(s) y
.
Additional benefits are provided by the fact that:
Extension methods may take generic arguments, thus letting you define inverted operations on groups of similar objects.
The params
keyword lets your API behave in a ‘variadic’ fashion by unchaining you from explicit collection initialization.
Additional level of expressiveness can be gained from passing in lambdas or expressions.
The disadvantages of this approach are:
Having to manage separate classes containing extension methods.
API pollution — any time you define an extension method on a generic type, all objects get an additional IntelliSense popup member.
Possible performance overhead — for example, in cases where you use a lambda instead of direct member access.
I use LIoC all over the place, just like I use the Maybe monad. It makes the code a lot more readable, maintainable and amenable to refactoring. ▪