Interfaces in C#
Introduction to Interfaces
An interface in C# is a contract that defines a set of methods and properties that a class must implement. Interfaces provide a way to achieve abstraction, multiple inheritance, and loose coupling in your applications.
Why Use Interfaces?
- Define common behavior across unrelated classes
- Implement multiple inheritance (which is not possible with classes in C#)
- Create loosely coupled systems
- Facilitate testing through dependency injection
- Enable polymorphic behavior across different class hierarchies
- Separate what needs to be done (interface) from how it's done (implementation)
Defining and Implementing Interfaces
An interface is defined using the interface
keyword. By convention, interface names in C# start with the letter "I".
Basic Interface Syntax
// Interface definition
public interface ILogger
{
// Method declaration (no implementation)
void Log(string message);
// Property declaration (no implementation)
bool IsEnabled { get; set; }
// Default interface method (C# 8.0+)
void LogError(string message)
{
Log($"ERROR: {message}");
}
}
// Class implementing the interface
public class ConsoleLogger : ILogger
{
// Implementing the property
public bool IsEnabled { get; set; } = true;
// Implementing the method
public void Log(string message)
{
if (IsEnabled)
{
Console.WriteLine($"[LOG]: {message}");
}
}
// No need to implement LogError as it has a default implementation
}
// Usage
public void Main()
{
ILogger logger = new ConsoleLogger();
logger.IsEnabled = true;
logger.Log("This is a regular log message");
logger.LogError("Something went wrong");
}
Key points about interfaces:
- Interface members do not have access modifiers - they are implicitly public
- Interfaces cannot contain fields
- Interfaces can contain methods, properties, events, and indexers
- Starting with C# 8.0, interfaces can include default method implementations
- A class that implements an interface must provide implementations for all interface members (unless they have default implementations)
Implementing Multiple Interfaces
One of the key advantages of interfaces is that a class can implement multiple interfaces, which is not possible with class inheritance in C#.
Multiple Interface Implementation
// First interface
public interface IReadable
{
string Read();
int Position { get; set; }
}
// Second interface
public interface IWritable
{
void Write(string text);
bool CanWrite { get; }
}
// Third interface
public interface IClosable
{
void Close();
bool IsClosed { get; }
}
// Class implementing multiple interfaces
public class TextFile : IReadable, IWritable, IClosable
{
private string content;
private bool isClosed;
public TextFile(string initialContent = "")
{
content = initialContent;
Position = 0;
isClosed = false;
}
// IReadable implementation
public string Read()
{
if (IsClosed)
throw new InvalidOperationException("Cannot read from a closed file");
return content;
}
public int Position { get; set; }
// IWritable implementation
public void Write(string text)
{
if (IsClosed)
throw new InvalidOperationException("Cannot write to a closed file");
content += text;
}
public bool CanWrite => !isClosed;
// IClosable implementation
public void Close()
{
isClosed = true;
}
public bool IsClosed => isClosed;
}
// Usage
public void Main()
{
TextFile file = new TextFile("Initial content. ");
// Using as IReadable
IReadable readable = file;
Console.WriteLine(readable.Read()); // Outputs: "Initial content. "
// Using as IWritable
IWritable writable = file;
writable.Write("Additional text.");
// Using as the original class
Console.WriteLine(file.Read()); // Outputs: "Initial content. Additional text."
// Using as IClosable
IClosable closable = file;
closable.Close();
// Now the file is closed
Console.WriteLine($"Can write: {file.CanWrite}"); // Outputs: "Can write: False"
// This would throw an exception
// file.Write("More text.");
}
Benefits of implementing multiple interfaces:
- A class can fulfill multiple roles or responsibilities
- Different aspects of a class's behavior can be separated into distinct interfaces
- The same class can be treated as different types in different contexts
- Multiple interfaces provide a way to achieve a form of multiple inheritance
Interface Inheritance
Interfaces can inherit from other interfaces, extending their contract. A class that implements a derived interface must implement all members from both the base and derived interfaces.
Interface Inheritance Example
// Base interface
public interface IEntity
{
int Id { get; set; }
DateTime CreatedDate { get; set; }
}
// Interface inheriting from IEntity
public interface IProduct : IEntity
{
string Name { get; set; }
decimal Price { get; set; }
string Category { get; set; }
}
// Interface inheriting from IEntity
public interface ICustomer : IEntity
{
string FirstName { get; set; }
string LastName { get; set; }
string Email { get; set; }
}
// Class implementing IProduct must implement all members from both IProduct and IEntity
public class Product : IProduct
{
// IEntity members
public int Id { get; set; }
public DateTime CreatedDate { get; set; }
// IProduct members
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
// Class implementing ICustomer must implement all members from both ICustomer and IEntity
public class Customer : ICustomer
{
// IEntity members
public int Id { get; set; }
public DateTime CreatedDate { get; set; }
// ICustomer members
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
// Additional non-interface members
public string FullName => $"{FirstName} {LastName}";
}
Key points about interface inheritance:
- An interface can inherit from multiple interfaces
- A class implementing a derived interface must implement all members from all base interfaces
- Interface inheritance creates a type hierarchy
- Interface inheritance is useful for creating specialized or extended versions of an interface
Explicit Interface Implementation
Sometimes a class may need to implement two interfaces that have members with the same name. In such cases, C# allows explicit interface implementation to resolve the ambiguity.
Explicit Implementation Example
// First interface
public interface IControl
{
void Paint();
void Resize(int width, int height);
}
// Second interface with a method that has the same name
public interface IPrintable
{
void Paint(); // Same name as in IControl
void Print();
}
// Class implementing both interfaces
public class Button : IControl, IPrintable
{
// Implicit implementation (shared)
public void Resize(int width, int height)
{
Console.WriteLine($"Resizing to {width}x{height}");
}
// Explicit implementation for IControl
void IControl.Paint()
{
Console.WriteLine("Painting control");
}
// Explicit implementation for IPrintable
void IPrintable.Paint()
{
Console.WriteLine("Preparing for printing");
}
// Explicit implementation for IPrintable
void IPrintable.Print()
{
Console.WriteLine("Printing");
}
// A public method that delegates to the interface methods
public void PaintForDisplay()
{
((IControl)this).Paint();
}
public void PaintForPrinting()
{
((IPrintable)this).Paint();
}
}
// Usage
public void Main()
{
Button button = new Button();
// Accessing the implicitly implemented method
button.Resize(100, 50); // Outputs: "Resizing to 100x50"
// Accessing the explicitly implemented methods through casting
((IControl)button).Paint(); // Outputs: "Painting control"
((IPrintable)button).Paint(); // Outputs: "Preparing for printing"
((IPrintable)button).Print(); // Outputs: "Printing"
// Using the public methods that delegate to interface methods
button.PaintForDisplay(); // Outputs: "Painting control"
button.PaintForPrinting(); // Outputs: "Preparing for printing"
// Using through interface references
IControl control = button;
control.Paint(); // Outputs: "Painting control"
IPrintable printable = button;
printable.Paint(); // Outputs: "Preparing for printing"
}
Key points about explicit interface implementation:
- Explicit implementation allows a class to provide different implementations for methods with the same name from different interfaces
- Explicitly implemented members cannot be accessed directly through the class instance - you need to cast to the interface type
- Explicit implementation can be used to hide interface members from the public interface of the class
- This technique is useful when implementing interfaces with conflicting or overlapping members
Default Interface Methods (C# 8.0+)
Starting with C# 8.0, interfaces can include default implementations for methods. This allows you to add new methods to interfaces without breaking existing implementations.
Default Interface Methods Example
// Interface with default method implementations
public interface ILogger
{
// Regular interface method (no implementation)
void Log(string message);
// Default implementation for LogWarning
void LogWarning(string message)
{
Log($"WARNING: {message}");
}
// Default implementation for LogError
void LogError(string message)
{
Log($"ERROR: {message}");
}
// Default implementation using other methods
void LogInformation(string message)
{
Log($"INFO: {message}");
}
}
// Minimal implementation only needs to implement the Log method
public class SimpleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
}
// Class that overrides a default implementation
public class VerboseLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
// Override the default implementation
public void LogWarning(string message)
{
Console.WriteLine($"[{DateTime.Now}] ⚠️ WARNING ⚠️: {message}");
}
}
// Usage
public void Main()
{
SimpleLogger simpleLogger = new SimpleLogger();
simpleLogger.Log("Regular message"); // Uses the implemented method
// These use the default implementations
((ILogger)simpleLogger).LogWarning("Something might be wrong");
((ILogger)simpleLogger).LogError("Something went wrong");
((ILogger)simpleLogger).LogInformation("Just FYI");
VerboseLogger verboseLogger = new VerboseLogger();
verboseLogger.Log("Regular message"); // Uses the implemented method
// This uses the overridden implementation
((ILogger)verboseLogger).LogWarning("Something might be wrong");
// These use the default implementations
((ILogger)verboseLogger).LogError("Something went wrong");
((ILogger)verboseLogger).LogInformation("Just FYI");
}
Benefits of default interface methods:
- Enables interface evolution without breaking existing implementations
- Provides a form of multiple inheritance of behavior in C#
- Allows utility methods to be added to interfaces
- Facilitates API design with reasonable defaults
- Supports optional interface methods without forcing implementers to provide empty implementations
Interface-based Programming
Interface-based programming is a design approach that focuses on using interfaces as the primary means of interaction between components. This promotes loose coupling and increases the flexibility and testability of your code.
Dependency Injection with Interfaces
// Interfaces
public interface IUserRepository
{
User GetById(int id);
List GetAll();
void Save(User user);
}
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public interface ILogger
{
void Log(string message);
}
// Service that depends on interfaces rather than concrete implementations
public class UserService
{
private readonly IUserRepository _userRepository;
private readonly IEmailService _emailService;
private readonly ILogger _logger;
// Dependencies are injected through the constructor
public UserService(
IUserRepository userRepository,
IEmailService emailService,
ILogger logger)
{
_userRepository = userRepository;
_emailService = emailService;
_logger = logger;
}
public void RegisterUser(string username, string email)
{
try
{
// Check if user exists
var existingUsers = _userRepository.GetAll();
if (existingUsers.Any(u => u.Email == email))
{
throw new Exception("Email already registered");
}
// Create and save new user
var user = new User { Username = username, Email = email };
_userRepository.Save(user);
// Send welcome email
_emailService.SendEmail(
email,
"Welcome to our service",
$"Hello {username}, thank you for registering!"
);
_logger.Log($"User {username} registered successfully");
}
catch (Exception ex)
{
_logger.Log($"Error registering user: {ex.Message}");
throw;
}
}
}
// Concrete implementations
public class SqlUserRepository : IUserRepository
{
// Implementation using SQL database
public User GetById(int id) { /* Implementation */ }
public List GetAll() { /* Implementation */ }
public void Save(User user) { /* Implementation */ }
}
public class SmtpEmailService : IEmailService
{
// Implementation using SMTP
public void SendEmail(string to, string subject, string body) { /* Implementation */ }
}
public class FileLogger : ILogger
{
// Implementation using file system
public void Log(string message) { /* Implementation */ }
}
// Usage
public void Main()
{
// Create concrete implementations
IUserRepository userRepository = new SqlUserRepository();
IEmailService emailService = new SmtpEmailService();
ILogger logger = new FileLogger();
// Inject dependencies
UserService userService = new UserService(userRepository, emailService, logger);
// Use the service
userService.RegisterUser("johndoe", "john@example.com");
// For testing, we could use mock implementations instead:
// IUserRepository mockRepository = new MockUserRepository();
// IEmailService mockEmailService = new MockEmailService();
// ILogger mockLogger = new MockLogger();
// UserService testService = new UserService(mockRepository, mockEmailService, mockLogger);
}
Benefits of interface-based programming:
- Loose coupling - Components depend on abstractions rather than concrete implementations
- Testability - Dependencies can be mocked or stubbed for unit testing
- Flexibility - Implementations can be replaced without changing client code
- Separation of concerns - Interfaces define what needs to be done, classes define how it's done
- Dependency injection - Interfaces enable clean dependency injection patterns
- Extensibility - New implementations can be added without modifying existing code
Common Interface Patterns
Repository Pattern
// Repository interface for data access
public interface IRepository where T : class
{
T GetById(int id);
IEnumerable GetAll();
void Add(T entity);
void Update(T entity);
void Delete(T entity);
void SaveChanges();
}
// Entity class
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// Repository implementation using Entity Framework
public class CustomerRepository : IRepository
{
private readonly DbContext _context;
public CustomerRepository(DbContext context)
{
_context = context;
}
public Customer GetById(int id)
{
return _context.Set().Find(id);
}
public IEnumerable GetAll()
{
return _context.Set().ToList();
}
public void Add(Customer entity)
{
_context.Set().Add(entity);
}
public void Update(Customer entity)
{
_context.Entry(entity).State = EntityState.Modified;
}
public void Delete(Customer entity)
{
_context.Set().Remove(entity);
}
public void SaveChanges()
{
_context.SaveChanges();
}
}
Observer Pattern
// Observer interface
public interface IObserver
{
void Update(T data);
}
// Subject interface
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
// Concrete subject
public class WeatherStation : ISubject
{
private List> _observers = new List>();
private WeatherData _weatherData;
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(_weatherData);
}
}
// Method to set new weather data and notify observers
public void SetMeasurements(float temperature, float humidity, float pressure)
{
_weatherData = new WeatherData
{
Temperature = temperature,
Humidity = humidity,
Pressure = pressure,
Timestamp = DateTime.Now
};
Notify();
}
}
// Data class
public class WeatherData
{
public float Temperature { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
public DateTime Timestamp { get; set; }
}
// Concrete observers
public class DisplayDevice : IObserver
{
public string DeviceName { get; set; }
public DisplayDevice(string name)
{
DeviceName = name;
}
public void Update(WeatherData data)
{
Console.WriteLine($"{DeviceName} displaying: Temp: {data.Temperature}°C, Humidity: {data.Humidity}%, Pressure: {data.Pressure}hPa");
}
}
public class WeatherLogger : IObserver
{
public void Update(WeatherData data)
{
Console.WriteLine($"[LOG] {data.Timestamp}: Temp: {data.Temperature}°C, Humidity: {data.Humidity}%, Pressure: {data.Pressure}hPa");
}
}
Best Practices for Interfaces
Interface Design Guidelines
- Interface Segregation Principle - Keep interfaces small, focused, and cohesive
- Name interfaces clearly - Use the "I" prefix and descriptive names that reflect their purpose
- Design for consumers - Focus on the needs of the code that will use the interface
- Be consistent - Use consistent naming and parameter patterns across related interfaces
- Prefer composition - Compose larger interfaces from smaller ones rather than creating monolithic interfaces
- Consider versioning - Design interfaces with future evolution in mind
- Document behaviors - Clearly document expected behavior, exceptions, and constraints
- Favor immutability - When appropriate, design interfaces that promote immutable implementations
Common Interface Mistakes to Avoid
- "Fat" interfaces - Interfaces with too many methods or unrelated methods
- Breaking interface contracts - Changing existing interfaces in a way that breaks implementations
- Implementation details in interfaces - Including details that leak implementation concerns
- Not using interfaces for abstraction - Creating interfaces that mirror concrete classes without adding abstraction
- Over-abstraction - Creating interfaces for every class without a clear need
- Ignoring inheritance hierarchies - Not considering how interfaces fit into the broader type system
- Excessive casting - Designing systems that require frequent casting between interfaces