DEV Community

Cover image for Getting Started With C#: Interfaces
Nathan B Hankes
Nathan B Hankes

Posted on • Updated on

Getting Started With C#: Interfaces

This article is part of a series. For part one, visit the link below:
Part 1: Getting Started with C#: Classes, Objects, and Namespaces

In C#, an interface specifies a set of methods, properties, events, or indexers (collectively referred to as class members) that a class must define. This programmatic obligation is referred to as a contract. The interface contract functionally serves as a blueprint that classes must adhere to, ensuring consistent behavior, enabling polymorphism (we'll get more into this later), and providing a host of other benefits.

To apply an interface, a class must use the :InterfaceName syntax after the class name. In the example below, you'll see the Truck and Car class names followed by :IVehicle. This means that both of these classes are bound to the IVehicle interface contract. By implementing :IVehicle on these classes, the class enters into a contract that promises to define the members stipulated by the IVehicle interface.

Continuing with our example from Part 1 here's an interface and its implementation:

namespace VehicleInventory
{
    public interface IVehicle
    {
        string Make {get; set;}
        string Model {get; set;}
        int Year {get; set;}

        void StartEngine();
        void CheckOil();
    }

    public class Car: IVehicle
    {
      private string make;
      private string model;
      private int year;

      public string Make { get; set; }
      public string Model { get; set; }
      public int Year { get; set; }

      public Car(string make, string model, int year)
      {
        this.make = make;
        this.model = model;
        this.year = year;
      }

      public void StartEngine()
      {
        Console.WriteLine("Car engine is started.");
      }

      public void CheckOil()
      {
        Console.WriteLine("Car oil checked.");
      }
    }

    public class Truck: IVehicle
    {
      private string make;
      private string model;
      private int year;

      public string Make {get; set;}
      public string Model {get; set;}
      public int Year {get; set;}

      public Truck(string make, string model, int year)
      {
        this.make = make;
        this.model = model;
        this.year = year;
      }

      public void StartEngine()
      {
        Console.WriteLine("Truck engine is started.");
      }

      public void CheckOil()
      {
        Console.WriteLine("Truck oil checked.");
      }
    }
}
Enter fullscreen mode Exit fullscreen mode

You'll notice that the IVehicle interface is implemented with the interface keyword. The interface does not provide any implementation details. It only defines members. The classes that implement this interface must provide implementations for all the members defined in the interface (in our case, methods and properties). This is why the Car class and the Truck class both have StartEngine() and CheckOil() method implementations. And this is why they both have the Make, Model, and Year properties defined.

By separating the interface from the class implementation, we create a clear distinction between the contract and the actual implementation. While it may require writing additional code in each class that implements an interface, the benefits of code organization, reusability, flexibility, and testability outweigh the initial effort. Let's take a look at some of these benefits below.

Benefits of Using Interfaces

Implementing methods individually in each class may seem like more work initially, but it serves an important purpose and offers several advantages:

Contract Enforcement

As already discussed, interfaces provide a way to enforce a contract or agreement between classes. By implementing an interface, a class commits to providing specific methods or functionality defined by the interface. This ensures that all implementing classes adhere to the same set of behaviors.

Polymorphism

Interfaces enable polymorphism, allowing objects of different classes to be treated interchangeably. This flexibility is valuable when writing code that works with multiple types of objects based on a shared set of methods. For example, you can create a collection of vehicles (objects implementing the IVehicle interface) and perform common actions on them without worrying about the specific class. Let's look at an example:

using System;

namespace VehicleInventory
{
    class Program
    {
        static void Main(string[] args)
        {
            IVehicle car = new Car("Honda", "Civic", 2022);
            IVehicle truck = new Truck("Ford", "F-150", 2021);

            PerformVehicleActions(car);
            PerformVehicleActions(truck);
        }

        static void PerformVehicleActions(IVehicle vehicle)
        {
            Console.WriteLine($"Make: {vehicle.Make}");
            Console.WriteLine($"Model: {vehicle.Model}");
            Console.WriteLine($"Year: {vehicle.Year}");

            vehicle.StartEngine();
            vehicle.CheckOil();

            Console.WriteLine();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the Main method above, we create instances of Car and Truck and assign them to variables of type IVehicle. This allows us to treat both objects polymorphically.

The PerformVehicleActions method takes an IVehicle parameter and prints the vehicle details, starts the engine, checks the oil, and adds a line break for separation.

Because the IVehicle interface guarantees the Make, Model, Year, StartEngine(), and CheckOil(), the PerformVehicleActions method can treat the Truck and Car classes the same.

Loose Coupling and Flexibility

Using interfaces reduces tight coupling between classes. Instead of depending on specific class implementations, you can depend on the interface. This promotes better separation of concerns and improves the flexibility of your code. You can easily introduce new classes that implement the interface or modify existing classes without affecting the rest of the codebase.

Let's say we want to add a new class to identify a new vehicle type. In the example below, we'll create a new Motorcycle class:

public class Motorcycle : IVehicle
{
    private string make;
    private string model;
    private int year;

    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }

    public Motorcycle(string make, string model, int year)
    {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public void StartEngine()
    {
        Console.WriteLine("Motorcycle engine is started.");
    }

    public void CheckOil()
    {
        Console.WriteLine("Motorcycle oil checked.");
    }
}
Enter fullscreen mode Exit fullscreen mode

With the addition of the Motorcycle class, we can now create instances of Motorcycle and pass them to the PerformVehicleActions method defined in our polymorphism example above -- without modifying the PerformVehicleActions method itself. This showcases the flexibility provided by the interface, as the existing code can seamlessly work with the new Motorcycle class without any breaking changes.

Testing and Mocking

Interfaces help facilitate unit testing and mocking. You can create mock implementations of interfaces during testing to isolate and verify the behavior of specific components without relying on actual implementations. This helps in writing robust and maintainable tests.

The interface implementation in the provided example facilitates testing and mocking by allowing the creation of test doubles or mock objects that implement the IVehicle interface. This enables isolated unit testing and the ability to simulate different scenarios without relying on the actual concrete implementations of Car and Truck. Here's an example:

using System;
using Moq;
using Xunit;

namespace VehicleInventory.Tests
{
    public class VehicleTests
    {
        [Fact]
        public void PerformVehicleActions_Car_StartEngineAndCheckOilCalled()
        {
            // Arrange
            var carMock = new Mock<IVehicle>();
            carMock.Setup(c => c.Make).Returns("Honda");
            carMock.Setup(c => c.Model).Returns("Civic");
            carMock.Setup(c => c.Year).Returns(2022);

            var vehicleActions = new VehicleActions();

            // Act
            vehicleActions.PerformVehicleActions(carMock.Object);

            // Assert
            carMock.Verify(c => c.StartEngine(), Times.Once);
            carMock.Verify(c => c.CheckOil(), Times.Once);
        }

        [Fact]
        public void PerformVehicleActions_Truck_StartEngineAndCheckOilCalled()
        {
            // Arrange
            var truckMock = new Mock<IVehicle>();
            truckMock.Setup(t => t.Make).Returns("Ford");
            truckMock.Setup(t => t.Model).Returns("F-150");
            truckMock.Setup(t => t.Year).Returns(2021);

            var vehicleActions = new VehicleActions();

            // Act
            vehicleActions.PerformVehicleActions(truckMock.Object);

            // Assert
            truckMock.Verify(t => t.StartEngine(), Times.Once);
            truckMock.Verify(t => t.CheckOil(), Times.Once);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, we use the Moq mocking framework and the xUnit testing framework to demonstrate testing and mocking with the interface implementation. We create two test methods: PerformVehicleActions_Car_StartEngineAndCheckOilCalled and PerformVehicleActions_Truck_StartEngineAndCheckOilCalled.

In each test method, we create a mock object of the IVehicle interface using Mock. We set up the necessary properties (Make, Model, and Year) for the mock objects to return specific values during the test.

Next, we instantiate the VehicleActions class, which contains the PerformVehicleActions method that we want to test. We pass the mock object (carMock.Object or truckMock.Object) to the PerformVehicleActions method.

Finally, we use the Verify method provided by the mock objects to assert that the StartEngine and CheckOil methods are called exactly once during the execution of the PerformVehicleActions method.

By creating mock objects that implement the IVehicle interface, we can isolate the behavior being tested and verify that the expected methods are called, without relying on the actual implementations of Car and Truck. This enables effective unit testing and helps identify issues in the VehicleActions class while maintaining test independence.

Adding Interfaces to Your Codebase

Interfaces are typically organized into namespaces alongside classes and other related interfaces. Remember from part one, a namespace is a container for organizing related types, such as classes and interfaces. It helps prevent naming conflicts and provides a logical structure to your code.

When it comes to storing interfaces in your codebase, there is no universally prescribed folder structure or naming convention. However, it's generally recommended to follow a structure that reflects the logical organization of your codebase and promotes maintainability. Here are some suggestions:

Create a separate folder or directory for interfaces: You can have a dedicated folder specifically for interfaces, which can make it easier to locate and manage them.

Use meaningful and descriptive names: Name your interfaces based on the functionality they represent. Make sure the names are clear and indicative of their purpose.

Group related interfaces together: If you have interfaces that are closely related, you can place them in the same folder or subfolder. This makes it easier for developers to find related interfaces when working on a specific feature or module.

Follow a consistent naming convention: It's good practice to follow a consistent naming convention for interfaces, such as prefixing them with "I" (e.g., IExampleInterface). This convention helps distinguish interfaces from classes and makes your code more readable.

Below is an example from the open source nopCommerce repository. You'll notice the interface files (staring with capital I) are in the same folder as the class files. In the External folder, all classes and interfaces are built inside of the namespace Nop.Services.Authentication.External:
Image description

Ultimately, the specific folder structure and naming conventions will depend on your project's requirements and the preferences of your development team. The key is to ensure consistency and maintainability throughout your codebase.

Top comments (0)