Program to an interface, not an implementation
Applying this promotes flexibility and maintainability by reducing dependencies on specific implementations.
When I first encountered the principle of “program to an interface, not an implementation”, I struggled a bit to fully grasp what it meant. So, I went down the rabbit hole in an effort to educate myself and learn more about it. In essence, the principle is a core concept in object-oriented design (OOD) and programming, which aims to promote flexible, scalable, and maintainable code.
The meaning of “Program to an interface”
This principle is really about dependency relationships which have to be carefully managed in a large app. – Erich Gamma
When you "program to an interface", you are writing code that communicates with other parts of a program or with other programs through abstractions like interfaces or abstract classes, rather than specific, concrete implementations. This means the code only knows about the interface (a set of methods and properties that describe a particular type of behavior), not about the details of how these methods and properties are implemented.
Let us consider the following example where we are “programming to an implementation” to help understand the concept even further:
public class PepperoniPizza
{
public string Eat()
{
return "mmm, pepperoni";
}
}
internal class Program
{
private static void Main(string[] args)
{
PepperoniPizza pizza = new PepperoniPizza();
pizza.Eat(); // returns "mmm, pepperoni"
}
}
Here we can see that the interface (the what) is tightly coupled with the implementation (the how), which can lead to challenges when your code becomes more complex. To create a looser coupling, we can abstract the interface of PepperoniPizza, either by introducing an 'interface' or an abstract class.
public interface IPizza
{
string Eat();
}
public class PepperoniPizza : IPizza
{
public string Eat()
{
return "mmm, pepperoni";
}
}
internal class Program
{
private static void Main(string[] args)
{
IPizza pizza = new PepperoniPizza();
pizza.Eat(); // returns "mmm, pepperoni"
}
}
We know it's a PepperoniPizza, but we can use the IPizza reference polymorphically, which means that we can reuse it. In other words, we can easily accommodate more than one type of pizza (woohoo, more pizza). Let's create one more pizza to illustrate how this works:
public interface IPizza
{
string Eat();
}
public class PepperoniPizza : IPizza
{
public string Eat()
{
return "mmm, pepperoni";
}
}
public class CheesePizza : IPizza
{
public string Eat()
{
return "mmm, cheese";
}
}
internal class Program
{
private static void Main(string[] args)
{
List<IPizza> pizzas = [new PepperoniPizza(), new CheesePizza()];
pizzas.ForEach(pizza => pizza.Eat()); // returns "mmm, pepperoni" and "mmm, cheese";
}
}
From the example, we can see that we are using the same interface to consume both pizzas. We simply do not care how the 'Eat' method is implemented; our primary interest is that we can call 'Eat' and enjoy our pizza.
A potential next step could be to replace the hard-coded instantiation of pizzas with some sort of method.
Interface vs. Abstract
Choosing between these two really depends on what you want to do, but luckily for us, Erich Gamma can shed some light on the matter.
As always there is a trade-off, an interface gives you freedom with regard to the base class, an abstract class gives you the freedom to add new methods later. – Erich Gamma
Interfaces are well-suited for providing common functionality to unrelated classes. However, extending the interface also means that you will need to implement additional methods in every class that uses the interface. On the other hand, abstract classes should primarily be used for objects that are closely related. If you know that the subclasses will share some common implementation, then you should use an abstract class, because interfaces do not have any implementation of their members.
Interface vs. API
Once you have given out your code and you no longer have access to all the clients, then you’re in the API business. – Erich Gamma
The difference between an interface and an API can be quickly summarized with the above statement. Clients can mean a few things. It might be another group within the company, or it could be a customer. Once the API has been published, it must be supported to ensure it does not break.
A final word
When you are implementing an interface or inheriting from an abstract class, you could say that you have a contract with the object. This, of course, means that the object will always adhere to the rules of one or the other. This type of contract, for example, can help us when we are using dependency injection or want to create a mock object.