Card 04/ 09

ExampleDifficulty: Beginner2 min

SOLID Example: One Checkout Class That Breaks All Five

Pricing, discounts, saving and email in one class, and five change requests that hurt.

One ordinary Python class can break all five SOLID principles at once, and this example shows it, so that each principle has a concrete line of code attached to it. The class is a shop checkout that a small team wrote quickly. It works, and it prints the right receipt. Read it first, then read the change requests that arrive the following month.

py
class MySQLDatabase:
    def insert(self, table, row):
        print(f"INSERT INTO {table}: {row}")


class SmtpMailer:
    def send(self, to, body):
        print(f"MAIL to {to}: {body}")


class Checkout:
    def __init__(self):
        self.items = []

    def add(self, name, price, quantity):
        self.items.append((name, price, quantity))

    def total(self, customer_type):
        subtotal = sum(price * quantity for _, price, quantity in self.items)
        if customer_type == "member":
            return subtotal * 0.9
        if customer_type == "staff":
            return subtotal * 0.7
        return subtotal

    def save(self):
        MySQLDatabase().insert("orders", self.items)

    def email_receipt(self, address, customer_type):
        SmtpMailer().send(address, f"You paid {self.total(customer_type):.2f}")

    def print_label(self):
        print(f"Shipping label for {len(self.items)} lines")


class InStoreCheckout(Checkout):
    def email_receipt(self, address, customer_type):
        raise NotImplementedError("in-store sales print a paper receipt")

    def print_label(self):
        raise NotImplementedError("nothing to ship")


online = Checkout()
online.add("Notebook", 4.50, 2)
online.add("Pen", 1.20, 5)
online.save()
online.email_receipt("[email protected]", "member")

till = InStoreCheckout()
till.add("Pen", 1.20, 1)
try:
    till.email_receipt("[email protected]", "regular")
except NotImplementedError as error:
    print(f"Failed: {error}")
OutputRan with python:3.12-slim (Python 3.12.13)
INSERT INTO orders: [('Notebook', 4.5, 2), ('Pen', 1.2, 5)]
MAIL to [email protected]: You paid 13.50
Failed: in-store sales print a paper receipt
A checkout that prices, saves, emails and labels, and a subclass that refuses two of those jobs.

The highlighted lines are the trouble. total decides the discount with an if statement per customer type. save and email_receipt each create their own MySQLDatabase and SmtpMailer inside the method, so nobody can hand the checkout a different store or a fake mailer for a test. InStoreCheckout inherits methods it cannot honour and raises an error from them instead.

Now the change requests arrive. Each one hurts for a different reason, and each reason is one SOLID principle.

Five change requests against the checkout, and the principle each one shows broken
Change requestWhat goes wrongPrinciple broken
Finance adds a student price.Another branch goes into total, and the class that already works is edited again.Open/closed
Marketing rewrites the receipt wording.The pricing class is opened to change an email, and pricing is put at risk.Single responsibility
Operations moves to PostgreSQL and the tests need no real database.MySQLDatabase is created inside save, so it cannot be swapped.Dependency inversion
A nightly job emails a receipt for every checkout.It crashes on the first in-store sale, because the subclass breaks its parent's promise.Liskov substitution
A self-service kiosk must use Checkout but never ships or emails.It still has to provide email_receipt and print_label, as stubs that do nothing.Interface segregation

Notice that one class produces all five failures, and that the fixes are separate. Moving email out solves the marketing request but not the database one. That is why the principles are taught one at a time: each names a different seam along which this class should come apart.