Understanding SOLID Principles in .NET: Write Better, Scalable, and Maintainable Code
The SOLID principles are the cornerstone of good software design. Whether you're building small applications or large enterprise systems in .NET, applying these principles can help you write code that is easier to understand, maintain, and extend.
In this article, we'll explore each of the SOLID principles with practical C# examples in the .NET ecosystem.
🧱 What is SOLID?
SOLID is an acronym for five object-oriented design principles:
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
These principles were introduced by Robert C. Martin and have become a standard for writing clean and maintainable software.
1. Single Responsibility Principle (SRP)
"A class should have only one reason to change."
This means that a class should only have one job or responsibility.
❌ Bad Example
public class Invoice
{
public void CalculateTotal() { /* ... */ }
public void SaveToDatabase() { /* ... */ }
public void PrintInvoice() { /* ... */ }
}
Here, Invoice
is doing too much — it calculates, persists data, and handles printing.
✅ Good Example
public class InvoiceCalculator
{
public void CalculateTotal() { /* ... */ }
}
public class InvoiceRepository
{
public void SaveToDatabase() { /* ... */ }
}
public class InvoicePrinter
{
public void PrintInvoice() { /* ... */ }
}
Each class now has a single responsibility.
2. Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
This principle promotes the use of abstractions so that the behavior of a module can be extended without modifying its source code.
❌ Bad Example
public class Discount
{
public double CalculateDiscount(string customerType)
{
if (customerType == "Regular") return 0.1;
if (customerType == "Premium") return 0.2;
return 0;
}
}
✅ Good Example
public interface IDiscountStrategy
{
double GetDiscount();
}
public class RegularCustomerDiscount : IDiscountStrategy
{
public double GetDiscount() => 0.1;
}
public class PremiumCustomerDiscount : IDiscountStrategy
{
public double GetDiscount() => 0.2;
}
Now you can add new strategies without modifying existing code.
3. Liskov Substitution Principle (LSP)
"Derived classes must be substitutable for their base classes."
This principle asserts that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program. In simpler terms, subclasses should enhance, not weaken or break, the behavior promised by the base class.
Violating LSP often introduces unexpected behavior, especially in polymorphic code, and can lead to fragile systems.
❌ Bad Example
public class Bird
{
public virtual void Fly() { /* default flying logic */ }
}
public class Ostrich : Bird
{
public override void Fly()
{
throw new NotImplementedException();
}
}
In this example, Ostrich
inherits from Bird
and overrides the Fly
method by throwing an exception. This violates LSP because any code that expects a Bird
to fly safely might crash or behave incorrectly when passed an Ostrich
.
✅ Better Design
public interface IBird { }
public interface IFlyingBird : IBird
{
void Fly();
}
public class Sparrow : IFlyingBird
{
public void Fly() { /* flying logic */ }
}
public class Ostrich : IBird
{
// No Fly method; ostriches do not fly
}
This approach respects LSP by not requiring Ostrich
to implement functionality it does not possess. Clients depending on IFlyingBird
know all implementations can fly, while those dealing with general IBird
don't make assumptions about flying.
🧠 Key Takeaways
- Avoid forcing subclasses to override or disable base class functionality.
- Design with appropriate abstractions that reflect real capabilities.
- Ensure derived classes honor the expectations set by the base class.
By following LSP, you ensure that components remain interchangeable, reducing bugs and making your code more robust and extensible.
4. Interface Segregation Principle (ISP)
"Clients should not be forced to depend on interfaces they do not use."
❌ Bad Example
public interface IWorker
{
void Work();
void Eat();
}
public class Robot : IWorker
{
public void Work() { /* ... */ }
public void Eat() => throw new NotImplementedException();
}
✅ Good Example
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public class Human : IWorkable, IEatable
{
public void Work() { /* ... */ }
public void Eat() { /* ... */ }
}
public class Robot : IWorkable
{
public void Work() { /* ... */ }
}
Separate interfaces make code more flexible.
5. Dependency Inversion Principle (DIP)
"Depend on abstractions, not on concretions."
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Bad Example
public class EmailService
{
public void SendEmail(string message) { /* ... */ }
}
public class Notification
{
private EmailService _emailService = new EmailService();
public void Send(string message)
{
_emailService.SendEmail(message);
}
}
✅ Good Example
public interface IMessageService
{
void Send(string message);
}
public class EmailService : IMessageService
{
public void Send(string message) { /* ... */ }
}
public class Notification
{
private readonly IMessageService _messageService;
public Notification(IMessageService messageService)
{
_messageService = messageService;
}
public void Notify(string message)
{
_messageService.Send(message);
}
}
Now Notification
is not tightly coupled to EmailService
.
✅ Final Thoughts
Applying the SOLID principles in .NET helps you build:
- Modular and testable components
- Easier-to-understand codebases
- Scalable applications ready for change
While not every class needs to follow all principles strictly, being mindful of SOLID will gradually elevate the quality of your code.
If you found this article helpful follow me on GitHub, Twitter and Bluesky for more content.
Thanks for reading!
Top comments (0)