Skip to main content

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

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
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!")
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

# 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"}')
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"}
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

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)
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]"})
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

SmellDescriptionFix
Long MethodFunction > 20 linesExtract smaller functions
God ClassClass does too muchSplit into focused classes
Feature EnvyMethod uses other class’s data moreMove method to that class
Primitive ObsessionUsing primitives instead of objectsCreate value objects
Magic NumbersUnexplained numeric literalsUse named constants
Duplicate CodeSame code in multiple placesExtract to shared function
Deep NestingMany levels of if/forExtract, 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

PrincipleOne-linerWhen to Apply
SRPOne reason to changeClass has multiple unrelated methods
OCPExtend, don’t modifyAdding features requires changing existing code
LSPSubtypes substitutableSubclass can’t fulfill parent’s contract
ISPSmall, focused interfacesClass implements methods it doesn’t need
DIPDepend on abstractionsHigh-level depends on low-level directly
DRYDon’t repeat yourselfSame logic in multiple places
KISSKeep it simpleOver-engineered solution
YAGNIBuild what you need nowBuilding 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.