| Factory-vs-Abstract-Factory-Pattern :👈 | 👉:Singleton Pattern |
SOLID Principles in C# |
Mastering SOLID Principles in C#
Your guide to building maintainable, scalable, and testable .NET applications.
- Introduction
- SRP
- OCP
- LSP
- ISP
- DIP
- Pitfalls
- Cheat Sheet
- Case Study
- Design Patterns
- Automated Testing
- Resources
Introduction
SOLID is an acronym for five design principles that help you write cleaner, more robust object-oriented code. Coined by Robert C. Martin, these guidelines guard against rigid, fragile, and tightly coupled designs.
Mnemonic for SOLID:
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
1. Single Responsibility Principle (SRP)
A class should have one, and only one, reason to change. Grouping unrelated responsibilities leads to complex classes that break whenever any feature changes.
Example: Invoice Processing
// Bad: One class does everything
public class InvoiceManager
{
public void Print(Invoice inv) { /*...*/ }
public void Save(Invoice inv) { /*...*/ }
}
// Good: Separate responsibilities
public class InvoicePrinter
{
public void Print(Invoice inv) { /* print logic */ }
}
public class InvoiceRepository
{
public void Save(Invoice inv) { /* db logic */ }
}
SRP often leads to more classes but each with a laser-focused role. This improves testability and reduces merge conflicts.
2. Open/Closed Principle (OCP)
Entities should be open for extension, but closed for modification. You should be able to add new behavior without touching existing code.
Example: Discount Strategies
// Abstract base
public abstract class DiscountStrategy
{
public abstract decimal Apply(decimal total);
}
// No discount
public class NoDiscount : DiscountStrategy
{
public override decimal Apply(decimal total) => total;
}
// 10% off
public class TenPercentDiscount : DiscountStrategy
{
public override decimal Apply(decimal total) => total * 0.9m;
}
// New strategies? Just extend DiscountStrategy.
3. Liskov Substitution Principle (LSP)
Derived types must be substitutable for their base types. If a subclass breaks behavior, clients expecting the base type will fail.
Anti-Example: Birds
// Violates LSP
public class Bird
{
public virtual void Fly() { /*...*/ }
}
public class Ostrich : Bird
{
public override void Fly() =>
throw new NotSupportedException();
}
Refactored: Separate Hierarchies
public abstract class Bird { }
public class FlyingBird : Bird
{
public void Fly() { /*...*/ }
}
public class Ostrich : Bird
{
// No Fly method needed
}
4. Interface Segregation Principle (ISP)
No client should depend on methods it does not use. Large interfaces force classes to implement irrelevant members.
Example: Multi-Function Devices
// Segregated interfaces
public interface IPrinter
{
void Print(Document doc);
}
public interface IScanner
{
void Scan(Document doc);
}
// Implement only what you need
public class SimplePrinter : IPrinter
{
public void Print(Document doc) { /*...*/ }
}
public class AllInOne : IPrinter, IScanner
{
public void Print(Document doc) { /*...*/ }
public void Scan(Document doc) { /*...*/ }
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Example: Notification Service
// Abstraction
public interface IMessageSender
{
void Send(string message);
}
// Low-level
public class EmailSender : IMessageSender
{
public void Send(string message) =>
Console.WriteLine("Email: " + message);
}
// High-level
public class NotificationService
{
private readonly IMessageSender _sender;
public NotificationService(IMessageSender sender) =>
_sender = sender;
public void Notify(string msg) =>
_sender.Send(msg);
}
Common Pitfalls & Anti-Patterns
- Over-engineering: Don’t force SOLID everywhere—apply pragmatically.
- God Objects: Violates SRP; grows uncontrolled.
- Fat Interfaces: Violates ISP; bloats implementations.
- Switch Statements for types: Violates OCP; better with polymorphism.
Quick Cheat Sheet
| Principle | Key Idea | C# Example |
|---|---|---|
| SRP | One class, one responsibility | InvoicePrinter, InvoiceRepository |
| OCP | Extend, don’t modify | DiscountStrategy subtypes |
| LSP | Subtypes must behave like their base | Separate FlyingBird vs Ostrich |
| ISP | Many small interfaces | IPrinter, IScanner |
| DIP | Depend on abstractions | IMessageSender + DI |
Real-World Case Study
Imagine a retail application where pricing, tax calculations, and shipping vary by region. Applying SOLID lets you isolate each concern:
- SRP: One class for pricing rules, one for tax, one for shipping.
- OCP: New regional rules implemented via inheriting base rule classes.
- DIP: Checkout service depends on IPricingRule, ITaxCalculator abstractions.
Integrating SOLID with Design Patterns
Many GoF patterns complement SOLID:
- Strategy (OCP): Swap algorithms at runtime by passing different strategy objects.
- Factory Method (DIP): Create instances via abstract factories to decouple construction.
- Decorator (SRP/OCP): Extend behavior without modifying original classes.
Automated Testing with SOLID
SOLID code is inherently testable:
- SRP: Each class has a small, focused surface—easy to mock dependencies.
- DIP: Depend on interfaces, so inject fakes or mocks for unit tests.
// Example: mocking IMessageSender
var mockSender = new Mock();
var service = new NotificationService(mockSender.Object);
service.Notify("Hello");
mockSender.Verify(m => m.Send("Hello"), Times.Once);
Further Reading & Resources
| Factory-vs-Abstract-Factory-Pattern :👈 | 👉:Singleton Pattern |
Comments and Discussion