SOLID Principles in C#: Clean Code Made Easy
Writing maintainable, flexible, and scalable software is every developer’s goal. The SOLID principles are a cornerstone of object-oriented programming (OOP) that help achieve this.
Here’s a breakdown with bad vs good examples, pros, and cons.
1. Single Responsibility Principle (SRP)
Definition:
A class should have only one reason to change — it should handle a single responsibility.
Bad Example:
public class InvoiceProcessor
{
public void GenerateInvoice(Invoice invoice) { /* generate logic */ }
public void SaveInvoiceToDatabase(Invoice invoice) { /* save logic */ }
public void SendInvoiceEmail(Invoice invoice) { /* email logic */ }
}
Good Example:
public class InvoiceGenerator
{
public void Generate(Invoice invoice) { /* generate logic */ }
}
public class InvoiceRepository
{
public void Save(Invoice invoice) { /* save logic */ }
}
public class InvoiceMailer
{
public void Send(Invoice invoice) { /* email logic */ }
}
Pros:
- Clear code responsibility
- Easier to maintain and test
- Changes in one area don’t affect others
Cons:
- More classes to manage
- Slightly more complex initial design
2. Open/Closed Principle (OCP)
Definition:
Software entities should be open for extension but closed for modification.
Bad Example:
public class DiscountCalculator
{
public double CalculateDiscount(Customer customer)
{
if (customer.Type == "VIP") return 0.2;
if (customer.Type == "Regular") return 0.1;
return 0;
}
}
Good Example:
public interface IDiscountStrategy
{
double GetDiscount();
}
public class VipDiscount : IDiscountStrategy { public double GetDiscount() => 0.2; }
public class RegularDiscount : IDiscountStrategy { public double GetDiscount() => 0.1; }
public class DiscountCalculator
{
public double CalculateDiscount(IDiscountStrategy strategy) => strategy.GetDiscount();
}
Pros:
- Easy to add new features
- Reduces risk of breaking existing code
Cons:
- More upfront design and interfaces
- Slightly more complex for small projects
3. Liskov Substitution Principle (LSP)
Definition:
Objects of a subclass should be replaceable with objects of the parent class without breaking functionality.
Bad Example:
public class Bird { public virtual void Fly() { } }
public class Ostrich : Bird { public override void Fly() { throw new NotImplementedException(); } }
Good Example:
public interface IFlyable { void Fly(); }
public class Sparrow : IFlyable { public void Fly() { /* flying logic */ } }
public class Ostrich { /* no Fly method since it cannot fly */ }
Pros:
- Correct abstraction
- Avoids runtime errors
Cons:
- Requires careful interface design
- Can lead to more interfaces
4. Interface Segregation Principle (ISP)
Definition:
No client should be forced to depend on methods it does not use.
Bad Example:
public interface IMachine
{
void Print();
void Scan();
void Fax();
}
public class OldPrinter : IMachine
{
public void Print() { /* ok */ }
public void Scan() { throw new NotImplementedException(); }
public void Fax() { throw new NotImplementedException(); }
}
Good Example:
public interface IPrinter { void Print(); }
public interface IScanner { void Scan(); }
public interface IFax { void Fax(); }
public class OldPrinter : IPrinter { public void Print() { /* ok */ } }
Pros:
- Cleaner and focused interfaces
- Easier to maintain and implement
Cons:
- More interfaces to track
- Slightly higher design effort
5. Dependency Inversion Principle (DIP)
Definition:
High-level modules should not depend on low-level modules; both should depend on abstractions.
Bad Example:
public class FileLogger { public void Log(string message) { } }
public class UserService
{
private FileLogger _logger = new FileLogger();
public void CreateUser(string name) { _logger.Log("User created: " + name); }
}
Good Example:
public interface ILogger { void Log(string message); }
public class FileLogger : ILogger { public void Log(string message) { } }
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger) { _logger = logger; }
public void CreateUser(string name) { _logger.Log("User created: " + name); }
}
Pros:
- Promotes decoupling
- Easy to swap implementations
- Improves testability
Cons:
- Requires dependency injection setup
- Slightly more boilerplate code
How Good or Bad is SOLID in Real Projects?
SOLID principles are widely regarded as best practices for writing maintainable, flexible, and testable code. They help developers design systems that can adapt to change without breaking existing functionality, making long-term maintenance much easier. Projects that follow SOLID tend to have clearer responsibilities, decoupled modules, and more reusable components.
However, SOLID is not a silver bullet. In small projects or simple scripts, strictly following SOLID can lead to unnecessarily complex designs, with many classes and interfaces that add overhead without real benefit. Additionally, improper application—like over-abstraction or creating too many tiny interfaces—can make the code harder to understand for new developers.
In practice, SOLID works best when applied thoughtfully, balancing clean design with pragmatism. It’s a guideline, not a strict rulebook—developers should adapt the principles to the size and complexity of the project rather than following them dogmatically.