Previous Factory-vs-Abstract-Factory-Pattern Question Answer Home Page Next

SOLID Principles in C#

Mastering SOLID Principles in C#

Your guide to building maintainable, scalable, and testable .NET applications.

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.

Dependency Inversion Principle (DIP)

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

Previous Factory-vs-Abstract-Factory-Pattern Question Answer Home Page Next
*