As a developer, writing code that just works is never enough. Your code needs to be maintainable, testable, and scalable. This is where the SOLID principles of object-oriented design become essential.
SOLID is an acronym for,
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
I’ll try my best to guide you through each SOLID principle with real-world scenarios and hands-on C# examples.
Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change.
Imagine a reporting feature:
public class ReportService { public string GenerateReport() { // logic to generate report return "Report Data"; } public void SaveReport(string data) { File.WriteAllText("report.txt", data); } }
This class does two things: generates and saves reports. If saving logic changes, this class changes. That violates SRP.
Refactored:
public class ReportGenerator { public string GenerateReport() => "Report Data"; } public class ReportSaver { public void SaveReport(string data) => File.WriteAllText("report.txt", data); }
Now each class has a single responsibility.
Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
You have a payment system:
public class PaymentService { public void ProcessPayment(string type) { if (type == "CreditCard") Console.WriteLine("Processing Credit Card"); else if (type == "PayPal") Console.WriteLine("Processing PayPal"); } }
Adding new types means modifying this class. That breaks OCP.
Refactored with Strategy Pattern:
public interface IPaymentProcessor { void Process(); } public class CreditCardProcessor : IPaymentProcessor { public void Process() => Console.WriteLine("Processing Credit Card"); } public class PayPalProcessor : IPaymentProcessor { public void Process() => Console.WriteLine("Processing PayPal"); } public class PaymentService { private readonly IPaymentProcessor _processor; public PaymentService(IPaymentProcessor processor) => _processor = processor; public void ProcessPayment() => _processor.Process(); }
Now new payment types are just new classes.
Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without breaking behavior.
public class Bird { public virtual void Fly() => Console.WriteLine("Flying"); } public class Ostrich : Bird { public override void Fly() => throw new NotImplementedException(); }
This violates LSP. Ostrich
cannot be used where Bird
is expected.
Fix:
public interface IBird { void Move(); } public class Sparrow : IBird { public void Move() => Console.WriteLine("Flying"); } public class Ostrich : IBird { public void Move() => Console.WriteLine("Running"); }
Now each bird moves as it naturally should.
Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they do not use.
public interface IWorker { void Work(); void Eat(); } public class Robot : IWorker { public void Work() => Console.WriteLine("Working"); public void Eat() => throw new NotImplementedException(); }
Robots don’t eat. This is wrong.
Refactored:
public interface IWorkable { void Work(); } public interface IEatable { void Eat(); } public class Human : IWorkable, IEatable { public void Work() => Console.WriteLine("Working"); public void Eat() => Console.WriteLine("Eating"); } public class Robot : IWorkable { public void Work() => Console.WriteLine("Working"); }
Each class implements only what it needs.
Dependency Inversion Principle (DIP)
Definition: Depend on abstractions, not concretions.
public class EmailService { public void SendEmail(string to, string msg) => Console.WriteLine("Sending Email"); } public class Notification { private EmailService _emailService = new EmailService(); public void Notify(string msg) => _emailService.SendEmail("admin@example.com", msg); }
Tightly coupled. Hard to test or switch services.
Refactored:
public interface IMessageService { void Send(string to, string message); } public class EmailService : IMessageService { public void Send(string to, string message) => Console.WriteLine("Sending Email"); } public class Notification { private readonly IMessageService _messageService; public Notification(IMessageService messageService) => _messageService = messageService; public void Notify(string msg) => _messageService.Send("admin@example.com", msg); }
Now the Notification class is decoupled from the specific implementation.
Finally,
The SOLID principles are more than theory. They’re practical, powerful tools that will make your real-life projects cleaner, more adaptable, and easier to maintain. Each principle helps you avoid pitfalls that can make your code brittle and hard to change.
The best time to adopt SOLID was yesterday. The second-best time is now.
Happy coding!