Interfaces are one of the most lightweight components in Object Oriented Programming. Defining a new interface is quite a simple process without as many rules and restrictions as in the case of class definitions. This simplicity, however, has also prevented many programmers from realizing the true importance of interfaces for writing clean and loosely coupled code.
The top 5 reasons why interfaces are useful:
- They are a contract in the code.
- Make implementation changes less painful.
- Serve as connecting points between modules.
- Interfaces encourage testable code.
- Interfaces foster the single responsibility principle and interface segregation principle.
Read this article to learn why are interfaces useful in object-oriented programming and why you should use them in your codebase.
What is an interface, and why is it useful?
The answer to the question of “Why interfaces are useful” may not seem straightforward at first sight. However, if you look close enough, it’s not too difficult to see how proper use of interfaces helps you write better quality code in the long run.
What is an interface in OOP?
An interface is similar to a class except that it doesn’t contain actual implementations of methods or properties it defines. Instead, it stops at introducing the behavior an object of the interface’s type is expected to display. The responsibility of writing the code to enable the defined behavior falls into the hands of the class which implements that particular interface.
For example, think of an “IShipment” interface that captures the behavior associated with product shipments.
public interface IShipment
{
void StartShipment();
void FinishShipment();
string GetShipmentStatus();
}
It would contain methods to start and finish a shipment and retrieve the shipping status of an IShipment object. If a separate product class implements the above interface, it must include all three methods the interface introduced with their implementations to control the product’s shipment behavior.
Why do you need interfaces when there are abstract classes?
Defining methods without their actual implementations is not unique to interfaces. It’s also one of the main characteristics of an abstract class. Does this mean abstract classes are a replacement for interfaces?
No. Here’s why.
While abstract classes can introduce implementation-less methods and properties, they are not limited to having only such fields. For example, before C# 8.0, interfaces couldn’t give such default implementations to methods and properties they defined. However, this has now changed. Starting with C# 8.0, interfaces now support default interface methods.
public interface IDefault
{
void MethodName() { Console.Write("Default implementation"); }
}
The interface method MethodName
has a default implementation in the interface.
The other more decisive difference that sets apart the two is the abstract class can’t support a multiple inheritance structure. Much like any other class, in C#, a class can inherit only one abstract class as its parent. But a class can implement more than one interface, allowing it to belong to different groupings to mimic different real-world scenarios.
Now that you understand the importance of having interfaces as a separate entity in the domain of Object Oriented Programming, it’s time to figure out how and where they become useful to programmers.
#1 An interface is a contract in the code
Interfaces are useful in object-oriented programming because they specify the behavior that an object must implement. An interface is a contract between an object and its code. If an object implements an interface, it is guaranteed to support the behavior specified by that interface.
When a class implements an interface, it also agrees to implement all the behavior the interface defines without exception. This dynamic between the two entities is not much different from a contract agreement. One party consents to the conditions of the other to establish a partnership. However, in this case, the interface’s conditions are non-negotiable because the class has to either implement all its methods or give up on using the interface.
This guarantee of a class implementing all the interface’s methods allows outside parties to interact with the system without knowing the exact implementation details.
In the previous example of the IShipment interface, the system doesn’t need to know additional information about the specific product to start and finish the shipment and retrieve the shipping status. Instead, it accepts a regular IShipment object and calls the required methods defined by the interface to perform these actions.
Determining which class’s method implementation to use happens only during the program’s runtime by looking at the exact type of the received object.
To reiterate the above idea, the contract that binds an interface and the class that implements it provides one of the easiest ways to achieve abstraction in OOP. It exposes only the object’s functionality to the outside world without revealing implementation-level details. This loose coupling between objects enables programmers to write more reusable and maintainable code in the long term.
#2 Interfaces make implementation changes less painful
Interfaces help you take advantage of polymorphism in OOP.
By allowing an object’s runtime type to differ from its declared type, interfaces add considerable flexibility to mostly statically typed languages like C#.
In addition, it enables you to add runtime-dependent implementations to the code. For example, in the case of the IShipment interface, the system can pick the type of product (book, phone, food) it will create based on user input without worrying about that decision affecting shipment-related method calls.
At the same time, consider a scenario where you want to change the shipment implementations of several product classes. Or say you must replace an existing product class with a new one. Completing such an implementation change becomes much less painful if you have relied on an interface in your code since the start. This is because the interface allows you to handle the change at the product level without significantly modifying the rest of the system. In addition, what you have used to interact with the system, in this case, is the interface, not the individual products themselves.
#3 Interfaces can serve as connecting points between modules
Modules are a common sight in large-scale applications. For example, a real-world hospital management system would contain separate modules for:
- patient management,
- staff management,
- billing, etc.
In such cases, the task of developing individual modules falls into the hands of different teams. However, each team would still have to rely on the functionality of other modules to allow the system to work as one application.
If a team had to learn every implementation detail about the other module to make such a collaboration work, it would beat the purpose of distributing the responsibilities in the first place. So then, how can you support connections between modules without burdening outsiders with unnecessary implementation details?
The answer is, of course, interfaces.
Interfaces allow you to reveal only the behavior and services each module exposes to the outside world without mentioning the implementations. It gives access to a much less complex process while adding an extra security layer to the module.
#4 Interfaces encourage testable code
Proper use of interfaces makes your code easily testable. In addition, they allow you to quickly isolate the unit under testing, stopping other parts of the system from influencing the final results.
How do interfaces achieve this?
For example, assume that you are testing a module that connects to another module through an interface to process payments. Use the dependency injection to inject the payment interface implementation. Then you can create a mock class that implements the second module’s public interface and replicates its payment processing behavior. In that case, you can easily isolate the first module from being influenced by the operations and errors of the second module.
#5 Fostering the single-responsibility principle and interface segregation principle with interfaces
Good interfaces allow you to identify boundaries in your code. Be it the boundary of a class, method, or module, interfaces can help you discover if they are becoming unnecessarily large.
If you’re familiar with SOLID design principles in OOP, interfaces play a particularly significant role in safeguarding two:
- single responsibility principle (SRP)
- interface segregation principle (ISP)
Single Responsibility Principle states that every class, method, or module should have one responsibility and one logical function to make the code easier to read, reuse, and maintain. However, identifying when a unit grows out of this mold is not always a simple task. But observing the unit’s dependencies with interfaces often acts as a good metric of knowing when and how it surpasses SRP recommendations. For example, a single unit relying on too many interfaces is most likely to have taken more than one responsibility. In such cases, you can split the large unit into several units, divided along the lines of the non-overlapping dependencies.
On the other hand, Interface Segregation Principle states that a class shouldn’t be forced to implement a method in an interface it doesn’t need. Since classes are contract-bound to implement all methods defined in an interface, a breach of ISP becomes a sign of either the class or the interface growing too big. It gives you a warning sign to refactor your code and distribute the unfitting functionality to new classes or interfaces.
Constantly refactoring with an eye on these SRP and ISP principles allows you to tighten the boundaries of your code without forming tight dependencies between different units. As a result, it helps you keep the code quality high.
Should every class implement interfaces?
The importance of interfaces for programmers is not a matter up for debate. But does it mean every class you write has to implement an interface?
No.
If the class and its functionality don’t have alternative implementations, you don’t have to associate it with an interface. There’s no need to use interfaces in your code just for the sake of it.
However, defining an interface even when one class will implement it is a common practice in C# applications. The reason is that they bring an additional level of abstraction and don’t add too much overhead to the code.
Conclusion
Interfaces are useful in object-oriented programming because they allow for a level of abstraction. This means that the details of how an object is implemented are hidden from the user of the object. The user only needs to know the contract defined by the interface.
Despite their simplicity, interfaces are one of the crucial components in OOP for writing loosely-coupled, high-quality code. They help you achieve the much-lauded abstraction promised by OOP and a form of polymorphism that supports runtime-dependent behavior in applications.
If you know how to create good interfaces and when to use them in code, you’re already on your way to taking full advantage of the object-oriented way of programming when building real-world applications.