Overview
Understanding when to use interfaces versus abstract classes is crucial for designing flexible, maintainable systems. The choice impacts testability, extensibility, and code organization.
Core Concepts
Interfaces - Defining Contracts
// Interface with properties, methods, and events
public interface INotificationService
{
// Property
bool IsEnabled { get; }
// Method
Task SendAsync(string message, string recipient);
// Event
event EventHandler<NotificationSentEventArgs> NotificationSent;
}
// Multiple interface implementation
public interface ILoggable
{
void Log(string message);
}
public interface IDisposable
{
void Dispose();
}
public class EmailService : INotificationService, ILoggable, IDisposable
{
public bool IsEnabled { get; private set; } = true;
public event EventHandler<NotificationSentEventArgs> NotificationSent;
public async Task SendAsync(string message, string recipient)
{
// Send email
NotificationSent?.Invoke(this, new NotificationSentEventArgs(recipient));
}
public void Log(string message) => Console.WriteLine(message);
public void Dispose() => IsEnabled = false;
}
Abstract Classes - Shared Implementation
public abstract class PaymentProcessor
{
// Protected field - shared state
protected readonly ILogger _logger;
// Constructor - initialize shared dependencies
protected PaymentProcessor(ILogger logger)
{
_logger = logger;
}
// Template Method Pattern - defines algorithm structure
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
_logger.LogInformation("Starting payment processing");
// Shared validation
ValidateRequest(request);
// Abstract - derived classes implement
var result = await ExecutePaymentAsync(request);
// Shared logging
_logger.LogInformation("Payment completed: {Status}", result.Status);
return result;
}
// Concrete method - shared implementation
protected virtual void ValidateRequest(PaymentRequest request)
{
if (request.Amount <= 0)
throw new ArgumentException("Amount must be positive");
}
// Abstract method - must be implemented
protected abstract Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request);
}
public class CreditCardProcessor : PaymentProcessor
{
public CreditCardProcessor(ILogger logger) : base(logger) { }
protected override async Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request)
{
// Credit card specific implementation
_logger.LogInformation("Processing credit card payment");
// ... implementation
return new PaymentResult { Status = "Success" };
}
}
public class PayPalProcessor : PaymentProcessor
{
public PayPalProcessor(ILogger logger) : base(logger) { }
protected override async Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request)
{
// PayPal specific implementation
_logger.LogInformation("Processing PayPal payment");
// ... implementation
return new PaymentResult { Status = "Success" };
}
// Override validation for PayPal-specific rules
protected override void ValidateRequest(PaymentRequest request)
{
base.ValidateRequest(request);
if (string.IsNullOrEmpty(request.PayPalEmail))
throw new ArgumentException("PayPal email required");
}
}
Default Interface Methods (C# 8+)
public interface IConfigurable
{
// Abstract member - must be implemented
string GetConfiguration(string key);
// Default implementation - can be overridden
string GetConfigurationOrDefault(string key, string defaultValue)
{
var value = GetConfiguration(key);
return string.IsNullOrEmpty(value) ? defaultValue : value;
}
// Static member (C# 11+)
static string DefaultConfigPath => "appsettings.json";
}
public class AppConfiguration : IConfigurable
{
private readonly Dictionary<string, string> _config = new();
public string GetConfiguration(string key)
{
return _config.TryGetValue(key, out var value) ? value : null;
}
// Optional: override default implementation
// public string GetConfigurationOrDefault(string key, string defaultValue) { ... }
}
// Usage
IConfigurable config = new AppConfiguration();
var value = config.GetConfigurationOrDefault("Key", "Default");
Explicit Interface Implementation
public interface IReader
{
void Read();
}
public interface IWriter
{
void Write();
}
// Both interfaces might have conflicting members
public interface IFileReader
{
void Open();
}
public interface INetworkReader
{
void Open(); // Same name as IFileReader!
}
public class DataProcessor : IFileReader, INetworkReader
{
// Explicit implementation - resolves naming conflicts
void IFileReader.Open()
{
Console.WriteLine("Opening file");
}
void INetworkReader.Open()
{
Console.WriteLine("Opening network connection");
}
// Implicit implementation - default behavior
public void Open()
{
Console.WriteLine("Opening default");
}
}
// Usage
var processor = new DataProcessor();
processor.Open(); // "Opening default"
((IFileReader)processor).Open(); // "Opening file"
((INetworkReader)processor).Open(); // "Opening network connection"
Common Built-in Interfaces
// IDisposable - cleanup resources
public class DatabaseConnection : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (!_disposed)
{
// Cleanup
_disposed = true;
}
}
}
// IEnumerable<T> - enable foreach
public class CustomCollection : IEnumerable<int>
{
private readonly int[] _items = { 1, 2, 3 };
public IEnumerator<int> GetEnumerator()
{
foreach (var item in _items)
yield return item;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// IComparable<T> - enable sorting
public class Person : IComparable<Person>
{
public string Name { get; set; }
public int CompareTo(Person other)
{
return string.Compare(Name, other?.Name, StringComparison.Ordinal);
}
}
// IEquatable<T> - value equality
public class Point : IEquatable<Point>
{
public int X { get; set; }
public int Y { get; set; }
public bool Equals(Point other)
{
if (other is null) return false;
return X == other.X && Y == other.Y;
}
public override bool Equals(object obj) => Equals(obj as Point);
public override int GetHashCode() => HashCode.Combine(X, Y);
}
Interview Tips
Tip 1: Always mention that interfaces are about "what" (capabilities) while abstract classes are about "what" and partial "how" (shared behavior).
Tip 2: Know that interface is the go-to choice for dependency injection and unit testing because it enables mocking.
Tip 3: Be ready to discuss C# 8 default interface methods and how they change the interface vs abstract class decision.
Common Interview Questions
-
What is the main difference between an interface and an abstract class?
- Interface: Contract only, multiple inheritance, no state, members are public. Abstract class: Contract + implementation, single inheritance, can have state and constructors. Use interface for capabilities; abstract class for shared implementation among related classes.
-
When would you use an interface over an abstract class?
- Use interfaces when: defining capabilities that unrelated classes can implement, need multiple inheritance, designing for testability/mocking, creating loosely coupled systems. Use abstract classes when: sharing code among closely related classes, need protected members or state.
-
Can an abstract class have a constructor?
- Yes. Abstract class constructors initialize shared state and are called by derived class constructors via
base(). They ensure proper initialization of the base class portion.
- Yes. Abstract class constructors initialize shared state and are called by derived class constructors via
-
What are default interface methods and why were they added?
- C# 8 feature allowing interfaces to have method implementations. Added for: library evolution without breaking changes, reducing need for abstract base classes, traits-like composition. Derived classes can override them.
-
How do you implement multiple interfaces with the same method signature?
- Use explicit interface implementation:
void IInterface1.Method()andvoid IInterface2.Method(). Access specific implementation by casting to the interface type.
- Use explicit interface implementation:
-
Can you instantiate an interface or abstract class?
- No direct instantiation. Interfaces are instantiated through implementing classes. Abstract classes are instantiated through derived concrete classes. Both can be used as variable types for polymorphism.
-
What is the diamond problem and how does C# handle it?
- Multiple inheritance can cause ambiguity when two parent classes have the same method. C# avoids this by allowing only single class inheritance. Multiple interface inheritance is allowed; conflicts are resolved with explicit implementation.