C# Interface Best Practices
Interfaces are a powerful feature in C# that enable clean, extensible, and maintainable code. However, to leverage them effectively, you need to follow certain best practices. This guide will walk you through the most important interface best practices in C# to help you write better code.
Introduction to Interface Best Practices
An interface in C# defines a contract that classes can implement. While interfaces are relatively simple to create, using them effectively requires thoughtful design. Following best practices ensures your interfaces remain useful, maintainable, and aligned with object-oriented design principles.
Naming Conventions
Use the "I" Prefix
In C#, interfaces should always start with the capital letter "I" to distinguish them from classes.
// Good
public interface IRepository { }
// Not recommended
public interface Repository { }
Use Descriptive Names
Interface names should be descriptive and typically represent a capability or behavior.
// Good
public interface IComparable { }
public interface IDisposable { }
public interface IEnumerable { }
// Not recommended
public interface IMyInterface { }
public interface IStuff { }
Design Principles
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on methods they don't use. Keep interfaces focused and cohesive.
// Not recommended: Too many responsibilities in one interface
public interface IEmployee
{
void CalculateSalary();
void SaveToDatabase();
void GenerateReport();
void SendEmail();
}
// Better: Segregated interfaces
public interface ISalaryCalculator
{
void CalculateSalary();
}
public interface IDataPersistence
{
void SaveToDatabase();
}
public interface IReportGenerator
{
void GenerateReport();
}
public interface IEmailSender
{
void SendEmail();
}
Keep Interfaces Small and Focused
Small interfaces are easier to implement, understand, and maintain.
// Good
public interface IReader
{
string ReadData();
}
public interface IWriter
{
void WriteData(string data);
}
// Implementation can choose to implement both
public class FileHandler : IReader, IWriter
{
public string ReadData()
{
return File.ReadAllText("data.txt");
}
public void WriteData(string data)
{
File.WriteAllText("data.txt", data);
}
}
Design for Extension
Design interfaces so that they can be extended without breaking existing implementations.
// Version 1
public interface ILogger
{
void Log(string message);
}
// Version 2 - Extended without breaking existing implementations
public interface IAdvancedLogger : ILogger
{
void LogWithSeverity(string message, Severity severity);
}
Implementation Practices
Avoid Public Fields
Interfaces should only contain methods, properties, events, and indexers—never fields.
// Not valid - will cause compiler error
public interface IExample
{
int count; // Error: interfaces cannot contain fields
}
// Correct way
public interface IExample
{
int Count { get; set; }
}
Explicit vs. Implicit Implementation
Use implicit implementation for methods that are core to the class's public API. Use explicit implementation when implementing multiple interfaces with naming conflicts or when hiding implementation details.
// Implicit implementation
public class SimpleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
// Explicit implementation
public class MultiLogger : ILogger, IAuditLogger
{
// Explicitly implementing ILogger.Log
void ILogger.Log(string message)
{
Console.WriteLine($"Standard Log: {message}");
}
// Explicitly implementing IAuditLogger.Log
void IAuditLogger.Log(string message)
{
Console.WriteLine($"Audit Log: {message}");
}
// Public method that can call either implementation
public void LogMessage(string message, bool isAudit)
{
if (isAudit)
((IAuditLogger)this).Log(message);
else
((ILogger)this).Log(message);
}
}
Example usage of explicit implementation:
MultiLogger logger = new MultiLogger();
// logger.Log("Test"); // This would not compile
// You need to cast to the interface
((ILogger)logger).Log("This is a standard log");
((IAuditLogger)logger).Log("This is an audit log");
// Or use the public method
logger.LogMessage("This could be either", true);
Default Interface Methods (C# 8.0+)
In C# 8.0 and later, you can provide default implementations for interface members.
public interface ILogger
{
void Log(string message);
// Default implementation
void LogError(string message)
{
Log($"ERROR: {message}");
}
}
// Implementing class only needs to implement Log
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// LogError is inherited with its default implementation
}
Usage example:
ILogger logger = new ConsoleLogger();
logger.Log("Normal message"); // Output: Normal message
logger.LogError("Something bad happened"); // Output: ERROR: Something bad happened
Practical Examples
Repository Pattern
Interfaces are commonly used in the Repository pattern to abstract data access:
public interface ICustomerRepository
{
Customer GetById(int id);
IEnumerable<Customer> GetAll();
void Add(Customer customer);
void Update(Customer customer);
void Delete(int id);
}
public class SqlCustomerRepository : ICustomerRepository
{
private readonly string _connectionString;
public SqlCustomerRepository(string connectionString)
{
_connectionString = connectionString;
}
public Customer GetById(int id)
{
// Implementation using SQL Server
return new Customer { Id = id, Name = "Sample Customer" };
}
public IEnumerable<Customer> GetAll()
{
// Implementation using SQL Server
return new List<Customer>
{
new Customer { Id = 1, Name = "Customer 1" },
new Customer { Id = 2, Name = "Customer 2" }
};
}
public void Add(Customer customer)
{
// Implementation using SQL Server
Console.WriteLine($"Added customer: {customer.Name}");
}
public void Update(Customer customer)
{
// Implementation using SQL Server
Console.WriteLine($"Updated customer: {customer.Name}");
}
public void Delete(int id)
{
// Implementation using SQL Server
Console.WriteLine($"Deleted customer with ID: {id}");
}
}
Dependency Injection
Interfaces are a key component of dependency injection, enabling loose coupling:
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public class SmtpEmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
// Implementation using SMTP
Console.WriteLine($"Email sent to {to} with subject: {subject}");
}
}
public class OrderProcessor
{
private readonly IEmailService _emailService;
// Constructor injection
public OrderProcessor(IEmailService emailService)
{
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
// Process the order
// Send confirmation email
_emailService.SendEmail(
order.CustomerEmail,
"Order Confirmation",
$"Your order #{order.OrderId} has been processed successfully."
);
}
}
Example usage:
// Setup
IEmailService emailService = new SmtpEmailService();
OrderProcessor processor = new OrderProcessor(emailService);
// Process an order
var order = new Order
{
OrderId = "ORD12345",
CustomerEmail = "[email protected]",
// Other order details
};
processor.ProcessOrder(order);
// Output: Email sent to [email protected] with subject: Order Confirmation
When to Use Interfaces vs. Abstract Classes
Understanding when to use interfaces versus abstract classes is an important design decision:
Interface | Abstract Class |
---|---|
Multiple inheritance | Single inheritance only |
No implementation (except default methods in C# 8.0+) | Can provide implementation |
Defines a contract | Defines a base implementation |
No fields | Can have fields |
No constructor | Can have constructors |
Use when defining capabilities | Use when defining a base type |
Example of choosing between the two:
// Interface - defines a capability
public interface IPayable
{
decimal CalculatePayment();
}
// Abstract class - defines a base type with shared implementation
public abstract class Employee
{
public string Name { get; set; }
public string EmployeeId { get; set; }
protected Employee(string name, string employeeId)
{
Name = name;
EmployeeId = employeeId;
}
public abstract decimal CalculateMonthlySalary();
// Shared implementation
public string GetEmployeeDetails()
{
return $"{EmployeeId}: {Name}";
}
}
// Concrete implementation
public class FullTimeEmployee : Employee, IPayable
{
public decimal AnnualSalary { get; set; }
public FullTimeEmployee(string name, string employeeId, decimal annualSalary)
: base(name, employeeId)
{
AnnualSalary = annualSalary;
}
public override decimal CalculateMonthlySalary()
{
return AnnualSalary / 12;
}
public decimal CalculatePayment()
{
return CalculateMonthlySalary();
}
}
Summary
Following these interface best practices in C# will help you create more maintainable, flexible, and robust code:
- Use proper naming conventions (
IName
) - Keep interfaces small and focused (Interface Segregation Principle)
- Design interfaces for extension without breaking changes
- Choose between explicit and implicit implementation based on your needs
- Leverage default interface methods (C# 8.0+) where appropriate
- Understand when to use interfaces versus abstract classes
- Use interfaces to enable dependency injection and loose coupling
- Design interfaces around behavior rather than data
By applying these principles, you'll create code that's easier to test, maintain, and adapt as requirements change.
Additional Resources and Exercises
Resources
- Microsoft Documentation on Interfaces
- SOLID Principles in C#
- Design Patterns: Elements of Reusable Object-Oriented Software by the Gang of Four
Practice Exercises
-
Repository Interface: Create an interface called
IProductRepository
with methods for CRUD operations. Then implement this interface with two different classes:InMemoryProductRepository
andSqlProductRepository
. -
Service Pattern: Create an interface called
INotificationService
with methods for different notification types. Implement it with classes likeEmailNotificationService
andSmsNotificationService
. -
Interface Segregation: Refactor a large interface into smaller, more focused interfaces according to the Interface Segregation Principle.
-
Default Implementation: Create an interface with default method implementations (C# 8.0+) and explore how implementing classes can override or use the default behavior.
-
Strategy Pattern: Implement the Strategy design pattern using interfaces to define different algorithms that can be selected at runtime.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)