← Back to list

SOLID Principles: Beyond the Prompt

A walkthrough of what each SOLID principle actually catches , using one order-processing class as the running example.

Chinmoy Debnath · 2026-07-13 16:52 · 30 claps · 3.7 min read
#software-development #programming #python #design-principles #software-engineering
Open on Medium ↗
Wiki topics: 💻 · Programming 🏃 · Running & Endurance

SOLID Principles: Beyond the Prompt

A walkthrough of what each SOLID principle actually catches — using one order-processing class as the running example.

S: Single Responsibility Principle — A class should have only one reason to change, handling a single, specific job. O: Open/Closed Principle — Should be open for extension but closed for modification. L: Linskov Substitution Principle — Any subclass can be used in place of its parent class without causing unexpected behaviour. I: Interface Segregation Principle — It is better to have multiple, small, client-specific interfaces rather than one large. D: Dependency Inversion Principle — High-level modules should depend on abstractions (interfaces) rather than concrete low-level implementations

The starting point

class OrderProcessor:
    def process_order(self, order, payment_type, send_email=True):
        if order["total"] <= 0:
            raise ValueError("Invalid order total")
        if payment_type == "credit_card":
            print(f"Charging {order['total']} to credit card")
        elif payment_type == "paypal":
            print(f"Charging {order['total']} via PayPal")
        else:
            raise ValueError("Unknown payment type")
        # ...save, email, log - all inline

One class, five jobs: validation, payment, persistence, notification, logging. Let’s take them apart one principle at a time.

Step 1 — SRP: one reason to change

The question to ask: for each piece of logic, what business event would force this to change? Pricing rules changing and email copy changing are unrelated events — they shouldn’t live in code that changes together.

class ValidateOrder:
    def __init__(self, order): self.order = order
    def validate(self):
        if self.order["total"] <= 0:
            raise ValueError("Invalid order total")

class ChargePayment:
    def __init__(self, order, payment_type):
        self.order, self.payment_type = order, payment_type
    def pay(self):
        if self.payment_type == "credit_card":
            print(f"Charging {self.order['total']} to credit card")
        # ...

class OrdersRepo:
    def save_order(self, order): ...

class SendEmail:
    def send(self): ...

class LogOrder:
    def log(self): ...

class OrderProcessor:
    def process_order(self, order, payment_type, send_email=True):
        ValidateOrder(order).validate()
        ChargePayment(order, payment_type).pay()
        # ... coordinate the rest

Common trap: doing the work inside __init__ instead of a named method. That makes the class a disguised function - you can't call it twice, and you can't swap it polymorphically, which breaks the next step.

Step 2 — OCP + DIP: add without editing

The interview tell: “what if we add Apple Pay?” If the answer involves opening ChargePayment and adding an elif, that's OCP failing - and it's failing because of a DIP gap: the high-level class is coupled to concrete payment types instead of an abstraction.

from abc import ABC, abstractmethod

class ChargePayment(ABC):
    def __init__(self, order): self.order = order
    @abstractmethod
    def charge(self): ...

class CreditCard(ChargePayment):
    def charge(self): print(f"Charging {self.order['total']} to credit card")

class PayPal(ChargePayment):
    def charge(self): print(f"Charging {self.order['total']} via PayPal")

class ApplePayPayment(ChargePayment):   # added - nothing else touched
    def charge(self): print(f"Charging {self.order['total']} via Apple Pay")

class OrderProcessor:
    def process_order(self, order, payment_method: ChargePayment, send_email=True):
        ValidateOrder(order).validate()
        payment_method.charge()   # no if/elif, no knowledge of concrete type
        # ...

Verification, not assumption: OCP is proven by naming every file you didn’t open when adding ApplePayPayment. If any existing class needed an edit, it hadn't actually held.

Step 3 — LSP: the contract, not just the method signature

Adding a subclass that “type-checks” isn’t enough — it has to honor what callers reasonably assume. A payment that can legitimately fail (CryptoPayment) isn't an LSP violation if the contract documents it. It's a violation if it fails in a way the abstraction never promised, breaking correct callers.

class PaymentDeclinedError(Exception): pass

class ChargePayment(ABC):
    @abstractmethod
    def charge(self):
        """Raises PaymentDeclinedError on failure. Never raises anything else."""

class CryptoPayment(ChargePayment):
    def charge(self):
        raise PaymentDeclinedError("Crypto charge failed")

A caller written against the documented contract can catch PaymentDeclinedError specifically - no surprises, no bare except Exception needed.

Step 4 — ISP: don’t force unused methods

New requirement: refund(). But Apple Pay and Crypto (in this scenario) don't support it. Bolting refund() onto ChargePayment forces every subclass to implement a method some of them have no real answer for - that's the violation.

class Refundable(ABC):
    @abstractmethod
    def refund(self): ...

class CreditCard(ChargePayment, Refundable):
    def refund(self): print(f"Refunding {self.order['total']}")

class PayPal(ChargePayment, Refundable):
    def refund(self): print(f"Refunding {self.order['total']}")

# ApplePayPayment, CryptoPayment: ChargePayment only - no refund() at all

Callers should be guarded before the call, not after:

def refund_order(self, payment_method: Refundable):
    if not isinstance(payment_method, Refundable):
        raise TypeError(f"{type(payment_method).__name__} does not support refunds")
    payment_method.refund()

Final class diagram (after Step 4)

The takeaway

SRP made the class splittable. OCP + DIP made it extensible without edits. LSP made new subclasses trustworthy. ISP kept subclasses from carrying methods they can’t honour. None of these are abstract rules — each one is a direct answer to a bug or a design trap that shows up the moment you try to extend real code.

Bonus: Logger Framework class diagram

Same principles, applied to a multi-sink, multi-level logging system — Logger coordinates, Sink is the OCP-friendly extension point, LOG_LEVEL stays a simple enum.

Why this shape holds up: Logger never imports or references ConsoleSink, FileSink, or HTTPSink by name - it only calls .printLog() on whatever's in its sinks list. Adding a new sink type means writing one new class; Logger is never reopened. That's OCP and DIP, the same mechanism as the payment hierarchy above, just applied to a different domain.


메타데이터
post_id
f031bbee3bdb
slug
solid-principles-beyond-the-prompt-f031bbee3bdb
url
https://medium.com/@chinmoy1113/solid-principles-beyond-the-prompt-f031bbee3bdb
canonical_url
https://medium.com/@chinmoy1113/solid-principles-beyond-the-prompt-f031bbee3bdb
author_url
https://medium.com/@chinmoy1113
status
ok
fetched_at
2026-08-12 21:38:07