Design Patterns, Explained Simply · Foundations: Thinking in Patterns

SOLID, Part 1: Single Responsibility and Open/Closed

The S and O of SOLID: give each class one reason to change, and add new behaviour by adding code rather than editing working code.

The problem

An Invoice class calculates totals, applies discounts, formats a PDF, and emails the customer. One month finance changes the tax rule, design changes the PDF layout, and marketing adds a "loyalty" discount. Three teams edit the same file, and a change to the email text breaks the tax calculation tests.

The discount logic is an if/else chain inside total(). Every new promotion means editing it, so every release risks breaking the promotions that already work.

Real-world analogy

A restaurant has a chef, a cashier and a waiter. If the menu changes, only the chef retrains (single responsibility). When the restaurant adds a new card machine, nobody rebuilds the counter: the counter has a standard socket, and the new machine plugs into it (open for extension, closed for modification).

The idea

SOLID is five design principles collected by Robert C. Martin. The first two:

  • S: Single Responsibility Principle (SRP). A class should have one reason to change, meaning it answers to one group of people (finance, design, marketing). Split code that changes for different reasons.
  • O: Open/Closed Principle (OCP). Code should be open for extension, closed for modification: you add behaviour by adding new code (a new class or function) instead of editing code that already works and is tested.

SRP is about who asks for changes. OCP is usually achieved with an interface plus a list of implementations: exactly the shape of the Strategy pattern.

The code

The invoice is split into a calculator (finance's rules), a formatter (design's layout), and pluggable discount rules (marketing's promotions). Adding the loyalty discount is a new class; nothing existing is edited.

Split responsibilities and pluggable discount rulesTry it yourself
class TenPercentOver100:
    def discount(self, subtotal):
        return subtotal // 10 if subtotal > 10000 else 0


class LoyaltyMember:  # added later: a new class, no edits elsewhere
    def __init__(self, is_member):
        self.is_member = is_member

    def discount(self, subtotal):
        return 500 if self.is_member else 0


class InvoiceCalculator:  # finance's reason to change
    def __init__(self, rules):
        self.rules = rules

    def total(self, items):
        subtotal = sum(price * qty for price, qty in items)
        return subtotal - sum(rule.discount(subtotal) for rule in self.rules)


class InvoiceFormatter:  # design's reason to change
    def render(self, total):
        return f"TOTAL: {total // 100}.{total % 100:02d} USD"


items = [(4500, 2), (2500, 1)]  # prices in cents
calculator = InvoiceCalculator([TenPercentOver100(), LoyaltyMember(True)])
print(InvoiceFormatter().render(calculator.total(items)))
print(InvoiceFormatter().render(InvoiceCalculator([]).total(items)))

Output

TOTAL: 98.50 USD
TOTAL: 115.00 USD

The subtotal is 115.00. The 10% rule takes 11.50, loyalty takes 5.00, so the total is 98.50. A tax-rule change touches only InvoiceCalculator; a layout change touches only InvoiceFormatter; a new promotion is a new class.

Common mistakes

  • Reading SRP as "a class does one thing". It is about one reason to change. A class with five small methods that all serve finance's rules is fine.
  • Splitting too early. Ten one-method classes for a 50-line script is harder to read than one class. Split when two kinds of change actually collide.
  • Making everything "open" up front. You can't predict every extension point. Add one when the second variation appears (the "rule of three" is a good guide).

Key points

  • SRP: one reason to change per class, meaning one group of people whose requests change it.
  • OCP: add behaviour by adding code, not by editing working code.
  • OCP is usually implemented with an interface and a list of implementations (Strategy).
  • Apply both when changes actually collide, not speculatively.
  • Further reading: SOLID and The Single Responsibility Principle by Robert C. Martin.

Practice questions

What does "one reason to change" mean in the Single Responsibility Principle?

Answer

A class should answer to one source of change, typically one group of people or one concern (finance's tax rules, design's layout). If two different groups would ask for edits to the same class, it has two responsibilities and should be split.

How do you usually make code "open for extension, closed for modification"?

Answer

Introduce an abstraction (an interface, a function type, or a plugin list) at the point that varies. New behaviour becomes a new implementation that you register, and the code that uses the abstraction doesn't change. Strategy, Decorator and Observer are common ways to get there.

Can applying SRP and OCP too aggressively hurt a codebase?

Answer

Yes. Splitting every class and adding extension points nobody uses creates many tiny files and indirection that make the code harder to follow. Apply them when changes actually collide or a second variation appears.

Open in the app to mark lessons complete, take notes and track your progress.