Interfaces vs Abstract Classes
Both interfaces and abstract classes define contracts, but they serve different purposes and have different capabilities.
Quick Comparison
| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | Yes | No |
| Implementation | Default methods only (C# 8+) | Can have full implementation |
| Fields | No instance fields | Can have fields |
| Constructors | No | Yes |
| Access modifiers | Public by default | Any access modifier |
| Versioning | Adding members breaks implementers | Can add virtual members safely |
Interfaces
Interfaces define a contract - a set of methods that implementing classes must provide.
// Interface defines WHAT must be done
public interface IRepository<T>
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// Multiple interface inheritance is allowed
public interface IAuditable
{
DateTime CreatedAt { get; }
DateTime? ModifiedAt { get; }
string CreatedBy { get; }
}
public interface ISoftDeletable
{
bool IsDeleted { get; }
DateTime? DeletedAt { get; }
}
// A class can implement multiple interfaces
public class User : IAuditable, ISoftDeletable
{
public int Id { get; set; }
public string Name { get; set; }
// IAuditable implementation
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string CreatedBy { get; set; }
// ISoftDeletable implementation
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
Default Interface Methods (C# 8+)
public interface ILogger
{
void Log(string message);
// Default implementation - can be overridden
void LogWarning(string message) => Log($"WARNING: {message}");
void LogError(string message) => Log($"ERROR: {message}");
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
// Uses default LogWarning and LogError
}
public class FileLogger : ILogger
{
public void Log(string message) { /* Write to file */ }
// Override default implementation
public void LogError(string message)
{
Log($"!!! CRITICAL ERROR !!! {message}");
}
}
Abstract Classes
Abstract classes provide a base implementation that derived classes can extend.
// Abstract class defines WHAT + provides HOW (partially)
public abstract class BaseRepository<T> where T : class
{
// Protected state that derived classes can access
protected readonly DbContext _context;
protected readonly ILogger _logger;
// Constructor - abstract classes can have constructors
protected BaseRepository(DbContext context, ILogger logger)
{
_context = context;
_logger = logger;
}
// Abstract methods - MUST be implemented
public abstract Task<T> GetByIdAsync(int id);
// Virtual methods - CAN be overridden
public virtual async Task<List<T>> GetAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
// Concrete methods - shared implementation
public async Task AddAsync(T entity)
{
ValidateEntity(entity);
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
_logger.Log($"Added {typeof(T).Name}");
}
// Protected helper method
protected virtual void ValidateEntity(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
}
}
// Derived class provides specific implementations
public class ProductRepository : BaseRepository<Product>
{
public ProductRepository(DbContext context, ILogger logger)
: base(context, logger) { }
public override async Task<Product> GetByIdAsync(int id)
{
return await _context.Set<Product>()
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id);
}
public override async Task<List<Product>> GetAllAsync()
{
// Override to include related data
return await _context.Set<Product>()
.Include(p => p.Category)
.ToListAsync();
}
}
When to Use Each
Use Interfaces When:
- Defining a contract for multiple unrelated classes
- Multiple inheritance is needed
- Testing/mocking - interfaces are easier to mock
- API boundaries - defining public contracts
- Loose coupling - depending on abstractions
// Good use of interface - unrelated classes share behavior
public interface IExportable
{
byte[] Export(string format);
}
public class Report : IExportable
{
public byte[] Export(string format) { /* ... */ }
}
public class Invoice : IExportable
{
public byte[] Export(string format) { /* ... */ }
}
public class UserProfile : IExportable
{
public byte[] Export(string format) { /* ... */ }
}
Use Abstract Classes When:
- Sharing implementation across related classes
- Template Method pattern - define algorithm skeleton
- State sharing - derived classes need access to fields
- Versioning - can add members without breaking derived classes
- Constructor logic - need initialization in base class
// Good use of abstract class - shared behavior and state
public abstract class Document
{
protected string _content;
protected DateTime _createdAt;
protected Document()
{
_createdAt = DateTime.UtcNow;
}
// Template method - defines algorithm structure
public void Process()
{
Validate();
Transform();
Save();
}
protected abstract void Validate();
protected abstract void Transform();
protected virtual void Save()
{
// Default save implementation
File.WriteAllText($"doc_{_createdAt:yyyyMMdd}.txt", _content);
}
}
public class WordDocument : Document
{
protected override void Validate() { /* Word-specific validation */ }
protected override void Transform() { /* Word-specific transform */ }
}
public class PdfDocument : Document
{
protected override void Validate() { /* PDF-specific validation */ }
protected override void Transform() { /* PDF-specific transform */ }
}
Combining Both
Often the best design uses both:
// Interface for the contract
public interface INotificationService
{
Task SendAsync(string recipient, string message);
Task<bool> ValidateRecipientAsync(string recipient);
}
// Abstract class for shared implementation
public abstract class NotificationServiceBase : INotificationService
{
protected readonly ILogger _logger;
protected NotificationServiceBase(ILogger logger)
{
_logger = logger;
}
// Common implementation
public async Task SendAsync(string recipient, string message)
{
if (!await ValidateRecipientAsync(recipient))
{
_logger.LogWarning($"Invalid recipient: {recipient}");
return;
}
await SendInternalAsync(recipient, message);
_logger.Log($"Notification sent to {recipient}");
}
// Abstract - must be implemented
public abstract Task<bool> ValidateRecipientAsync(string recipient);
protected abstract Task SendInternalAsync(string recipient, string message);
}
// Concrete implementations
public class EmailNotificationService : NotificationServiceBase
{
public EmailNotificationService(ILogger logger) : base(logger) { }
public override async Task<bool> ValidateRecipientAsync(string recipient)
{
return recipient.Contains("@");
}
protected override async Task SendInternalAsync(string recipient, string message)
{
// Send email
}
}
public class SmsNotificationService : NotificationServiceBase
{
public SmsNotificationService(ILogger logger) : base(logger) { }
public override async Task<bool> ValidateRecipientAsync(string recipient)
{
return recipient.All(char.IsDigit) && recipient.Length >= 10;
}
protected override async Task SendInternalAsync(string recipient, string message)
{
// Send SMS
}
}
// Dependency injection uses the interface
services.AddScoped<INotificationService, EmailNotificationService>();
Interview Tips
Common Questions:
- βWhatβs the difference between an interface and an abstract class?β
- βWhen would you use one over the other?β
- βCan you have implementation in an interface?β
Key Points:
- Interface: Contract only, multiple inheritance, best for APIs
- Abstract class: Partial implementation, single inheritance, best for code sharing
- Interfaces are better for dependency injection and testing
- Abstract classes are better for template method and shared state
- C# 8+ interfaces can have default implementations
Design Guidelines:
- Prefer interfaces for external APIs
- Use abstract classes for internal hierarchies
- Donβt use abstract classes just to share code (use composition)
- Keep interfaces small and focused (ISP)