SOLID Principles: Beyond the Prompt
A walkthrough of what each SOLID principle actually catches , using one order-processing class as the running example.
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