Skip to main content

.NET Interfaces

Introduction

In the world of .NET and object-oriented programming, interfaces play a crucial role in designing flexible, maintainable, and extensible applications. An interface in .NET is essentially a contract that defines a set of methods, properties, events, or indexers without any implementation. It serves as a blueprint that classes must follow when they implement that interface.

Think of an interface like a formal agreement: "Any class that implements this interface promises to provide these specific features." This concept is particularly powerful because it allows for loose coupling between components and enables polymorphism, a key principle in object-oriented programming.

In this tutorial, we'll explore what interfaces are, how to define and implement them in .NET, and how they can be used to solve real-world programming challenges.

Understanding Interfaces

What is an Interface?

An interface is defined using the interface keyword in C#, followed by the interface name (which by convention starts with an uppercase "I").

csharp
public interface IShape
{
double CalculateArea();
double CalculatePerimeter();
string GetShapeType();
}

This interface defines a contract with three methods that any implementing class must provide. Important characteristics of interfaces include:

  • Interfaces cannot contain implementations, only declarations
  • Interface members are implicitly public
  • Interfaces can't include fields
  • Interfaces can contain methods, properties, events, and indexers

Why Use Interfaces?

Interfaces offer several key benefits:

  1. Abstraction: They separate what something does from how it does it
  2. Multiple Implementation: Unlike classes, C# classes can implement multiple interfaces
  3. Polymorphism: They enable treating different object types uniformly
  4. Testing: They make unit testing easier by allowing mock implementations
  5. Dependency Injection: They're fundamental to implementing dependency injection patterns

Creating and Implementing Interfaces

Defining an Interface

Let's create a simple interface for a logging system:

csharp
public interface ILogger
{
void LogInformation(string message);
void LogWarning(string message);
void LogError(string message, Exception ex);
bool IsEnabled { get; }
}

Implementing an Interface

To implement an interface, a class uses the colon (:) syntax and must provide implementations for all the interface members:

csharp
public class ConsoleLogger : ILogger
{
public bool IsEnabled { get; set; } = true;

public void LogInformation(string message)
{
if (IsEnabled)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"INFO: {message}");
Console.ResetColor();
}
}

public void LogWarning(string message)
{
if (IsEnabled)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"WARNING: {message}");
Console.ResetColor();
}
}

public void LogError(string message, Exception ex)
{
if (IsEnabled)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"ERROR: {message}");
Console.WriteLine($"Exception: {ex.Message}");
Console.ResetColor();
}
}
}

Using the Implementation

Here's how you might use the logger in your application:

csharp
class Program
{
static void Main(string[] args)
{
ILogger logger = new ConsoleLogger();

logger.LogInformation("Application started");

try
{
// Some code that might throw an exception
int result = 10 / 0;
}
catch (Exception ex)
{
logger.LogError("An error occurred during calculation", ex);
}

logger.LogWarning("Application is closing");
}
}

Output:

INFO: Application started
ERROR: An error occurred during calculation
Exception: Attempted to divide by zero.
WARNING: Application is closing

Implementing Multiple Interfaces

One of the powerful features of interfaces is that a class can implement multiple interfaces:

csharp
public interface IReadable
{
string Read();
}

public interface IWritable
{
void Write(string text);
}

public class TextFile : IReadable, IWritable
{
private string content = "";

public string Read()
{
return content;
}

public void Write(string text)
{
content = text;
}
}

This example shows a TextFile class that implements both IReadable and IWritable interfaces, making it both readable and writable.

Interface Inheritance

Interfaces can inherit from other interfaces, extending the contract:

csharp
public interface IBasicLogger
{
void Log(string message);
}

public interface IAdvancedLogger : IBasicLogger
{
void LogWithSeverity(string message, LogSeverity severity);
}

public enum LogSeverity
{
Low,
Medium,
High,
Critical
}

// This class must implement both Log and LogWithSeverity
public class FileLogger : IAdvancedLogger
{
public void Log(string message)
{
// Implementation here
Console.WriteLine($"Basic Log: {message}");
}

public void LogWithSeverity(string message, LogSeverity severity)
{
// Implementation here
Console.WriteLine($"{severity} Log: {message}");
}
}

Explicit Interface Implementation

When a class implements multiple interfaces that have methods with the same signature, or when you want to hide interface members from the class's public interface, you can use explicit interface implementation:

csharp
public interface IDrawable
{
void Draw();
}

public interface IPrintable
{
void Print();
void Draw(); // Same signature as IDrawable.Draw
}

public class Document : IDrawable, IPrintable
{
// Explicit implementation for IDrawable
void IDrawable.Draw()
{
Console.WriteLine("Drawing document for screen display");
}

// Explicit implementation for IPrintable
void IPrintable.Draw()
{
Console.WriteLine("Drawing document for printer");
}

public void Print()
{
Console.WriteLine("Printing document");
}
}

When using explicit interface implementation:

csharp
Document doc = new Document();
// doc.Draw(); // This would not compile - the method is not visible

// Instead, you must cast to the specific interface
((IDrawable)doc).Draw(); // Outputs: Drawing document for screen display
((IPrintable)doc).Draw(); // Outputs: Drawing document for printer

// The Print method is still available directly
doc.Print(); // Outputs: Printing document

Default Interface Methods (C# 8.0+)

Starting with C# 8.0, interfaces can include default implementations for methods:

csharp
public interface INotifiable
{
void SendNotification(string message);

// Default implementation
void SendUrgentNotification(string message)
{
Console.WriteLine($"URGENT: {message}");
SendNotification(message);
}
}

public class EmailNotifier : INotifiable
{
public void SendNotification(string message)
{
Console.WriteLine($"Sending email: {message}");
}
// No need to implement SendUrgentNotification, it has a default implementation
}

Real-World Example: Dependency Injection with Interfaces

One common use of interfaces is in dependency injection patterns. Here's a simplified example:

csharp
// Interface defining data repository operations
public interface ICustomerRepository
{
Customer GetById(int id);
void Save(Customer customer);
List<Customer> GetAll();
}

// Service that depends on the repository interface, not the concrete implementation
public class CustomerService
{
private readonly ICustomerRepository _repository;

// Constructor injection
public CustomerService(ICustomerRepository repository)
{
_repository = repository;
}

public Customer GetCustomer(int id)
{
return _repository.GetById(id);
}

public void UpdateCustomerAddress(int id, string newAddress)
{
var customer = _repository.GetById(id);
if (customer != null)
{
customer.Address = newAddress;
_repository.Save(customer);
}
}
}

// A concrete implementation of the repository
public class SqlCustomerRepository : ICustomerRepository
{
public Customer GetById(int id)
{
// Actual implementation would query a SQL database
return new Customer { Id = id, Name = "John Doe", Address = "123 Main St" };
}

public void Save(Customer customer)
{
// Implementation for saving to a SQL database
Console.WriteLine($"Saving customer {customer.Id} to SQL database");
}

public List<Customer> GetAll()
{
// Would retrieve all customers from a SQL database
return new List<Customer>
{
new Customer { Id = 1, Name = "John Doe", Address = "123 Main St" },
new Customer { Id = 2, Name = "Jane Smith", Address = "456 Oak Ave" }
};
}
}

// Customer model
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}

Using these components:

csharp
// Create a concrete implementation of ICustomerRepository
ICustomerRepository repository = new SqlCustomerRepository();

// Inject the repository into the service
CustomerService service = new CustomerService(repository);

// Use the service
Customer customer = service.GetCustomer(1);
Console.WriteLine($"Found customer: {customer.Name} at {customer.Address}");

service.UpdateCustomerAddress(1, "789 New Street");

Output:

Found customer: John Doe at 123 Main St
Saving customer 1 to SQL database

The beauty of this approach is that we could easily swap out the SqlCustomerRepository with a different implementation (like MockCustomerRepository for testing or ApiCustomerRepository for fetching from an API) without changing the CustomerService code.

Best Practices for Using Interfaces

  1. Keep interfaces focused and cohesive: Follow the Interface Segregation Principle (part of SOLID) - prefer many small, specific interfaces over one large interface.

  2. Use meaningful names: Interface names should be descriptive of the behavior they represent. Use verb phrases like IComparable, IDisposable, or role names like IRepository.

  3. Start interface names with 'I': This is a widely accepted convention that makes interfaces easily identifiable.

  4. Consider interface inheritance carefully: While possible, deep interface inheritance hierarchies can be confusing.

  5. Don't force implementation: Don't add methods to an interface just because one implementation needs it. If only some implementations need a method, consider creating a new interface.

  6. Design for extensibility: Think about how the interface might evolve and change over time.

  7. Document the expected behavior: Since interfaces only define what methods should exist, not how they should work, good documentation is essential.

Summary

Interfaces are a powerful tool in the .NET developer's toolkit, providing a way to define contracts that classes must follow. They enable:

  • Abstraction between what needs to be done and how it's done
  • Polymorphism, allowing different objects to be treated uniformly
  • Multiple inheritance of behavior contracts (unlike class inheritance)
  • Loose coupling between components, making code more maintainable and testable
  • Implementation of common design patterns like dependency injection

By understanding and effectively using interfaces, you can create more flexible, testable, and maintainable .NET applications.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Interface Implementation: Create an IShape interface with methods for calculating area and perimeter. Implement it for Circle, Rectangle, and Triangle classes.

  2. Multiple Interface Implementation: Create a IPlayable and IRecordable interface, then implement both in a MediaFile class.

  3. Interface Inheritance: Design an interface hierarchy for a notification system with INotification as the base interface and specialized interfaces like IEmailNotification and ISmsNotification.

  4. Dependency Injection: Create a simple program that uses interfaces and dependency injection to switch between different data storage mechanisms (in-memory list, file-based storage, mock database).

  5. Real-world Application: Implement a simple logging system using interfaces that can log to the console, files, or both, depending on configuration. Include different log levels (Info, Warning, Error).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)