TL;DR
- Composition in C# helps you follow all SOLID principles naturally.
- Use composition to break responsibilities into focused services and inject dependencies.
- Add new features by injecting new implementations, not modifying existing code.
- Rely on interfaces for flexibility, testability, and avoiding inheritance pitfalls.
- Composition leads to modular, maintainable, and robust C# applications.
You’ve probably heard that composition is better than inheritance, but have you noticed how it naturally makes your code follow SOLID principles? Let’s break down why choosing composition over inheritance leads to cleaner, more maintainable code without you even trying.
What We Mean by Composition
Instead of saying “a class IS-A something” (inheritance), composition says “a class HAS-A something.” Rather than extending base classes, you inject dependencies and delegate work to other objects.
// Inheritance approach
public class EmailNotificationService : NotificationService
{
// Inherits everything, good or bad
}
// Composition approach
public class NotificationManager
{
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
public NotificationManager(IEmailSender emailSender, ILogger logger)
{
_emailSender = emailSender;
_logger = logger;
}
}
With composition, your class focuses on its main job and delegates specialized work to injected services.
Single Responsibility: Each Class Has One Job
Inheritance often leads to bloated base classes that do too much. Composition forces you to break responsibilities into focused services:
public class OrderProcessor
{
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
private readonly INotificationService _notificationService;
public async Task ProcessOrder(Order order)
{
await _paymentService.ProcessPayment(order.Payment);
await _inventoryService.ReserveItems(order.Items);
await _notificationService.SendConfirmation(order.CustomerId);
}
}
OrderProcessor
coordinates the workflow, it doesn’t handle payments, inventory, or notifications itself. Each service has one clear responsibility.
Open/Closed: Extend Without Modifying
Need a new notification method? With composition, you just create a new implementation:
public interface INotificationService
{
Task SendNotification(string message, string recipient);
}
// Add SMS without touching existing code
public class SmsNotificationService : INotificationService
{
public Task SendNotification(string message, string recipient) =>
// SMS implementation
Task.CompletedTask;
}
Your OrderProcessor
doesn’t need to change, just inject the new service.
Liskov Substitution: Avoid Inheritance Surprises
Inheritance hierarchies often break when child classes change expected behavior. Composition sidesteps this by depending on stable interfaces:
// Instead of fragile inheritance chains
public interface IReportGenerator
{
byte[] GenerateReport(ReportData data);
}
public class ReportService
{
private readonly IReportGenerator _generator;
// Works with any generator that follows the contract
public byte[] CreateReport(ReportData data) => _generator.GenerateReport(data);
}
No surprise behaviors, just predictable interfaces.
Interface Segregation: Small, Focused Contracts
Composition naturally leads to smaller, more focused interfaces. Instead of one fat interface, you inject multiple specialized ones:
public class UserService
{
private readonly IUserValidator _validator;
private readonly IPasswordHasher _hasher;
private readonly IUserRepository _repository;
// Each dependency has a specific, narrow purpose
}
Each service only depends on the interfaces it actually needs.
Dependency Inversion: Depend on Abstractions
Composition makes dependency inversion automatic. Your high-level classes depend on abstractions (interfaces), not concrete implementations:
public class CustomerService
{
private readonly ICustomerRepository _repository; // Abstraction
private readonly IEmailValidator _validator; // Abstraction
// High-level policy doesn't depend on low-level details
}
The CustomerService
doesn’t care if data comes from SQL Server, MongoDB, or a web API, it just depends on ICustomerRepository
.
How Composition Helps Each SOLID Principle
Principle | How Composition Helps |
---|---|
Single Responsibility | Each composed service has one focused job |
Open/Closed | Add new behaviors by injecting new implementations |
Liskov Substitution | Avoid inheritance pitfalls with stable interface contracts |
Interface Segregation | Inject multiple small, focused interfaces instead of fat ones |
Dependency Inversion | High-level classes depend on injected abstractions, not concrete types |
The Takeaway
Composition keeps your code like LEGO bricks, easy to rearrange, test, and extend. Instead of fighting inheritance hierarchies and trying to force SOLID compliance, composition makes it natural. Your classes become coordinators that delegate work to specialized services, and SOLID principles just happen.
Next time you’re tempted to create a base class, ask yourself: “Could I inject this behavior instead?” Your future self will thank you.
Frequently Asked Questions
How does composition support the SOLID principles in C#?
What is the difference between composition and inheritance?
How does composition help with the Single Responsibility Principle?
How does composition enable the Open/Closed Principle?
How does composition avoid Liskov Substitution Principle violations?
How does composition relate to the Interface Segregation Principle?
How does composition support the Dependency Inversion Principle?
Can you give a real-world example of composition in C#?
Yes. For example:
public class OrderProcessor
{
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
public OrderProcessor(IPaymentService paymentService, IInventoryService inventoryService)
{
_paymentService = paymentService;
_inventoryService = inventoryService;
}
}
Here, OrderProcessor
delegates payment and inventory logic to injected services.
Why is composition preferred over inheritance for maintainable code?
How does composition improve testability in C# applications?
See other oops posts
- Composition Over Inheritance in C#: Write Flexible, Maintainable Code
- DIP vs DI vs IoC: Understanding Key Software Design Concepts
- Cohesion vs Coupling in Object-Oriented Programming: A Complete Guide
- Encapsulation and Information Hiding in C#: Best Practices and Real-World Examples
- Object-Oriented Programming: Core Principles and C# Implementation
- How Polymorphism Makes C# Code Flexible: Real-World Examples and Best Practices
- SOLID Principles in C#: A Practical Guide with Real‑World Examples