Interfaces - mcbride-clint/DeveloperCurriculum GitHub Wiki

Overview

Interfaces are a very important concept in .Net that allows for massive gains in code functionality, flexibility, and testability. This can be a very tough concept to grasp and use efficiently.

An interface defines a contract that other classes can implement. It can contain methods, properties, events, or indexers. An interface can not be initialized directly but can be used in method signatures or generics to make your code more descriptive without needing to explicitly call out dependencies. A class may implement any number of Interfaces.

Interfaces, by convention, are named starting with an capital I.

// This interface outlines that any class that implements it must have a FirstName and LastName properties as well as a GetFullName method.
public interface INameable {
  string FirstName { get; set; }
  string LastName { get; set; }
  string GetFullName();
}

Use Cases

To be used effectively, interfaces are used at the boundaries of an application's architectural layers as well as anywhere that dependencies can be pulled out of logic to separate programming concerns.

Consider the following examples:

Separation of Concerns

// This class has a direct dependency on System.Console
// Even if this is the only way that errors are currently reported in this application, the code could be improved to allow for future improvements.
public class ErrorChecker{
  public void Check() {
    if(HasErrors()){
      System.Console.WriteLine("There is an Error");
    }
  }
}

// We can create an INotificationService Interface to describe the ability to report errors and extract the responsibility of handling how to report errors into other  classes
public interface INotificationServiceI {
  void Notify(string message);
}

// Then we can create a class that contains the System.Console dependency and implements INotificationService 
public class ConsoleNotificationService : INotificationService {
  public void Notify(string message){
    System.Console.WriteLine(message);
  }
}

The new Error Checker class can then receive a INotificationService instance to allow for flexibility in the future.

public class ErrorChecker {
  private INotificationService _notificationService;

  public ErrorChecker(INotificationService notificationService) {
    _notificationService = notificationService;
  }

  public void Check() {
    if(HasErrors()){
     _notificationService("There is an Error");
    }
  }
}

In the future, other implementations of INotificationService can be created and as long as the interface does not change then the concerns of Error Checker will continue to work without modification.

// Email Notification Service
public class EmailNotificationService : INotificationService { ... }

// SMS Notification Service
public class SmsNotificationService : INotificationService { ... }

// File Notification Services
public class FileNotificationService : INotificationService { ... }

// Testing Notification Service
// This could be used in Automated testing and could save messages into an in memory collection that can be read later
// The initial System.Console version could not be properly tested in Unit Tests
public class TestingNotificationService : INotificationService { ... }

Boundaries of Architectural Layers

Consider that your code has a Application Layer and a Data Access Layer. If you do not use interfaces then the two layers would be coupled directly to the other. Because they are coupled, the layers would not be able to be tested in isolation from the other.

So you could structure your business logic class, in this case UserService, so that it uses an interface for a IUserRepository. This makes the code say that it requires a class that can get User data but does not concern its self with how or where that data comes from.

public interface IUserRepository {
  IEnumerable<User> GetAll();
}

public class UserService {
  private IUserRepository _userRepo;

  public UserService(IUserRepository userRepo) {
    _userRepo = userRepo;
  }

  public IEnumerable<User> GetAll() {
    return _userRepo.GetAll();
  }
}

The IUserRepository interface allows for there to be multiple implementations that can provide User data using different methods, each of these will be compatible with the UserService business logic

public class SqlUserRepository : IUserRepository { ... }
public class OracleUserRepository : IUserRepository { ... }
public class InMemoryUserRepository : IUserRepository { ... }

Repo Programming Examples:

See Also

⚠️ **GitHub.com Fallback** ⚠️