Skip to main content
CodeOath
← All posts

Architecture & Patterns110 min total · 26 parts

ACID, SOLID, and Design Patterns: A Complete Software Design Reference

Part 12 of 26 · ~2 min

SOLID Applied: Refactoring a Violation-Riddled Class

Reading a single principle's example in isolation only goes so far. Watching several of them break down together, in one plausible class, and then watching the same fix resolve all of them at once, is what actually makes the relationships between these principles sink in.

// Before: violates SRP (pulling data + composing a message + sending it, all
// in one class), OCP (a channel switch that grows for every new notification
// method), and DIP (a concrete SqlConnection constructed inside the class).
public class ShiftDigestService
{
    private readonly SqlConnection _connection = new SqlConnection("...");

    public void SendWeeklyDigest(int volunteerId, string channel)
    {
        var shifts = _connection.Query<ShiftAssignment>(
            "SELECT * FROM ShiftAssignments WHERE VolunteerId = @id AND WeekOf = @week",
            new { id = volunteerId, week = DateTime.Today }).ToList();
        var digest = $"You have {shifts.Count} shift(s) this week.";

        if (channel == "sms") SmsGateway.Send("+15551234567", digest);
        else if (channel == "push") PushGateway.Send(volunteerId, digest);
        // adding "email" means editing this method again
    }
}
// After: each responsibility is its own class (SRP); new channels are added
// by implementing IShiftNotifier, not editing existing code (OCP); everything
// depends on interfaces, supplied from outside (DIP).
public interface IShiftRepository { List<ShiftAssignment> GetForWeek(int volunteerId, DateTime weekStart); }
public interface IShiftNotifier { void Send(string message); }

public class SqlShiftRepository : IShiftRepository
{
    private readonly SqlConnection _connection;
    public SqlShiftRepository(SqlConnection connection) => _connection = connection;
    public List<ShiftAssignment> GetForWeek(int volunteerId, DateTime weekStart) =>
        _connection.Query<ShiftAssignment>(
            "SELECT * FROM ShiftAssignments WHERE VolunteerId = @id AND WeekOf = @week",
            new { id = volunteerId, week = weekStart }).ToList();
}

public class SmsShiftNotifier : IShiftNotifier
{
    public void Send(string message) => SmsGateway.Send("+15551234567", message);
}

public class EmailShiftNotifier : IShiftNotifier // added later — no existing class touched
{
    public void Send(string message) => EmailGateway.Send("volunteer@example.org", message);
}

public class ShiftDigestService
{
    private readonly IShiftRepository _repository;
    private readonly IShiftNotifier _notifier;

    public ShiftDigestService(IShiftRepository repository, IShiftNotifier notifier)
    {
        _repository = repository;
        _notifier = notifier;
    }

    public void SendWeeklyDigest(int volunteerId, DateTime weekStart)
    {
        var shifts = _repository.GetForWeek(volunteerId, weekStart);
        _notifier.Send($"You have {shifts.Count} shift(s) this week.");
    }
}

Testing ShiftDigestService now means handing it a stub IShiftRepository and a stub IShiftNotifier and running the test in-process — no database connection string anywhere in sight, no SMS bill racking up from a test suite. And the day the nonprofit decides push notifications are worth adding, this class doesn't need to be opened, let alone have its existing tests rewritten.