OOP and SOLID
DabbaGo wants to accept wallet payments. A developer opens OrderService and finds 900 lines that calculate prices, save to PostgreSQL, send SMS messages, and choose a payment method with a long if chain. Adding one payment method means editing a class that everything depends on, and every change breaks something unrelated. Object-oriented design, guided by five principles, is how architects keep code from growing into this.
The rule
Small objects, one job each, joined through interfaces.
Object-oriented programming (OOP) groups data together with the behaviour that uses it. An Order object holds its items and knows how to total them. Four ideas carry most of the weight:
- Encapsulation: an object hides its inner data and exposes only safe operations.
- Abstraction: callers see what an object does, not how.
- Inheritance: a type can build on another type.
- Polymorphism: different objects answer the same message in their own way, so
pay()works for UPI, card or wallet.
SOLID names five principles for keeping objects healthy:
- S, Single responsibility: a class should have one reason to change. Pricing rules and SMS templates change for different reasons, so they belong in different classes.
- O, Open/closed: open to extension, closed to modification. Add a payment method by adding a class, not by editing tested code.
- L, Liskov substitution: any subtype must work wherever its parent type is expected. If
WalletPaymentrefuses refunds that everyPaymentMethodpromises, it breaks callers. - I, Interface segregation: prefer several small interfaces over one large one, so no class is forced to implement methods it does not use.
- D, Dependency inversion: high-level logic depends on abstractions, and details such as PostgreSQL plug in from outside.
class PaymentMethod: # the abstraction
def pay(self, amount): ...
class UpiPayment(PaymentMethod):
def pay(self, amount): ... # talk to the UPI gateway
class WalletPayment(PaymentMethod):
def pay(self, amount): ... # deduct from the wallet
class OrderService:
def __init__(self, repository, notifier):
self.repository = repository # any storage that saves orders
self.notifier = notifier # SMS, email, or a fake in tests
def checkout(self, order, method: PaymentMethod):
method.pay(order.total())
self.repository.save(order)
self.notifier.send(order.customer, "Your tiffin is booked!")Adding wallets is now a new class. OrderService never changes.
The shortcut
For any class, ask "What would make me edit this?" List the answers.
- More than one kind of answer → split it (S).
- "Adding a new kind of X" is on the list → add an interface for X (O).
- "Changing the database or provider" is on the list → invert that dependency (D).
List the reasons to change
New price rules, a new database, a new SMS provider, a new payment method. Four kinds of reason: single responsibility is broken.
The trap is applying SOLID mechanically, turning a 20-line class into ten interfaces with one method each. The principles serve changeability; if nothing is likely to change, keep it simple.
Drills
Answer fast — trust the rule.
A class formats SMS messages and also calculates tiffin prices. Which principle does it break?
To add wallet payments without editing OrderService, what should DabbaGo do?
Which OOP idea lets method.pay(amount) work for UPI, card and wallet alike?
OrderService creates its own PostgreSQL connection inside checkout. Which principle does passing in a repository fix?
Remember
- Small objects, one job each, joined through interfaces.
- Shortcut: "what would make me edit this class?" Many kinds of answer means split.
- Trap: applying SOLID mechanically until simple code drowns in interfaces.
