A few principles often come to mind when building software. While Program to an Interface doesn’t have an easy-to-remember acronym – unlike e.g. DRY (Don’t Repeat Yourself) – it’s nonetheless useful to keep in mind. In this week’s newsletter, we’ll take an introductory look at what it means, and how you can make your code more flexible by following this advice.
The Problem with Using Explicit Types
Programming to an interface involves working with abstractions rather than concrete implementations. To explain what this means and how it’s helpful, consider the following example:
var primeNumbers = new List<int> { 2, 3, 5, 7, 11 };
Here we have a List
containing the first five prime numbers. We can pass it into the following method as an argument to write each number to the console.
void OutputToConsole(List<int> numbers)
{
foreach (var n in numbers)
{
Console.WriteLine(n);
}
}
So far so good.
But let’s imagine we later receive a new requirement: we need to know when new prime numbers are added to our list. Luckily, we don’t need to worry about the notification system – we can simply change our List
to an ObservableCollection and subscribe to its CollectionChanged
event.
var primeNumbers = new ObservableCollection<int> { 2, 3, 5, 7, 11 };
Unfortunately, our existing code no longer compiles. OutputToConsole
is expecting a List<int>
, but we’re now passing an ObservableCollection<int>
. This is a quick and easy fix in our example. Afterall, we only have one method parameter type to change. But not all real-world projects are as simple – it could have resulted in a bigger change if we had more code expecting a List<int>
. And it would have been simpler too if we didn’t have to make any changes to our method signatures at all.
Adding Resilience
When programming to an interface, we write code around the data contract it needs rather than the object types it interacts with. By relying on abstractions rather than specific implementations, we loosen the coupling between dependencies: our code becomes more adaptable, letting us switch freely between compatible implementations without requiring additional changes.
In our example, OutputToConsole
is a simple method: it takes a set of numbers, iterates through it, and writes each number in turn to the console. If we analyse its requirements, we’ll find we simply need a collection where we can access each element once. We could rewrite it using an IEnumerable
.
void OutputToConsole(IEnumerable<int> numbers)
{
foreach (var n in numbers)
{
Console.WriteLine(n);
}
}
By making this change, it doesn’t matter if we declare our prime number collection as:
var primeNumbers = new List<int> { 2, 3, 5, 7, 11 };
or in any of the following ways:
var primeNumbers = new ObservableCollection { 2, 3, 5, 7, 11 };
var primeNumbers = new HashSet { 2, 3, 5, 7, 11 };
var primeNumbers = new[] { 2, 3, 5, 7, 11 };
As the data types in all four of the preceding declarations implement IEnumerable
, no changes are required to OutputToConsole
.
Summary
Programming to an interface loosens code dependency coupling. By focussing on data contracts, you can freely switch between compatible implementations without further changes to the rest of your code.
By using the simplest interfaces that still let you do what you need to, you’ll have a good chance of writing code that won’t need updating even if your system’s other requirements do.
Thanks for reading!
This article is from my newsletter. If you found it useful, please consider subscribing. You’ll get more articles like this delivered straight to your inbox (once per week), plus bonus developer tips too!
Top comments (2)
That's a very good article that is succinct and makes a great point with great example code. Thanks for writing this up.
Thanks for reading.
I've got a few more articles on this concept lined up, each with a slightly different angle. It's a fairly simple concept, but it can be helpful in so many ways.
Back when I was learning C#, I often thought of interfaces as being extra work without much benefit. I've since learned how they can help to add some structure and separation. And it's something I'd like to help others to see too.