Documentation Index Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
๐ The OCP Rule
โSoftware entities should be open for EXTENSION but closed for MODIFICATION.โ
Think of your smartphone:
You can add new apps (extension) โ
You donโt rewrite the iOS/Android code (modification) โ
Or think of a game console:
Add new games without changing the console hardware โ
Each game works because it follows the consoleโs interface โ
The Goal : When you need new features, ADD new code - donโt CHANGE existing code that already works!
The USB port analogy: A USB port on your laptop is open for extension (plug in a keyboard, mouse, drive, camera โ anything that speaks USB) but closed for modification (you never need to rewire the portโs circuitry to support a new device). The port defines a standard, and new devices conform to it. That is exactly what OCP looks like in code: you define an interface, and new behavior arrives as new classes that implement it โ never by editing the existing working code.
๐ฏ Why OCP Matters
Without OCP With OCP Adding feature = changing existing code Adding feature = adding new code Risk breaking working features Existing code stays untouched Must re-test everything Only test new code Fear of making changes Confidence in extensions
๐จ Spotting OCP Violations
The biggest red flag: if/elif chains that grow over time!
โ BAD: Growing If/Elif Chains
class DiscountCalculator :
def calculate ( self , customer_type , amount ):
# Every new customer type = modify this method!
if customer_type == "regular" :
return amount # No discount
elif customer_type == "premium" :
return amount * 0.9 # 10% off
elif customer_type == "vip" :
return amount * 0.8 # 20% off
elif customer_type == "employee" :
return amount * 0.7 # 30% off
# ๐ฑ Need student discount? Modify this file!
# ๐ฑ Need senior discount? Modify this file!
# ๐ฑ What if we forget a case?
else :
raise ValueError ( f "Unknown customer type: { customer_type } " )
# Adding new discount type:
# 1. Open this file
# 2. Add new elif
# 3. Risk breaking existing logic
# 4. Re-test everything
โ
GOOD: Extensible Design
from abc import ABC , abstractmethod
# Step 1: Define the interface
class DiscountStrategy ( ABC ):
@abstractmethod
def calculate ( self , amount : float ) -> float :
pass
# Step 2: Implement each strategy
class RegularDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount # No discount
class PremiumDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount * 0.9 # 10% off
class VIPDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount * 0.8 # 20% off
class EmployeeDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount * 0.7 # 30% off
# Step 3: Use the strategy
class DiscountCalculator :
def __init__ ( self , strategy : DiscountStrategy):
self .strategy = strategy
def calculate ( self , amount ):
return self .strategy.calculate(amount)
# DESIGN REASONING: Adding a new discount type requires ZERO changes
# to DiscountCalculator. You just create a new class. This is OCP --
# the calculator is closed for modification, open for extension.
class StudentDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount * 0.85 # 15% off
class SeniorDiscount ( DiscountStrategy ):
def calculate ( self , amount ):
return amount * 0.75 # 25% off
# Usage - no if/elif needed!
regular_calc = DiscountCalculator(RegularDiscount())
student_calc = DiscountCalculator(StudentDiscount())
print (regular_calc.calculate( 100 )) # 100.0
print (student_calc.calculate( 100 )) # 85.0
๐ฎ Real Example: Shape Drawing
โ Before: Modification Required
class GraphicsEditor :
def draw ( self , shapes ):
for shape in shapes:
# Adding new shape = modify this method!
if shape.type == "circle" :
self ._draw_circle(shape)
elif shape.type == "rectangle" :
self ._draw_rectangle(shape)
elif shape.type == "triangle" :
self ._draw_triangle(shape)
# Want hexagon? Modify here!
# Want star? Modify here!
def _draw_circle ( self , shape ):
print ( f "โญ Drawing circle with radius { shape.radius } " )
def _draw_rectangle ( self , shape ):
print ( f "โฌ Drawing rectangle { shape.width } x { shape.height } " )
def _draw_triangle ( self , shape ):
print ( f "๐บ Drawing triangle with base { shape.base } " )
โ
After: Extension Ready
from abc import ABC , abstractmethod
class Shape ( ABC ):
@abstractmethod
def draw ( self ):
pass
class Circle ( Shape ):
def __init__ ( self , radius ):
self .radius = radius
def draw ( self ):
print ( f "โญ Drawing circle with radius { self .radius } " )
class Rectangle ( Shape ):
def __init__ ( self , width , height ):
self .width = width
self .height = height
def draw ( self ):
print ( f "โฌ Drawing rectangle { self .width } x { self .height } " )
class Triangle ( Shape ):
def __init__ ( self , base , height ):
self .base = base
self .height = height
def draw ( self ):
print ( f "๐บ Drawing triangle with base { self .base } " )
class GraphicsEditor :
"""CLOSED for modification - won't change for new shapes!"""
def draw ( self , shapes ):
for shape in shapes:
shape.draw() # Each shape knows how to draw itself!
# ๐ Add new shapes without touching GraphicsEditor!
class Hexagon ( Shape ):
def __init__ ( self , side ):
self .side = side
def draw ( self ):
print ( f "โฌก Drawing hexagon with side { self .side } " )
class Star ( Shape ):
def __init__ ( self , points , size ):
self .points = points
self .size = size
def draw ( self ):
print ( f "โญ Drawing { self .points } -pointed star of size { self .size } " )
# Usage
editor = GraphicsEditor()
shapes = [
Circle( 5 ),
Rectangle( 10 , 20 ),
Hexagon( 8 ),
Star( 5 , 15 )
]
editor.draw(shapes) # Works without modifying GraphicsEditor!
๐ณ Real Example: Payment Processing
โ Before: If/Elif Nightmare
class PaymentProcessor :
def process ( self , payment_type , amount , details ):
if payment_type == "credit_card" :
return self ._process_credit_card(amount, details)
elif payment_type == "paypal" :
return self ._process_paypal(amount, details)
elif payment_type == "bank_transfer" :
return self ._process_bank_transfer(amount, details)
# ๐ฑ Apple Pay? Modify!
# ๐ฑ Crypto? Modify!
# ๐ฑ Google Pay? Modify!
else :
raise ValueError ( f "Unknown payment type: { payment_type } " )
def _process_credit_card ( self , amount , details ):
print ( f "๐ณ Processing $ { amount } via credit card" )
# 100 lines of credit card logic...
def _process_paypal ( self , amount , details ):
print ( f "๐
ฟ๏ธ Processing $ { amount } via PayPal" )
# 100 lines of PayPal logic...
def _process_bank_transfer ( self , amount , details ):
print ( f "๐ฆ Processing $ { amount } via bank transfer" )
# 100 lines of bank transfer logic...
โ
After: Plugin Architecture
from abc import ABC , abstractmethod
class PaymentMethod ( ABC ):
@abstractmethod
def process ( self , amount : float , details : dict ) -> bool :
pass
@abstractmethod
def get_name ( self ) -> str :
pass
class CreditCardPayment ( PaymentMethod ):
def process ( self , amount , details ):
card_number = details.get( 'card_number' , '****' )[ - 4 :]
print ( f "๐ณ Processing $ { amount :.2f} " )
print ( f " Card ending in { card_number } " )
print ( f " โ
Credit card payment successful!" )
return True
def get_name ( self ):
return "Credit Card"
class PayPalPayment ( PaymentMethod ):
def process ( self , amount , details ):
email = details.get( 'email' , 'unknown' )
print ( f "๐
ฟ๏ธ Processing $ { amount :.2f} " )
print ( f " PayPal account: { email } " )
print ( f " โ
PayPal payment successful!" )
return True
def get_name ( self ):
return "PayPal"
class BankTransferPayment ( PaymentMethod ):
def process ( self , amount , details ):
account = details.get( 'account' , 'unknown' )
print ( f "๐ฆ Processing $ { amount :.2f} " )
print ( f " Bank account: { account } " )
print ( f " โณ Bank transfer initiated (2-3 business days)" )
return True
def get_name ( self ):
return "Bank Transfer"
# PaymentProcessor is CLOSED - never needs modification!
class PaymentProcessor :
def process ( self , payment_method : PaymentMethod, amount : float , details : dict ):
print ( f " \n { '=' * 50 } " )
print ( f "Processing payment via { payment_method.get_name() } " )
print ( f " { '=' * 50 } " )
return payment_method.process(amount, details)
# ๐ Add new payment methods WITHOUT touching PaymentProcessor!
class ApplePayPayment ( PaymentMethod ):
def process ( self , amount , details ):
device = details.get( 'device' , 'iPhone' )
print ( f "๐ Processing $ { amount :.2f} " )
print ( f " Device: { device } " )
print ( f " โ
Apple Pay successful!" )
return True
def get_name ( self ):
return "Apple Pay"
class CryptoPayment ( PaymentMethod ):
def process ( self , amount , details ):
wallet = details.get( 'wallet' , '0x...' )[: 10 ]
crypto = details.get( 'currency' , 'BTC' )
print ( f "โฟ Processing $ { amount :.2f} in { crypto } " )
print ( f " Wallet: { wallet } ..." )
print ( f " โณ Waiting for blockchain confirmation..." )
print ( f " โ
Crypto payment confirmed!" )
return True
def get_name ( self ):
return "Cryptocurrency"
# Usage
processor = PaymentProcessor()
# Original payment methods work
processor.process(CreditCardPayment(), 99.99 , { "card_number" : "1234567890123456" })
processor.process(PayPalPayment(), 49.99 , { "email" : "user@example.com" })
# NEW payment methods work too - no modification to PaymentProcessor!
processor.process(ApplePayPayment(), 29.99 , { "device" : "iPhone 15" })
processor.process(CryptoPayment(), 199.99 , { "wallet" : "0x1234abcd..." , "currency" : "ETH" })
๐ง Real Example: Notification System
from abc import ABC , abstractmethod
from typing import List
class NotificationChannel ( ABC ):
@abstractmethod
def send ( self , recipient : str , message : str ) -> bool :
pass
class EmailChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ง Email to { recipient } : { message } " )
return True
class SMSChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ฑ SMS to { recipient } : { message[: 160 ] } " )
return True
class SlackChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ฌ Slack to { recipient } : { message } " )
return True
# NotificationService is CLOSED for modification!
class NotificationService :
def __init__ ( self , channels : List[NotificationChannel]):
self .channels = channels
def notify ( self , recipient : str , message : str ):
for channel in self .channels:
channel.send(recipient, message)
# ๐ Add new channels without touching NotificationService!
class PushNotificationChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ Push to { recipient } : { message } " )
return True
class DiscordChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ฎ Discord to { recipient } : { message } " )
return True
class WhatsAppChannel ( NotificationChannel ):
def send ( self , recipient , message ):
print ( f "๐ WhatsApp to { recipient } : { message } " )
return True
# Usage - mix and match channels!
print ( "=== Order Confirmation ===" )
order_notifier = NotificationService([
EmailChannel(),
SMSChannel(),
PushNotificationChannel()
])
order_notifier.notify( "customer@example.com" , "Your order #123 has been confirmed!" )
print ( " \n === Team Alert ===" )
team_notifier = NotificationService([
SlackChannel(),
DiscordChannel()
])
team_notifier.notify( "#devops" , "๐จ Server CPU at 95%!" )
๐ฏ Techniques for OCP
Strategy Pattern Swap algorithms at runtime class Context :
def __init__ ( self , strategy ):
self .strategy = strategy
Template Method Define skeleton, subclass fills details class Template :
def algorithm ( self ):
self .step1() # Fixed
self .step2() # Override
Decorator Pattern Wrap and add behavior class Decorator ( Component ):
def __init__ ( self , component ):
self .wrapped = component
Factory Pattern Create objects without specifying class class Factory :
def create ( self , type ):
return self .creators[ type ]()
๐งช Practice Exercise
Challenge: Fix the Report Generator
This report generator violates OCP. Fix it! class ReportGenerator :
def generate ( self , data , format_type ):
if format_type == "pdf" :
return self ._generate_pdf(data)
elif format_type == "excel" :
return self ._generate_excel(data)
elif format_type == "csv" :
return self ._generate_csv(data)
elif format_type == "html" :
return self ._generate_html(data)
# Adding JSON? Modify here!
# Adding XML? Modify here!
else :
raise ValueError ( f "Unknown format: { format_type } " )
def _generate_pdf ( self , data ):
return f "PDF Report: { data } "
def _generate_excel ( self , data ):
return f "Excel Report: { data } "
def _generate_csv ( self , data ):
return f "CSV Report: { data } "
def _generate_html ( self , data ):
return f "HTML Report: { data } "
๐ Key Takeaways
Before OCP After OCP If/elif chains Polymorphism Modify existing classes Add new classes Risk breaking things Safe extension Hard to test Easy to test Coupled code Decoupled code
Why OCP Matters in Production
Consider a real scenario: you are building an analytics pipeline that supports different export formats. The product manager says โwe need CSV export.โ You build it. A month later: โwe also need PDF.โ Then: โadd Excel.โ Then: โadd BigQuery export.โ If your code uses if/elif chains, every new format means editing the same file, risking regressions in existing formats, and re-testing everything. With OCP, each format is an independent class. Adding BigQuery export means creating one new file with one new class. The existing CSV, PDF, and Excel exports are untouched and untested โ because they did not change. That is the practical payoff.
A senior engineer would say: โThe if/elif chain is the code smell that tells you OCP is being violated. Whenever you see a growing conditional, ask yourself: can I replace this with polymorphism?โ
Interview Insight
OCP is the principle interviewers test most often without naming it. When they say โhow would you add support for a new payment method?โ or โwhat happens when we need a new notification channel?โ, they are checking whether your design is open for extension. The winning answer always involves creating a new class that implements an existing interface โ never modifying a working class. If you find yourself saying โI would add an elif in the existing method,โ pause and restructure. The refactored version with a Strategy or Factory pattern is almost always what the interviewer is looking for.
๐ Next: Liskov Substitution Principle
Now letโs learn about making sure child classes can truly replace their parents!
Continue to Liskov Substitution โ Learn the rule that keeps inheritance from causing surprises!