Interfaces allow you to create an abstraction layer in your code.  This abstraction allows for encapsulation and polymorphism – two of the pillars of Object-Oriented Programming.  Setting aside the nuance that C# 8 and above allows for some level of concrete implementation, Interfaces are simple declarations – signatures without any actual implementations.

interface IStudent
{
	int StudentId {get;set;}
	string FirstName {get;set;}
	string LastName {get;set;}

	void EnrollInClass(ClassDetails classDetails);
	void DropClass(int classId);
	void PayTuition(PaymentDetails paymentDetails);
	void Party();	
}

By convention, interface names begin with an uppercase `I`.  Although this is not a requirement, this is a convention that is widely accepted in the C# community and it is certainly adhered to in .NET (framework) itself. The example interface above, IStudent, states that any class that implements this interface at a minimum must have three properties – StudentId, FirstName, LastName and four methods – EnrollInClass, DropClass, PayTuition, Party. While the interface definition dictates the names, data types, return types, and parameters, the actual implementation of these elements are left for the concrete class that implements them.

For completeness’ sake, I should mention that C# interfaces does allow for default implementations in interface definitions since C# version 8. However, such usage should be avoided for normal use-cases and only be used if it proves to be an absolute necessity. In short, if you’re creating a brand-new interface and see the need for a default implementation, use an abstract class instead.

To implement an interface, create a class and reference the interface after the class name, separated by a colon:

public class Student : IStudent
{
    public int StudentId { get; set; }
    public string FirstName  { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;

    public void DropClass(int classId)
    {
        throw new NotImplementedException();
    }

    public void EnrollInClass(ClassDetails classDetails)
    {
        throw new NotImplementedException();
    }

    public void Party()
    {
        throw new NotImplementedException();
    }

    public void PayTuition(PaymentDetails paymentDetails)
    {
        throw new NotImplementedException();
    }
}

In the example above, the Student class implements the IStudent interface. The syntax to do this is identical to how inheritance works in C#. That is, if you want to create a class that inherits from another class, you reference the parent class after your sub-class name, separated by a colon. If you have a case where you want to inherit from a base-class and also implement an interface, the parent class must come first in the definition followed by one or more interface names, each separated by a comma. Note that in C#, you can only inherit from a single class while you can implement an unlimited number of interfaces.

public class Student : Person, IStudent, ITeacherAssistant
{
    // code goes here.
}

That’s it for the how. Let’s look at a few reasons as to why you may want to use them in your programs.

Abstractions

Interfaces allows you to reason with the various pieces of your program in an abstract manner.  Such abstractions help in the sheer understanding of your code, in the maintenance and upkeep of your codebase, especially as you add more and more functionality within your application.  In the case of debugging a potential flaw in your application, It allows you to concentrate on the pieces that you need to concentrate on while effectively eliminating other areas from your purview.

Loose-Coupling

Generally speaking, you always want to strive for loose-coupling in your code.  That is, eliminate or reduce the interdependence between classes or modules within your application.  For instance, you may have the need to send out text message notifications from within your application.  You can certainly bring in a third-party SDK, directly into your application or make HTTP calls directly from within your application to a third-party service, to carry out your texting needs.  However, this approach may be short-sighted.  If tomorrow, you want to use a different provider for your texting needs, you’ll have to rip out all the references to that previous provider and replace them with the new one.  If on the other hand, you properly structured your code, creating an interface and then creating a concrete implementation of that interface, you can now do a clean break from provider A and bring in provider B with a high degree of confidence. The only code that would need to be added is a new concrete implementation of the “test message sender” interface while the rest of your code-base remain virtually untouched.

Write Testable Code

Interfaces help greatly in the realm of automated testing, especially unit-testing. Take the text-messaging example from above. You want to write unit-tests for your code to ensure that they work as expected but in doing so, you don’t want your test code to connect to any real third-party services or send out any actual text-messages. If your code is utilizing an interface for your text message sending needs, you can replace the real implementation in your unit-tests for a fake text-message sender that mocks your calls to that service and sends a fake, predetermined response back. With this stand-in, you can now test your code with no dependencies on any third-party services, without having to address any of the challenges or implications surrounding the use of that service.

Closing Thoughts

Interfaces are certainly not unique to the C# Language.  Several other languages have the notion of interfaces either using the interface keyword or using other constructs that behave in a similar fashion.  If you are new to object-oriented programming, it may at first seem like an unnecessary step – to define interfaces and then create a concrete class that implements that interface.  However, as you mature in your programming journey and start to get into some intermediate and advanced practices such as dependency injection and automated unit and integration testing, interfaces suddenly become an indispensable tool in your toolkit.  

Leave a Comment

Your email address will not be published. Required fields are marked *