SOLID in One Interview: A Full Before/After Refactor
You'll learn to
- -Walk a single messy class through all five SOLID principles until it becomes an extensible, testable design
- -Practice narrating the refactor out loud the way a strong interview answer actually sounds
Each SOLID principle in isolation is straightforward. The actual interview skill is applying all five together, in the right order, to one messy class - and narrating your reasoning the whole way, since a silent refactor earns no credit. This chapter walks one realistic starting point (an OrderProcessor that has grown every responsibility over several sprints) through all five principles in sequence.
The Starting Point
class OrderProcessor:
def process(self, order, payment_type: str):
# validation
if not order.items:
raise ValueError("Empty order")
# payment - branches on type, grows with every new payment method
if payment_type == "credit_card":
charge_credit_card(order.total)
elif payment_type == "paypal":
charge_paypal(order.total)
# persistence - talks directly to MySQL
mysql_connection.execute("INSERT INTO orders ...")
# notification
send_email(order.customer_email, "Order confirmed")Applying SRP: Separate the Four Reasons to Change
Validation rules, payment processing, persistence, and notification each change for a different reason and a different stakeholder. Split into OrderValidator, PaymentProcessor, OrderRepository, and OrderNotifier, with OrderProcessor becoming a thin orchestrator that calls each in sequence.
Applying OCP: Kill the Payment-Type Branch
The if/elif on payment_type is the OCP smell from two chapters ago. Replace it with a PaymentStrategy interface and one class per payment method (CreditCardPayment, PayPalPayment), so a new payment method is a new class, not a new elif in a method every existing payment type already depends on.
Checking LSP: Do All PaymentStrategy Implementations Actually Substitute Cleanly?
Before moving on, sanity-check the new hierarchy: does every PaymentStrategy implementation charge() without surprising a caller that only knows the interface? If, say, a GiftCardPayment needs a partial-charge concept the interface never promised, that is caught here, before it becomes a production bug - not after.
Applying ISP: Split OrderRepository If It Has Grown Unrelated Methods
If OrderRepository has accumulated methods only some callers need (say, generate_analytics_report() alongside save() and find_by_id()), split it - callers that only ever save and fetch orders should not depend on a reporting method they never call.
Applying DIP: Depend on Interfaces at the Top
class OrderProcessor:
def __init__(self, validator: OrderValidator, payment: PaymentStrategy,
repo: OrderRepository, notifier: OrderNotifier):
self._validator = validator
self._payment = payment
self._repo = repo
self._notifier = notifier
def process(self, order: Order) -> None:
self._validator.validate(order)
self._payment.charge(order.total)
self._repo.save(order)
self._notifier.notify(order.customer_email, "Order confirmed")OrderProcessor now depends on four interfaces, injected through its constructor - none of it depends on a concrete database, a concrete payment provider, or a concrete email client. Every one of those four collaborators can be swapped, mocked in a test, or extended with a new implementation without ever touching OrderProcessor again.
When narrating this out loud in an interview, name the principle as you apply each step ("this is an SRP split because...", "this removes the OCP violation because..."). It signals you are reasoning from principles, not pattern-matching from memory.
After this refactor, the interviewer asks: "How would you unit test OrderProcessor.process() without hitting a real database or a real payment API?"
"I'd set up a test database and a sandbox payment account to run the test against."
"I wouldn't need either - since OrderProcessor depends on the OrderRepository and PaymentStrategy interfaces, I'd inject a fake in-memory repository and a fake payment strategy that just records what it was called with. The test verifies OrderProcessor's orchestration logic in isolation, which is exactly the payoff of applying Dependency Inversion in the first place."
In the OrderProcessor refactor, what specifically triggers applying the Single Responsibility Principle?