.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").
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:
- Abstraction: They separate what something does from how it does it
- Multiple Implementation: Unlike classes, C# classes can implement multiple interfaces
- Polymorphism: They enable treating different object types uniformly
- Testing: They make unit testing easier by allowing mock implementations
- 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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
// 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
-
Keep interfaces focused and cohesive: Follow the Interface Segregation Principle (part of SOLID) - prefer many small, specific interfaces over one large interface.
-
Use meaningful names: Interface names should be descriptive of the behavior they represent. Use verb phrases like
IComparable
,IDisposable
, or role names likeIRepository
. -
Start interface names with 'I': This is a widely accepted convention that makes interfaces easily identifiable.
-
Consider interface inheritance carefully: While possible, deep interface inheritance hierarchies can be confusing.
-
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.
-
Design for extensibility: Think about how the interface might evolve and change over time.
-
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
-
Basic Interface Implementation: Create an
IShape
interface with methods for calculating area and perimeter. Implement it forCircle
,Rectangle
, andTriangle
classes. -
Multiple Interface Implementation: Create a
IPlayable
andIRecordable
interface, then implement both in aMediaFile
class. -
Interface Inheritance: Design an interface hierarchy for a notification system with
INotification
as the base interface and specialized interfaces likeIEmailNotification
andISmsNotification
. -
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).
-
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).
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!