If you've ever worked on a project that started off simple but quickly became a maintenance nightmare, you’ve likely dealt with rigid, tightly coupled, and hard-to-scale code.
The SOLID principles exist to prevent exactly that. They help you write code that’s more organized, flexible, and easier to maintain. Let’s go through them with clear explanations and practical examples!
đź“Ś S - Single Responsibility Principle (SRP)
"A class should have only one reason to change."
Imagine a class that does everything: processes orders, generates invoices, and sends emails.
public class OrderService
{
public void ProcessOrder() { /* logic */ }
public void GenerateInvoice() { /* logic */ }
public void SendEmail() { /* logic */ }
}
❌ The problem? Any change in the invoice logic could unintentionally impact email sending.
âś… Solution: Separate responsibilities:
public class OrderProcessor { /* Processes the order */ }
public class InvoiceService { /* Generates invoices */ }
public class EmailService { /* Sends emails */ }
Now, each class has a single responsibility, making maintenance much easier.
đź“Ś O - Open/Closed Principle (OCP)
"Code should be open for extension but closed for modification."
Imagine a system that calculates discounts for different customers:
public class Order
{
public decimal CalculateDiscount(string customerType)
{
if (customerType == "Premium") return 0.2m;
if (customerType == "Regular") return 0.1m;
return 0m;
}
}
❌ The problem? Adding a new customer type requires modifying this class, breaking OCP.
âś… Solution: Use an extensible structure:
public interface IDiscount { decimal Apply(); }
public class PremiumDiscount : IDiscount { public decimal Apply() => 0.2m; }
public class RegularDiscount : IDiscount { public decimal Apply() => 0.1m; }
public class Order
{
private readonly IDiscount _discount;
public Order(IDiscount discount) { _discount = discount; }
public decimal CalculateDiscount() => _discount.Apply();
}
Now, we can add new discount types without modifying existing code.
đź“Ś L - Liskov Substitution Principle (LSP)
"Subclass objects should be replaceable with superclass objects without breaking the code."
Let’s say we have a Bird
class defining common behaviors:
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("The bird is flying!");
}
}
Now, we want to add a penguin, but it can’t fly! If we override Fly()
in a Penguin
subclass, we get an issue:
public class Penguin : Bird
{
public override void Fly()
{
throw new NotImplementedException("Penguins don't fly!");
}
}
❌ The problem? If a function expects a Bird
but gets a Penguin
, calling Fly()
could crash the application.
âś… Solution: Restructure the hierarchy:
public abstract class Bird { }
public interface IFlyable
{
void Fly();
}
public class Swallow : Bird, IFlyable
{
public void Fly() => Console.WriteLine("The swallow is flying!");
}
public class Penguin : Bird
{
public void Swim() => Console.WriteLine("The penguin is swimming!");
}
Now, only birds that actually fly implement IFlyable
, ensuring correct behavior.
đź“Ś I - Interface Segregation Principle (ISP)
"A class should not be forced to implement methods it doesn’t use."
Suppose we have an interface IEmployeeActions
that forces all classes to implement unnecessary methods:
public interface IEmployeeActions
{
void Work();
void Manage();
}
public class Developer : IEmployeeActions
{
public void Work() { /* logic */ }
public void Manage() { throw new NotImplementedException(); }
}
❌ The problem? A developer doesn’t manage, yet it’s forced to implement Manage()
.
âś… Solution: Create smaller, more specific interfaces:
public interface IWork { void Work(); }
public interface IManage { void Manage(); }
public class Developer : IWork
{
public void Work() { /* logic */ }
}
Now, each class implements only what it actually needs.
đź“Ś D - Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Suppose an order service directly depends on a payment service:
public class OrderService
{
private readonly PaymentService _paymentService = new PaymentService();
public void ProcessOrder() { _paymentService.ProcessPayment(); }
}
❌ The problem? If PaymentService
changes, OrderService might break.
âś… Solution: Use dependency injection and abstractions:
public interface IPaymentService { void ProcessPayment(); }
public class OrderService
{
private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public void ProcessOrder() { _paymentService.ProcessPayment(); }
}
Now, we can swap payment service implementations without modifying OrderService
.
🎯 Conclusion
SOLID principles aren’t just theoretical—they help create flexible and maintainable code. But remember: not everything needs to be over-abstracted. The key is to apply SOLID wisely, keeping code clean without overcomplicating it.
And you? Have you ever dealt with a project that completely ignored SOLID? Share your experience in the comments! 🚀
Top comments (0)