Learnixo

SOLID Principles in C# · Lesson 6 of 6

SOLID in Real .NET Projects — Violations and Fixes

Recognizing All Five Violations at Once

C#
// This single class violates all five SOLID principles
public class PatientManager
{
    private ApplicationDbContext _db = new ApplicationDbContext();

    // SRP: handles CRUD, reporting, notifications, and scheduling — too many responsibilities
    public void SavePatient(Patient p) { _db.Patients.Add(p); _db.SaveChanges(); }

    public byte[] GeneratePatientReport(Patient p)  // report logic in "manager"?
    {
        if (reportType == "PDF")   return GeneratePdf(p);   // OCP: new type = modify this
        else if (reportType == "Excel") return GenerateExcel(p);
        throw new NotSupportedException();
    }

    public void Send(string channel, string message)  // ISP: single method for all channels
    {
        if (channel == "SMS")  { /* SMS */ }
        if (channel == "Fax")  { throw new NotImplementedException(); }  // LSP violation
    }

    // DIP: concrete dependencies, not abstractions
}

Refactoring Step by Step

Step 1 — Apply SRP: Split the Class

C#
// PatientRepository: only persistence
// PatientReportService: only reporting
// PatientAlertService: only notifications
// PatientScheduler: only scheduling

// Each class has one reason to change:
//   PatientRepository: if the database changes
//   PatientReportService: if the report format changes
//   PatientAlertService: if the notification channel changes

Step 2 — Apply OCP: Extension Points for Varying Behavior

C#
// Report formats are extension points — each is a separate class
public interface IReportGenerator { byte[] Generate(Patient patient); }
public sealed class PdfReportGenerator  : IReportGenerator { /* ... */ }
public sealed class ExcelReportGenerator : IReportGenerator { /* ... */ }
// Adding HTML: new class, no existing code changes

Step 3 — Apply LSP: No NotImplementedException

C#
// Fax channel either fully implements IAlertChannel or is removed
// If Fax cannot send async alerts, it should not implement IAlertChannel
// Consider: ISyncAlertChannel separate interface for fax

Step 4 — Apply ISP: Split IAlertChannel by Role

C#
public interface IInstantAlert { Task SendAsync(PatientAlert alert, CancellationToken ct); }
public interface IScheduledAlert { Task ScheduleAsync(PatientAlert alert, DateTime sendAt, CancellationToken ct); }
// SMS implements IInstantAlert only — no ScheduleAsync
// Email implements both

Step 5 — Apply DIP: Inject Abstractions

C#
public sealed class PatientAlertService
{
    private readonly IEnumerable<IInstantAlert> _channels;
    private readonly IClock _clock;

    public PatientAlertService(IEnumerable<IInstantAlert> channels, IClock clock)
    {
        _channels = channels;
        _clock    = clock;
    }
}

The SOLID Trade-off Analysis

SOLID benefits:
  ✓ Code is easier to test (DIP + SRP)
  ✓ Features are easier to add (OCP)
  ✓ Interfaces are simpler (ISP)
  ✓ Substitutions work correctly (LSP)

SOLID costs:
  ✗ More files, more interfaces, more indirection
  ✗ Takes time to design correctly upfront
  ✗ Overkill for simple, short-lived scripts
  ✗ Can lead to over-engineering if applied blindly

Apply SOLID where:
  → The code is expected to change (new business rules, new channels, new formats)
  → Tests are written and rely on the design
  → Multiple developers work in the same area

Skip SOLID where:
  → Simple CRUD with no expected variation
  → Throwaway scripts and one-off utilities
  → Early prototyping where requirements are unknown

Red Flag / Green Answer

Red Flag: "We apply SOLID to everything — every class has an interface, every behavior has a strategy pattern, every rule has a specification. It's clean code."

Over-applying SOLID produces excessive indirection: a CreatePatientCommandHandler that calls IPatientFactory, which calls IPatientMrnValidator, which calls IMrnFormatPolicy. Reading code requires jumping through 6 abstractions to understand "create a patient." Clean code is readable and proportional to the problem. Apply SOLID to the areas where flexibility and testability are genuinely needed, not everywhere.

Green Answer:

We apply SRP when a class has multiple reasons to change. We apply OCP when we identify a recurring extension pattern (new notification channel, new report format). We apply LSP when we catch NotImplementedException in method stubs. We apply ISP when interfaces grow beyond 5-6 methods. We apply DIP when a class constructs its own dependencies or uses DateTime.UtcNow directly. SOLID is a guide, not a mandate.


Practical Checklist

Before writing a class, ask:
  □ What is this class responsible for? (SRP)
  □ Where will I need to add new behavior later? (OCP)
  □ Does every implementation honor the contract? (LSP)
  □ Does this interface have unused methods in some implementations? (ISP)
  □ Does this class create its own dependencies? (DIP)

Code review signals:
  ✗ Class has >200 lines: likely SRP violation
  ✗ if/switch on type or format: likely OCP violation
  ✗ NotImplementedException in a method: likely LSP + ISP violation
  ✗ Interface has >8 methods: likely ISP violation
  ✗ new ConcreteService() inside a class body: likely DIP violation

Key Takeaway

SOLID principles work together: SRP limits class size, OCP uses the extension points SRP creates, LSP makes those extension points reliable, ISP keeps them focused, and DIP makes them injectable and testable. Violations cluster: an SRP violation often breaks OCP (one large class does too many things in an if/else chain). Fixing SRP often also fixes OCP. Apply SOLID proportionally — to code that changes frequently, not to everything.