.NET Academy

SOLID principles

Five design principles for code that's easy to change — with practical examples instead of slogans.

14 min read

You will learn

  • Explain each SOLID principle with an example
  • Recognize code that violates them
  • Refactor a growing switch into pluggable strategies (open/closed)
  • Apply the principles pragmatically

Code that's easy to change

Software that matters keeps changing: new requirements, new integrations, bug fixes. SOLID is a set of five principles (popularized by Robert C. Martin) that make classes easier to change without breaking things. They're guidelines, not laws — use them to spot problems, not to add ceremony.

S — Single responsibility

A class should have one reason to change.

// ❌ Changes when the report format, the data source, OR the email provider changes
class MonthlyReport
{
    public void Run()
    {
        var sales = LoadSalesFromDatabase();
        var pdf = RenderPdf(sales);
        SendEmail("boss@example.com", pdf);
    }
}
 
// ✅ Each part has one job; MonthlyReport only coordinates
class MonthlyReport(ISalesRepository sales, IReportRenderer renderer, IEmailSender email)
{
    public async Task RunAsync() =>
        await email.SendAsync("boss@example.com", renderer.Render(await sales.GetMonthAsync()));
}

O — Open/closed

Open for extension, closed for modification.

A switch that grows with every new case means editing (and re-testing) the same code forever:

// ❌ Every new payment method edits this method
decimal Fee(string method, decimal amount) => method switch
{
    "card" => amount * 0.029m,
    "paypal" => amount * 0.034m + 0.35m,
    "bank" => 0.50m,
    _ => throw new NotSupportedException(),
};

Instead, put each variation behind an interface — adding a method means adding a class:

interface IFeePolicy { string Method { get; } decimal Fee(decimal amount); }
class CardFee : IFeePolicy { public string Method => "card"; public decimal Fee(decimal a) => a * 0.029m; }
// register all policies in DI; the calculator picks the matching one

This lesson's exercise applies exactly this to shipping rules.

L — Liskov substitution

Subtypes must be usable wherever their base type is expected.

If code written for Rectangle breaks when given a Square (because setting the width also changes the height), the hierarchy is wrong — even though "a square is a rectangle" in geometry. Warning signs: overrides that throw NotSupportedException, or callers checking if (x is SpecialSubtype).

I — Interface segregation

Clients shouldn't depend on methods they don't use.

// ❌ A read-only report must implement Save and Delete it never needs
interface IRepository<T> { T Get(int id); IEnumerable<T> All(); void Save(T item); void Delete(int id); }
 
// ✅ Small, focused interfaces
interface IReader<T> { T Get(int id); IEnumerable<T> All(); }
interface IWriter<T> { void Save(T item); void Delete(int id); }

D — Dependency inversion

Depend on abstractions, not concretions.

High-level policy (OrderService) shouldn't depend on low-level details (StripeClient). Both depend on an abstraction (IPaymentGateway), and the details are plugged in through dependency injection — what you did in Tracks 8 and 9. That's what makes the code testable and swappable.

Using SOLID pragmatically

  • Apply the principles when code starts to hurt — a class keeps growing, every change touches the same switch, tests are impossible to write.
  • Don't create an interface for every class "just in case" (YAGNI — you aren't gonna need it).
  • Prefer simple, readable code over pattern-heavy code. SOLID is a means, not a goal.

Key takeaways

  • S: one reason to change per class.
  • O: extend behavior by adding code (new classes), not editing working code.
  • L: subtypes must honor their base type's contract.
  • I: small, focused interfaces.
  • D: depend on abstractions; inject the details.

Exercise

ShippingCalculator should apply the first rule that has an answer — and new rules must be addable without editing the calculator (open/closed). Implement the three rules (FreeShippingOver, ExpressShipping, FlatRate) and ShippingCalculator.Calculate, which returns the first non-null result from its rules, in order.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

Each rule returns its price when it applies, otherwise null: order.Express ? price : null — note you may need (decimal?) casts in a ternary.

Hint 2

Calculate: foreach rule, var cost = rule.TryCalculate(order); if (cost is not null) return cost.Value; — and decide a fallback (e.g. throw if no rule applied).

Show solution
var calculator = new ShippingCalculator([new ExpressShipping(25), new FreeShippingOver(100), new FlatRate(5)]);
Console.WriteLine(calculator.Calculate(new Order(120, Express: false)));

record Order(decimal Total, bool Express);

interface IShippingRule
{
    /// <summary>The shipping cost, or null if this rule doesn't apply.</summary>
    decimal? TryCalculate(Order order);
}

class FreeShippingOver(decimal threshold) : IShippingRule
{
    public decimal? TryCalculate(Order order) => order.Total >= threshold ? 0m : null;
}

class ExpressShipping(decimal price) : IShippingRule
{
    public decimal? TryCalculate(Order order) => order.Express ? price : null;
}

class FlatRate(decimal price) : IShippingRule
{
    public decimal? TryCalculate(Order order) => price;
}

class ShippingCalculator(IEnumerable<IShippingRule> rules)
{
    public decimal Calculate(Order order)
    {
        foreach (var rule in rules)
        {
            if (rule.TryCalculate(order) is decimal cost) return cost;
        }
        throw new InvalidOperationException("No shipping rule applied.");
    }
}

Quiz

4 questions · 3 correct to pass
  1. 1.A ReportService loads data, calculates totals, formats PDFs and emails them. Which principle does it violate?

  2. 2.What does the open/closed principle ask for?

  3. 3.Square derives from Rectangle, but setting Square.Width also changes Height, breaking code that expects rectangles. Which principle is violated?

  4. 4.What does dependency inversion mean in practice?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)