Interface Anti-Patterns: The Vagrant
Ancient Knowledge
This article is getting old. It was written in the ancient times and the world of software development has changed a lot since then. I'm keeping it here for historical purposes, but I recommend you check out the newer articles on my site.
The proliferation of N-Tier architecture has caused a common anti-pattern with the use of interfaces in .NET applications. This anti-pattern often presents itself as a form of dependency injection (DI) with its usage intending to reduce the coupling between applications but often creates the exact opposite situation.
Contracts
To understand the usage of interfaces I think it's important to look at the fundamentals of contracts in software development. Lets take a look at one of the most basic contracts in C#, the humble function.
// The contract for the following function is void ChangePatientName(string)
void ChangePatientName(string name){
this.Name = name;
}
Basic functions define their contracts as parameters and return types. It's important to understand the function defines its own contract. That no external entity defines the contract for the function. This is an important, yet simple distinction that we need to understand before using interfaces. A more complex example of a function might be the following:
// The contract for the following function is Patient Create(string, string, string, int)
Patient Create(string name, string city, string state, int age){
return new Patient(){
FullName = name,
BirthCity = city,
BirthState = state,
CurrentAge = age
};
}
// This function could be called by a client application:
var name = "Bob Villa";
var city = "Green Bay";
var state = "WI";
var age = 99;
var patient = Create(name, city, state, age);
The Create
function defines its contract as a set of patient information. Again, note how the function itself defines its own contract and the calling code must comply with that.
Interfaces
If you start with basic contracts, then interfaces are a logical step forward. The parameters defined by the function can easily be exported to a container that defines that information. This container increases the portability of that information; instead of passing around individual variables as in our example above, we can now box them up in a container and pass that single container around.
interface IPatientInfo {
string Name {get;}
string City {get;}
string State {get;}
int Age {get;}
}
In the interface above, we have wrapped up our individual parameters into a simple interface.
Where do we define this interface?
It should be obvious after talking about contracts that the function itself should define the interface. More generically we could say that the consumer defines the interface. But watch out, because here is where our sneaky anti-pattern rears its ugly head. All too often this interface is defined in the calling application. It's easy to make this mistake because the application will work, the compiler will not complain, and it looks like your separating your code nicely and doing all those great things with patterns we know make software better. So what's the big deal? Lets jump into some code and see where the problems lie. ** Warning: Anti-Pattern **
// Assembly: Contoso.Logger
interface ILogger {
void Log(string message);
}
class FileLogger: ILogger {
void Log(string message){
// implemention details ommitted
}
}
// Assembly: Contoso.Services
class WidgetService {
ILogger _logger;
WidgetService(ILogger logger){
_logger = logger;
}
}
At first glance there really doesn't seem to be anything wrong with the above code. I have a FileLogger class and it implements an interface that can be consumed in the services assembly.
Problem #1: The WidgetService
now has more than one reason to change.
The contract is no longer defined by the consumer, and leaves the services assembly vulnerable to unexpected changes. It's conceivable that the Contoso.Logger
assembly is shared between applications, and a change made to the ILogger
to support another application would trickle down to the Contoso.Services
assembly.
Problem #2: Contoso.Logger becomes a dependency sink.
Let's imagine for a moment that we have 3 different loggers defined in Contoso.Logger
.
// Assembly: Contoso.Logger
interface ILogger {
void Log(string message);
}
class FileLogger: ILogger {
void Log(string message){
// implemention details ommitted
}
}
class HttpLogger: ILogger {
void Log(string message){
// implemention details ommitted
}
}
class NodeJsLogger: ILogger {
void Log(string message){
// implemention details ommitted
}
}
Suddenly, our Contoso.Logger assembly has references to System.Web and some magical NodeJs .NET assembly. If we implement this logger in our console application we now have to reference all of Contoso.Logger
s dependencies even if we aren't going to use them. Obviously a couple extra .NET framework references wouldn't be an issue, but if anything depends on third party assemblies, you will end up reducing the re-usability of your components very quickly.
Problem #3: Unit Testing
If we were to build up unit tests for the WidgetService
we would quickly find that it's impossible to do so without referencing the Contoso.Logger
assembly. This is a good indication the assembly is going to be difficult to test in isolation when we need to reference additional assemblies that are not under test.
Solution
Interfaces should be defined in the consumer assembly, which is going to provide much better flexibility in your application architecture.
// Assembly: Contoso.FileLogger
class FileLogger: ILogger {
void Log(string message){
// implemention details ommitted
}
}
// Assembly: Contoso.Services
interface ILogger {
void Log(string message);
}
class WidgetService {
ILogger _logger;
WidgetService(ILogger logger){
_logger = logger;
}
}
This approach keeps our contract in the trusty hands of the consumer and allows greater reuse of the assemblies. We can now test WidgetService
in isolation, since it no longer requires any external dependencies. UPDATE! Removed the last example as it really didn't highlight anything useful.
tldr;
Define your interfaces in the consumer, not in the implementation assembly.