Ultimate Interface Segregation: Dependency injection by Delegate
I've been on a bit of a tear about declaring dependency contracts and injecting only what is required. While examining the use of Interfaces in IoC and their shortcomings, I decided that taken to the extreme, dependencies come down to call dependencies, which could be modeled with delegates rather than interfaces. Instead of writing a novel, as I've been prone to, i thought I'd do a shorter post on my approach to this solution, and expand on the implementation in later posts.
To recap, in the SOLID principles, the Interface Segregation Principle states: Clients should not be forced to depend upon interfaces that they do not use. This means that interfaces should be fine-grained enough to expose no more than one responsibility. Taken to the extreme, this could be taken to mean that each interface only has a single method. There are valid SRP scenarios where a responsibility is modeled by more than one call, but let's start with the simplest scenario first, then see how well it applies to more complex responsibilities later.
In C# we have delegates, which describe a single method call. A delegate instance is a reference to a method that encapsulates a specific instance of a class, without exposing the underlying class (unless your delegate is a static method). A delegate can even be used to expose
Instead of declaring a list of interfaces that the IoC container should inject, classes would define their dependencies as delegates. Taking the example from my duck typing post, we would get the following dependency declarations.
First, we have the same service provider,
MessageQueue, which still doesn't need to implement an interface:
Next, we have the new
Producer, now declaring its dependency has a delegate:
And finally, we have the new
Consumer, also declaring a delegate for construction time injection:
Think of the delegate as your Method Interface. You could define your dependencies as
Action's, but that would obfuscate your dependencies beyond recognition in most scenarios. By using an explicit delegate, you get to attach the dependency to the class that has the dependency, in addition to having a descriptive signature.
Now, if we were to wire this up manually we'd get something like this:
That's simple enough, but not really very scalable, once you get a lot of dependencies to wire up. What we really need is an IoC container that let's us register delegates against classes, instead of having to have instances at dependency declaration time. Delegates can't be cast from one to another and are not, strictly speaking, types, which posts some challenges with creating a type-safe registration interface. There are a number of ways to accomplish this syntax, which I will elaborate on in my next post.