Overview
Good design principles lead to code that is maintainable, testable, and extensible. These principles are fundamental to writing professional-quality software.
SOLID Principles
S - Single Responsibility Principle
A class should have only one reason to change.
# ❌ Bad: Multiple responsibilities
class User :
def __init__ ( self , name , email ):
self .name = name
self .email = email
def save_to_database ( self ):
# Database logic
pass
def send_email ( self ):
# Email logic
pass
# ✅ Good: Single responsibility
class User :
def __init__ ( self , name , email ):
self .name = name
self .email = email
class UserRepository :
def save ( self , user ):
# Database logic only
pass
class EmailService :
def send ( self , user , message ):
# Email logic only
pass
O - Open/Closed Principle
Open for extension, closed for modification.
# ❌ Bad: Must modify class for new shapes
class AreaCalculator :
def calculate ( self , shape ):
if shape.type == "circle" :
return 3.14 * shape.radius ** 2
elif shape.type == "rectangle" :
return shape.width * shape.height
# Must add more elif for each shape!
# ✅ Good: Extend without modifying
from abc import ABC , abstractmethod
class Shape ( ABC ):
@abstractmethod
def area ( self ):
pass
class Circle ( Shape ):
def __init__ ( self , radius ):
self .radius = radius
def area ( self ):
return 3.14 * self .radius ** 2
class Rectangle ( Shape ):
def __init__ ( self , width , height ):
self .width = width
self .height = height
def area ( self ):
return self .width * self .height
# New shapes just implement Shape interface
class Triangle ( Shape ):
def __init__ ( self , base , height ):
self .base = base
self .height = height
def area ( self ):
return 0.5 * self .base * self .height
L - Liskov Substitution Principle
Subtypes must be substitutable for their base types.
# ❌ Bad: Square violates Rectangle's contract
class Rectangle :
def set_width ( self , width ):
self .width = width
def set_height ( self , height ):
self .height = height
class Square ( Rectangle ):
def set_width ( self , width ):
self .width = width
self .height = width # Violates expectation!
def set_height ( self , height ):
self .width = height
self .height = height
# ✅ Good: Separate abstractions
class Shape ( ABC ):
@abstractmethod
def area ( self ):
pass
class Rectangle ( Shape ):
def __init__ ( self , width , height ):
self .width = width
self .height = height
def area ( self ):
return self .width * self .height
class Square ( Shape ):
def __init__ ( self , side ):
self .side = side
def area ( self ):
return self .side ** 2
I - Interface Segregation Principle
Clients should not depend on interfaces they don’t use.
# ❌ Bad: Fat interface
class Worker ( ABC ):
@abstractmethod
def work ( self ):
pass
@abstractmethod
def eat ( self ):
pass
@abstractmethod
def sleep ( self ):
pass
class Robot ( Worker ):
def work ( self ):
print ( "Working..." )
def eat ( self ):
pass # Robots don't eat!
def sleep ( self ):
pass # Robots don't sleep!
# ✅ Good: Segregated interfaces
class Workable ( ABC ):
@abstractmethod
def work ( self ):
pass
class Eatable ( ABC ):
@abstractmethod
def eat ( self ):
pass
class Human ( Workable , Eatable ):
def work ( self ):
print ( "Working..." )
def eat ( self ):
print ( "Eating..." )
class Robot ( Workable ):
def work ( self ):
print ( "Working..." )
D - Dependency Inversion Principle
Depend on abstractions, not concretions.
# ❌ Bad: High-level depends on low-level
class MySQLDatabase :
def save ( self , data ):
print ( "Saving to MySQL..." )
class UserService :
def __init__ ( self ):
self .db = MySQLDatabase() # Tight coupling!
def create_user ( self , user ):
self .db.save(user)
# ✅ Good: Both depend on abstraction
class Database ( ABC ):
@abstractmethod
def save ( self , data ):
pass
class MySQLDatabase ( Database ):
def save ( self , data ):
print ( "Saving to MySQL..." )
class PostgreSQLDatabase ( Database ):
def save ( self , data ):
print ( "Saving to PostgreSQL..." )
class UserService :
def __init__ ( self , db : Database): # Inject dependency
self .db = db
def create_user ( self , user ):
self .db.save(user)
# Usage - easy to swap implementations
mysql_service = UserService(MySQLDatabase())
postgres_service = UserService(PostgreSQLDatabase())
Other Important Principles
DRY - Don’t Repeat Yourself
# ❌ Bad: Repeated logic
def calculate_employee_bonus ( employee ):
base_salary = employee.salary
years = employee.years_of_service
bonus = base_salary * 0.1 * years
return bonus
def calculate_manager_bonus ( manager ):
base_salary = manager.salary
years = manager.years_of_service
bonus = base_salary * 0.1 * years # Same logic!
bonus += 5000 # Manager extra
return bonus
# ✅ Good: Extract common logic
def calculate_base_bonus ( person ):
return person.salary * 0.1 * person.years_of_service
def calculate_employee_bonus ( employee ):
return calculate_base_bonus(employee)
def calculate_manager_bonus ( manager ):
return calculate_base_bonus(manager) + 5000
KISS - Keep It Simple, Stupid
# ❌ Over-engineered
class StringReverserFactory :
def create_reverser ( self ):
return StringReverser()
class StringReverser :
def reverse ( self , s ):
return '' .join( reversed ( list (s)))
# ✅ Simple
def reverse_string ( s ):
return s[:: - 1 ]
YAGNI - You Aren’t Gonna Need It
Don’t add functionality until you actually need it.
# ❌ Bad: Building for hypothetical future
class User :
def __init__ ( self , name ):
self .name = name
self .middle_name = None
self .suffix = None
self .nickname = None
self .avatar_url = None
self .theme_preference = None
self .notification_settings = {}
# 20 more fields "just in case"
# ✅ Good: Start minimal, add when needed
class User :
def __init__ ( self , name , email ):
self .name = name
self .email = email
Composition Over Inheritance
Favor object composition over class inheritance.
# ❌ Bad: Deep inheritance hierarchy
class Animal :
def move ( self ): pass
class Bird ( Animal ):
def fly ( self ): pass
class Penguin ( Bird ): # Problem: Penguins can't fly!
def fly ( self ):
raise Exception ( "Can't fly" )
# ✅ Good: Composition with behaviors
class FlyBehavior :
def fly ( self ):
print ( "Flying!" )
class WalkBehavior :
def walk ( self ):
print ( "Walking!" )
class Bird :
def __init__ ( self , fly_behavior = None , walk_behavior = None ):
self .fly_behavior = fly_behavior
self .walk_behavior = walk_behavior
# Penguin walks but doesn't fly
penguin = Bird( walk_behavior = WalkBehavior())
# Eagle flies and walks
eagle = Bird( fly_behavior = FlyBehavior(), walk_behavior = WalkBehavior())
Law of Demeter (Principle of Least Knowledge)
A method should only call methods on:
Its own object
Objects passed as parameters
Objects it creates
Its direct component objects
# ❌ Bad: Train wreck - reaching through objects
def get_city ( order ):
return order.get_customer().get_address().get_city()
# ✅ Good: Ask, don't reach
class Order :
def get_shipping_city ( self ):
return self .customer.get_shipping_city()
class Customer :
def get_shipping_city ( self ):
return self .address.city
def get_city ( order ):
return order.get_shipping_city()
Design Patterns
Creational Patterns
Singleton - One instance only
class DatabaseConnection :
_instance = None
def __new__ ( cls ):
if cls ._instance is None :
cls ._instance = super (). __new__ ( cls )
cls ._instance.connection = cls ._create_connection()
return cls ._instance
@ staticmethod
def _create_connection ():
return "Connected to DB"
# Both are the same instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
assert db1 is db2
Factory - Create objects without specifying exact class
from abc import ABC , abstractmethod
class Notification ( ABC ):
@abstractmethod
def send ( self , message ): pass
class EmailNotification ( Notification ):
def send ( self , message ):
print ( f "Email: { message } " )
class SMSNotification ( Notification ):
def send ( self , message ):
print ( f "SMS: { message } " )
class NotificationFactory :
@ staticmethod
def create ( notification_type : str ) -> Notification:
if notification_type == "email" :
return EmailNotification()
elif notification_type == "sms" :
return SMSNotification()
raise ValueError ( f "Unknown type: { notification_type } " )
# Usage
notification = NotificationFactory.create( "email" )
notification.send( "Hello!" )
Builder - Construct complex objects step by step
class QueryBuilder :
def __init__ ( self ):
self ._select = "*"
self ._from = ""
self ._where = []
self ._order_by = None
self ._limit = None
def select ( self , columns ):
self ._select = columns
return self
def from_table ( self , table ):
self ._from = table
return self
def where ( self , condition ):
self ._where.append(condition)
return self
def order_by ( self , column ):
self ._order_by = column
return self
def limit ( self , n ):
self ._limit = n
return self
def build ( self ):
query = f "SELECT { self ._select } FROM { self ._from } "
if self ._where:
query += " WHERE " + " AND " .join( self ._where)
if self ._order_by:
query += f " ORDER BY { self ._order_by } "
if self ._limit:
query += f " LIMIT { self ._limit } "
return query
# Fluent interface
query = (QueryBuilder()
.select( "name, email" )
.from_table( "users" )
.where( "status = 'active'" )
.where( "age > 18" )
.order_by( "created_at DESC" )
.limit( 10 )
.build())
Structural Patterns
Adapter - Make incompatible interfaces work together
# Legacy XML service
class LegacyXMLParser :
def parse_xml ( self , xml_string ):
return { "data" : "parsed from XML" }
# Modern code expects JSON
class JSONAdapter :
def __init__ ( self , xml_parser ):
self .xml_parser = xml_parser
def parse_json ( self , json_string ):
# Convert JSON to XML, parse, return result
xml_data = self ._json_to_xml(json_string)
return self .xml_parser.parse_xml(xml_data)
def _json_to_xml ( self , json_string ):
return "<xml>converted</xml>"
# Usage
legacy_parser = LegacyXMLParser()
adapter = JSONAdapter(legacy_parser)
result = adapter.parse_json( '{"key": "value"}' )
Decorator - Add behavior without modifying class
from functools import wraps
import time
def timing_decorator ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
start = time.time()
result = func( * args, ** kwargs)
print ( f " { func. __name__ } took { time.time() - start :.2f} s" )
return result
return wrapper
def retry_decorator ( max_attempts = 3 ):
def decorator ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
for attempt in range (max_attempts):
try :
return func( * args, ** kwargs)
except Exception as e:
if attempt == max_attempts - 1 :
raise
print ( f "Attempt { attempt + 1 } failed, retrying..." )
return wrapper
return decorator
@timing_decorator
@retry_decorator ( max_attempts = 3 )
def fetch_data ( url ):
# Simulate API call
return { "data" : "fetched" }
Facade - Simplified interface to complex subsystem
class VideoConverter :
def convert ( self , filename , format ):
print ( f "Converting { filename } to { format } " )
class AudioExtractor :
def extract ( self , video ):
print ( f "Extracting audio from { video } " )
class ThumbnailGenerator :
def generate ( self , video ):
print ( f "Generating thumbnail for { video } " )
class UploadService :
def upload ( self , file , platform ):
print ( f "Uploading { file } to { platform } " )
# Facade simplifies the complex workflow
class VideoPublishingFacade :
def __init__ ( self ):
self .converter = VideoConverter()
self .audio = AudioExtractor()
self .thumbnail = ThumbnailGenerator()
self .uploader = UploadService()
def publish ( self , video_file , platform ):
self .converter.convert(video_file, "mp4" )
self .audio.extract(video_file)
self .thumbnail.generate(video_file)
self .uploader.upload(video_file, platform)
# Simple usage
facade = VideoPublishingFacade()
facade.publish( "my_video.mov" , "youtube" )
Behavioral Patterns
Strategy - Interchangeable algorithms
from abc import ABC , abstractmethod
class PaymentStrategy ( ABC ):
@abstractmethod
def pay ( self , amount ): pass
class CreditCardPayment ( PaymentStrategy ):
def pay ( self , amount ):
print ( f "Paid $ { amount } with credit card" )
class PayPalPayment ( PaymentStrategy ):
def pay ( self , amount ):
print ( f "Paid $ { amount } with PayPal" )
class CryptoPayment ( PaymentStrategy ):
def pay ( self , amount ):
print ( f "Paid $ { amount } with crypto" )
class ShoppingCart :
def __init__ ( self , payment_strategy : PaymentStrategy):
self .payment_strategy = payment_strategy
def checkout ( self , amount ):
self .payment_strategy.pay(amount)
# Easy to swap payment methods
cart = ShoppingCart(CreditCardPayment())
cart.checkout( 100 )
cart = ShoppingCart(CryptoPayment())
cart.checkout( 100 )
Observer - Notify multiple objects of state changes
class EventEmitter :
def __init__ ( self ):
self ._listeners = {}
def on ( self , event , callback ):
if event not in self ._listeners:
self ._listeners[event] = []
self ._listeners[event].append(callback)
def emit ( self , event , data = None ):
if event in self ._listeners:
for callback in self ._listeners[event]:
callback(data)
# Usage
emitter = EventEmitter()
def on_user_created ( user ):
print ( f "Send welcome email to { user[ 'email' ] } " )
def on_user_created_log ( user ):
print ( f "Log: User { user[ 'name' ] } created" )
emitter.on( "user_created" , on_user_created)
emitter.on( "user_created" , on_user_created_log)
emitter.emit( "user_created" , { "name" : "John" , "email" : "[email protected] " })
Command - Encapsulate requests as objects
from abc import ABC , abstractmethod
class Command ( ABC ):
@abstractmethod
def execute ( self ): pass
@abstractmethod
def undo ( self ): pass
class AddTextCommand ( Command ):
def __init__ ( self , document , text ):
self .document = document
self .text = text
def execute ( self ):
self .document.content += self .text
def undo ( self ):
self .document.content = self .document.content[: - len ( self .text)]
class Document :
def __init__ ( self ):
self .content = ""
self .history = []
def execute ( self , command ):
command.execute()
self .history.append(command)
def undo ( self ):
if self .history:
command = self .history.pop()
command.undo()
# Usage with undo support
doc = Document()
doc.execute(AddTextCommand(doc, "Hello " ))
doc.execute(AddTextCommand(doc, "World!" ))
print (doc.content) # "Hello World!"
doc.undo()
print (doc.content) # "Hello "
Clean Code Practices
Meaningful Names
Use intention-revealing names
Avoid abbreviations (use customerAddress not custAddr)
Be consistent (don’t mix get, fetch, retrieve)
Use domain vocabulary
Small Functions
Do one thing well
Few parameters (≤3, use objects for more)
No side effects (pure functions)
Single level of abstraction
Comments
Code should be self-documenting
Comment WHY, not WHAT
Keep comments updated (stale comments are worse than none)
Use for legal, warnings, TODOs
Error Handling
Use exceptions, not error codes
Provide context in error messages
Don’t return null (use Optional/Maybe)
Fail fast
Code Smells to Avoid
Smell Description Fix Long Method Function > 20 lines Extract smaller functions God Class Class does too much Split into focused classes Feature Envy Method uses other class’s data more Move method to that class Primitive Obsession Using primitives instead of objects Create value objects Magic Numbers Unexplained numeric literals Use named constants Duplicate Code Same code in multiple places Extract to shared function Deep Nesting Many levels of if/for Extract, early return, guard clauses
# ❌ Deep nesting
def process_order ( order ):
if order:
if order.is_valid():
if order.has_items():
if order.customer.has_credit():
# finally do something
pass
# ✅ Guard clauses (early return)
def process_order ( order ):
if not order:
return
if not order.is_valid():
raise InvalidOrderError()
if not order.has_items():
raise EmptyOrderError()
if not order.customer.has_credit():
raise InsufficientCreditError()
# Clean path - do the actual work
process(order)
Principles Cheat Sheet
Principle One-liner When to Apply SRP One reason to change Class has multiple unrelated methods OCP Extend, don’t modify Adding features requires changing existing code LSP Subtypes substitutable Subclass can’t fulfill parent’s contract ISP Small, focused interfaces Class implements methods it doesn’t need DIP Depend on abstractions High-level depends on low-level directly DRY Don’t repeat yourself Same logic in multiple places KISS Keep it simple Over-engineered solution YAGNI Build what you need now Building for hypothetical future
Common Mistake : Over-applying principles can lead to over-engineering. Use judgment—simple problems need simple solutions. The goal is maintainability, not pattern purity.
Interview Tip : When discussing design principles, always mention trade-offs. SOLID principles add abstraction which can increase complexity. The art is knowing when the benefits outweigh the costs.