How Coding to an Interface Can Increase Your Code’s Flexibility
Write code that depends on what it needs, not what it’s given
Hi Friends.
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<int> { 2, 3, 5, 7, 11 };
var primeNumbers = new HashSet<int> { 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.
Bonus Developer Tip
Sometimes you might want to extract an app’s program icon/s from an .exe file. You can use 7-Zip to unpack .exe files, allowing you to find whatever you need.
These tips are exclusively for my Substack subscribers and readers, as a thank-you for being part of this journey. Some may be expanded into fuller articles in future, depending on their depth.
But you get to see them here, first.