Coding Better: Using Classes vs. Interfaces


We basically have two different ways to provide abstractions in our source code: Classes and Interfaces.  First, let's look at the differences between a class and an interface.  The biggest and most apparent difference is that a class can hold functional logic while an interface is used to organize code and/or provide a boundary between different levels of abstraction.  In OOP terms, the class provides both encapsulation and abstraction while the interface only provides abstraction but cannot directly encapsulate any logic.  The second big difference is that class in the .NET Framework we can only inherit from one class but can implement multiple interfaces.   While these differences seem to be minor, they become very significant to the structure, complexity, maintainability and readability of code.  This difference will also affect how the code will "grow", change and potentially break over time as code is modified.

Using Interfaces:

If you have ever tried to dig into a model with a deep class hierarchy (more than two levels of class inheritance), it quickly becomes complex and difficult to understand.  This complexity makes it less flexible and buggier and thus harder to maintain and modify.   The limitations on interfaces in that they cannot implement any functional code makes source code a easier to read because they provide a consistent surface are for consumption of functionality.  Also, when we see a piece of functionality coded to an interface we can think of the interface as more like a "window" into some functionality that we know is encapsulated and will reside somewhere else.  

The nice thing about a concise interface is that when diving into an object model, we can stop our descent when we hit an interface because the interface provides a nice solid boundary between the different levels of abstraction compared to when we hit a class and have to keep digging because we don't know what lies just beneath the surface of the class.  The ability for classes to encapsulate functionality is both their greatest asset in providing code reuse and flexibility but can also easily and quickly become a cause of unnecessary complexity if inheritance is not used judiciously.

One way to think of an interface is as an abstract class with no implementation and ignore the differences in inheritance.  When looking at it this way, the problem with using an interface is that if we make a change to the surface such as adding a method or changing a parameter signature for a method this change must be made in all the classes implementing the interface.  This cascading code break problem would also appear if we have an abstract class and add an abstract method because all classes derived from our abstract class would be required to implement the new abstract method.  This is something we really have to be careful of when exposing public interfaces and abstract classes for consumption but not really a concern if our interfaces are internal to our library and are only consumed by code in our library. 

Using classes:

Exposing an abstract class in lieu of an interface gives us the ability to provide a default implementation and not break any consuming libraries code.   Because of the way interfaces respond to change it is a good idea to avoid having interfaces model domain level logic because this is where the most change happens in code.  As a general rule of thumb interfaces should be used to model more granular, non-domain-specific functionality and abstract classes should be used for domain specific modeling.

One issue with using classes for providing extensibility is that in the .NET Framework we do not have multiple inheritance so our path for extensibility has no branches and we end up with overly complex and deep class hierarchies.   This is also why it is generally better to favor composition over inheritance for code extensibility.  Using composition we generally end up with much tighter encapsulation boundaries and we don't end up getting lost dredging through an inheritance chain. 

When I'm coding I try to limit the length of inheritance chains as much as possible.  The real power of OOP can only be fully harnessed with inheritance but it is a big gun and often not used judiciously enough.  If I see a class hierarchy with more than two levels of inheritance it is a red flag that encapsulation boundaries are possibly starting to get a bit fuzzy and I'll start thinking about refactoring.  One exception is when the long inheritance chain actually provides a simpler and more concise object model than using composition would have.   Many design patterns take advantage of the power of inheritance but when coupled with a complex domain sometimes make code unmanageable so they should only be used where necessasry. 

Picking what to use:

 Classes and interfaces need to be thought of as entirely different animals and are not both suited to do the same job well.  I have heard arguments that an abstract class is superior to an interface in that you can add additional methods and provide a default implementation so as not to break the code implementing the interface.  Personally, I think this argument means there are initial design flaws in the surface are of the interface.  Most likely the interface was modeling domain-level logic and a class actually should have been used in the first place.  

Using inheritance for extensibility usually causes "pockets" of code encapsulation in the different layers of inheritance and the maintainability of the code base will deteriorate over time.  However, sometimes there is no other way to make our code extensible from outside our library.  We have to keep in mind extensibility is different from consumption of functionality which can usually be achieved through composition.  Our goal should be to have a simple and clean model where encapsulation is as complete as possible for each class which will promote readability and maintainability and keep our domain model healthy.  Unfortunately there is no silver bullet and some thought is always required to make sure we are always striving to use the best tool for the job at hand.

To examine the differences between classes and interfaces we can also examine the surface area or boundaries exposed by the class or interface alone and ignore the implementation code residing inside classes for a minute.  Because interfaces will break consuming code if they change they should be thought of as defining "hard" boundaries between different levels of encapsulation.  Classes as much more malleable and can be thought of as providing "soft" boundaries. Because class boundaries are softer, classes are much more forgiving to future change.  Publicly exposed interfaces should not change after they are published because they will break consuming code. 

We have the same cascading break issue if method signatures change for the publicly exposed surface are of our code which is why it is a good idea to keep as many classes and interfaces internal to our library as possible and have publicly exposed methods take classes or interfaces as arguments wherever possible because it is more robust than exposing a long list of more granular parameters.

 I sometimes use internal interfaces to define hard domain level encapsulation boundaries  within my code because any breaks only affect my code base and will not break consuming code but I am also very careful to not ever expose them publicly.  These internal interfaces can provide scaffolding for the way code is structured in the library, provides nice hooks for testability and makes it easier to ensure consistency in the classes.  But when I need to expose some functionality publically for consumption by another library my first choice is composition.  My second choice is to opt for an abstract class with no implementation because it allows me more flexibility in changing my code and not breaking my consumers.   I'll only use an interface when it is non-domain specific and I can be reasonably sure it won't change.

Generally it is good to keep classes and interfaces as cohesive as possible which will limit their surface areas.  There is a correlation between the size of the surface are of our classes and interfaces and the probability they will be "hit" by change requests coming from bug fixes, refactoring, or changing requirements.  On the other hand, having too many granular classes can also clutter the domain and add additional complexity.  I have found that the lesser of the two evils can be found by focusing on writing cohesive code.

Wrap up:

Classes and interfaces need to be thought of as entirely different animals and are not both suited to do the same job well.   Classes (both abstract and concrete) are great for modeling domain-level objects and because of their softer surface area they can be added to in the future with less impact. 

Generally we should try to avoid having public interfaces model domain level logic because they have harder boundaries and the domain surface is usually subject to the greatest change over the life of the code.  Public interfaces are great for defining smaller more granular pieces of non-domain-specific functionality and defining rules for how classes should interact.  Public interfaces should generally be smaller and designed so that they will never have to change.  Some examples of good interface design are IDisposible, IEnumerable and IEnumerator because they are simple and can be useful for many different types of objects regardless of their domain. 

Internal interfaces can be coarser and define encapsulation boundaries in domain logic but we need to be careful that they don't become public because most likely they will change over time and cause cascading breaks which we don't want to bubble outside our code base. 

Finally, classes are primarily where our domain modeling and implementation should take place.  They can hold up better over time with larger surface are than interfaces would.   This is because classes have more malleable surface areas and can accommodate change as our domain becomes better defined or changes. 

I hope you found this article useful.

Until next time,
Happy coding