You are viewing a preview of this lesson. Sign in to start learning
Back to Python Programming

OOP & Patterns

Object-oriented programming and design patterns

Object-Oriented Programming & Design Patterns in Python

Master object-oriented programming (OOP) and design patterns with free flashcards and spaced repetition practice. This comprehensive lesson covers class design, inheritance, polymorphism, SOLID principles, and essential design patterns like Singleton, Factory, and Observerโ€”critical skills for writing maintainable, scalable Python applications.

Welcome to OOP & Design Patterns! ๐Ÿ’ป

Welcome to one of the most transformative topics in Python programming! Object-oriented programming isn't just a feature of Pythonโ€”it's a fundamental way of thinking about code organization and problem-solving. Design patterns take this further by providing battle-tested solutions to common software design challenges.

In this lesson, you'll learn how to:

  • Design robust classes with proper encapsulation and abstraction
  • Leverage inheritance and polymorphism to write reusable code
  • Apply SOLID principles for maintainable software architecture
  • Implement proven design patterns that professional developers use daily
  • Refactor procedural code into elegant object-oriented solutions

Whether you're building web applications, data pipelines, or automation tools, these concepts will elevate your Python skills to a professional level.


Core Concepts of Object-Oriented Programming ๐ŸŽฏ

Classes and Objects: The Foundation

Classes are blueprints that define the structure and behavior of objects. An object (or instance) is a specific realization of a class with actual data.

Key terminology:

  • Attributes: Variables that store object state (data)
  • Methods: Functions that define object behavior (actions)
  • Constructor: Special __init__ method that initializes new objects
  • self: Reference to the current instance
class Dog:
    # Class attribute (shared by all instances)
    species = "Canis familiaris"
    
    def __init__(self, name, age):
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
    
    def bark(self):
        return f"{self.name} says Woof!"
    
    def celebrate_birthday(self):
        self.age += 1
        return f"{self.name} is now {self.age} years old!"

# Creating objects
buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print(buddy.bark())  # Output: Buddy says Woof!
print(buddy.species)  # Output: Canis familiaris

๐Ÿ’ก Memory tip: Think of a class as a cookie cutter, and objects as the individual cookies it producesโ€”same shape, different decorations!

The Four Pillars of OOP ๐Ÿ›๏ธ

Pillar Description Python Implementation
Encapsulation Bundling data with methods that operate on that data; hiding internal details Private attributes with _ or __ prefix
Abstraction Exposing only essential features while hiding complexity Abstract base classes (ABC module)
Inheritance Creating new classes based on existing ones class Child(Parent):
Polymorphism Objects of different types responding to the same interface Method overriding, duck typing

Encapsulation: Protecting Your Data ๐Ÿ”’

Encapsulation controls access to object internals, preventing accidental modification and maintaining data integrity.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # Private attribute (name mangling)
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid amount"
    
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return f"Withdrew ${amount}. Remaining: ${self.__balance}"
        return "Insufficient funds or invalid amount"
    
    @property
    def balance(self):
        """Read-only access to balance"""
        return self.__balance

account = BankAccount("Alice", 1000)
print(account.deposit(500))  # Works through public method
print(account.balance)  # Read-only property: 1500
# account.__balance = 999999  # Won't affect actual balance (name mangling)

Privacy conventions in Python:

  • public_attr: Public, accessible anywhere
  • _protected_attr: Protected, internal use (convention only)
  • __private_attr: Private, name mangled to _ClassName__private_attr

โš ๏ธ Important: Python doesn't enforce true private accessโ€”it's more about signaling intent to other developers!

Inheritance: Building Class Hierarchies ๐ŸŒณ

Inheritance allows you to create specialized classes that inherit attributes and methods from parent classes.

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclass must implement speak()")
    
    def introduce(self):
        return f"I am {self.name}"

class Cat(Animal):
    def speak(self):
        return "Meow!"
    
    def purr(self):
        return f"{self.name} purrs contentedly"

class Dog(Animal):
    def speak(self):
        return "Woof!"
    
    def fetch(self):
        return f"{self.name} fetches the ball"

# Polymorphism in action
animals = [Cat("Whiskers"), Dog("Rex"), Cat("Mittens")]

for animal in animals:
    print(f"{animal.introduce()}: {animal.speak()}")
INHERITANCE HIERARCHY

       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚   Animal    โ”‚
       โ”‚  (abstract) โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚             โ”‚
  โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
  โ”‚   Cat   โ”‚   โ”‚   Dog   โ”‚
  โ”‚ speak() โ”‚   โ”‚ speak() โ”‚
  โ”‚ purr()  โ”‚   โ”‚ fetch() โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Multiple inheritance (use cautiously!):

class Flyer:
    def fly(self):
        return "Flying through the air!"

class Swimmer:
    def swim(self):
        return "Swimming in water!"

class Duck(Animal, Flyer, Swimmer):
    def speak(self):
        return "Quack!"

duck = Duck("Donald")
print(duck.speak())  # From Duck
print(duck.fly())    # From Flyer
print(duck.swim())   # From Swimmer

๐Ÿง  MRO Memory Trick: Python uses Method Resolution Order (MRO) to determine which method to call. Use ClassName.__mro__ to see the search order!

Polymorphism: One Interface, Many Forms ๐ŸŽญ

Polymorphism allows objects of different classes to be treated uniformly through a common interface.

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return f"Processing ${amount} via Credit Card"

class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return f"Processing ${amount} via PayPal"

class CryptoProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return f"Processing ${amount} via Cryptocurrency"

def checkout(processor: PaymentProcessor, amount: float):
    """Works with ANY payment processor!"""
    return processor.process_payment(amount)

# All three work with the same function
print(checkout(CreditCardProcessor(), 99.99))
print(checkout(PayPalProcessor(), 149.50))
print(checkout(CryptoProcessor(), 299.00))

๐Ÿ’ก Duck typing: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't require explicit inheritanceโ€”any object with the right methods will work!


SOLID Principles: Writing Clean OOP Code ๐Ÿ“

The SOLID principles are five guidelines for creating maintainable, flexible object-oriented software.

S - Single Responsibility Principle (SRP)

"A class should have only one reason to change."

โŒ Bad: Class doing too many things

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def save_to_database(self):
        # Database logic
        pass
    
    def send_welcome_email(self):
        # Email logic
        pass
    
    def generate_report(self):
        # Report generation logic
        pass

โœ… Good: Separate concerns

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_welcome_email(self, user):
        # Email logic only
        pass

class ReportGenerator:
    def generate_user_report(self, user):
        # Reporting logic only
        pass

O - Open/Closed Principle (OCP)

"Classes should be open for extension, but closed for modification."

โœ… Good: Extend behavior without modifying existing code

class Discount(ABC):
    @abstractmethod
    def calculate(self, amount):
        pass

class NoDiscount(Discount):
    def calculate(self, amount):
        return amount

class PercentageDiscount(Discount):
    def __init__(self, percentage):
        self.percentage = percentage
    
    def calculate(self, amount):
        return amount * (1 - self.percentage / 100)

class FixedDiscount(Discount):
    def __init__(self, fixed_amount):
        self.fixed_amount = fixed_amount
    
    def calculate(self, amount):
        return max(0, amount - self.fixed_amount)

# Add new discount types without changing existing code!

L - Liskov Substitution Principle (LSP)

"Derived classes must be substitutable for their base classes."

Subclasses should strengthen, not weaken, the parent class contract.

I - Interface Segregation Principle (ISP)

"Clients shouldn't be forced to depend on interfaces they don't use."

โœ… Good: Small, focused interfaces

class Readable(ABC):
    @abstractmethod
    def read(self):
        pass

class Writable(ABC):
    @abstractmethod
    def write(self, data):
        pass

class ReadOnlyFile(Readable):
    def read(self):
        return "Reading data..."

class ReadWriteFile(Readable, Writable):
    def read(self):
        return "Reading data..."
    
    def write(self, data):
        return f"Writing {data}..."

D - Dependency Inversion Principle (DIP)

"Depend on abstractions, not concretions."

โœ… Good: Inject dependencies

class Database(ABC):
    @abstractmethod
    def save(self, data):
        pass

class MySQLDatabase(Database):
    def save(self, data):
        return f"Saving to MySQL: {data}"

class UserService:
    def __init__(self, database: Database):  # Depends on abstraction
        self.database = database
    
    def create_user(self, user_data):
        return self.database.save(user_data)

# Easy to swap database implementations!
service = UserService(MySQLDatabase())

๐Ÿง  SOLID Memory Device: "Some Officers Love Ice-cream Daily"

  • Single Responsibility
  • Open/Closed
  • Liskov Substitution
  • Interface Segregation
  • Dependency Inversion

Essential Design Patterns ๐ŸŽจ

Design patterns are reusable solutions to common software design problems. Let's explore the most important ones!

Creational Patterns: Object Creation

1. Singleton Pattern ๐ŸŽฏ

Purpose: Ensure a class has only one instance and provide global access to it.

Use cases: Configuration managers, logging, database connections

class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connection = "Database connected!"
        return cls._instance
    
    def query(self, sql):
        return f"Executing: {sql}"

# Always returns the same instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True - same object!

Thread-safe Singleton (better approach):

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # Double-checked locking
                    cls._instance = super().__new__(cls)
        return cls._instance

2. Factory Pattern ๐Ÿญ

Purpose: Create objects without specifying exact classes.

from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class WindowsButton(Button):
    def render(self):
        return "Rendering Windows-style button"

class MacButton(Button):
    def render(self):
        return "Rendering Mac-style button"

class ButtonFactory:
    @staticmethod
    def create_button(os_type):
        if os_type == "Windows":
            return WindowsButton()
        elif os_type == "Mac":
            return MacButton()
        else:
            raise ValueError(f"Unknown OS: {os_type}")

# Client code doesn't need to know specific classes
button = ButtonFactory.create_button("Windows")
print(button.render())

Structural Patterns: Object Composition

3. Decorator Pattern ๐ŸŽ€

Purpose: Add behavior to objects dynamically without modifying their code.

class Coffee:
    def cost(self):
        return 5.0
    
    def description(self):
        return "Plain coffee"

class CoffeeDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    
    def cost(self):
        return self._coffee.cost()
    
    def description(self):
        return self._coffee.description()

class Milk(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 1.5
    
    def description(self):
        return self._coffee.description() + ", milk"

class Sugar(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.5
    
    def description(self):
        return self._coffee.description() + ", sugar"

# Build complex objects dynamically
coffee = Coffee()
coffee_with_milk = Milk(coffee)
coffee_with_milk_and_sugar = Sugar(coffee_with_milk)

print(coffee_with_milk_and_sugar.description())  # Plain coffee, milk, sugar
print(coffee_with_milk_and_sugar.cost())  # 7.0

4. Adapter Pattern ๐Ÿ”Œ

Purpose: Make incompatible interfaces work together.

class EuropeanSocket:
    def provide_240v(self):
        return "240V European power"

class AmericanPlug:
    def request_110v(self):
        return "Requesting 110V American power"

class PowerAdapter:
    def __init__(self, socket):
        self.socket = socket
    
    def request_110v(self):
        power = self.socket.provide_240v()
        return f"Adapting {power} to 110V"

# Use European socket with American device
euros_ocket = EuropeanSocket()
adapter = PowerAdapter(european_socket)
american_device = AmericanPlug()
print(adapter.request_110v())  # Works!

Behavioral Patterns: Object Interaction

5. Observer Pattern ๐Ÿ‘€

Purpose: Notify multiple objects about state changes (publish-subscribe).

class Subject:
    def __init__(self):
        self._observers = []
        self._state = None
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def detach(self, observer):
        self._observers.remove(observer)
    
    def notify(self):
        for observer in self._observers:
            observer.update(self._state)
    
    def set_state(self, state):
        self._state = state
        self.notify()

class Observer(ABC):
    @abstractmethod
    def update(self, state):
        pass

class EmailAlert(Observer):
    def update(self, state):
        print(f"Email Alert: State changed to {state}")

class SMSAlert(Observer):
    def update(self, state):
        print(f"SMS Alert: State changed to {state}")

# Usage
subject = Subject()
email = EmailAlert()
sms = SMSAlert()

subject.attach(email)
subject.attach(sms)

subject.set_state("CRITICAL")  # Both observers notified

6. Strategy Pattern ๐ŸŽฎ

Purpose: Define a family of algorithms and make them interchangeable.

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data):
        pass

class BubbleSort(SortStrategy):
    def sort(self, data):
        return sorted(data)  # Simplified

class QuickSort(SortStrategy):
    def sort(self, data):
        return sorted(data)  # Simplified

class DataProcessor:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy
    
    def set_strategy(self, strategy: SortStrategy):
        self.strategy = strategy
    
    def process(self, data):
        return self.strategy.sort(data)

# Change algorithm at runtime
processor = DataProcessor(BubbleSort())
print(processor.process([3, 1, 4, 1, 5]))

processor.set_strategy(QuickSort())
print(processor.process([3, 1, 4, 1, 5]))
DESIGN PATTERN CATEGORIES

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         DESIGN PATTERNS                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                             โ”‚
โ”‚  ๐Ÿ—๏ธ CREATIONAL (Object Creation)          โ”‚
โ”‚  โ”œโ”€ Singleton                               โ”‚
โ”‚  โ”œโ”€ Factory                                 โ”‚
โ”‚  โ”œโ”€ Abstract Factory                        โ”‚
โ”‚  โ””โ”€ Builder                                 โ”‚
โ”‚                                             โ”‚
โ”‚  ๐Ÿ”ง STRUCTURAL (Object Composition)         โ”‚
โ”‚  โ”œโ”€ Adapter                                 โ”‚
โ”‚  โ”œโ”€ Decorator                               โ”‚
โ”‚  โ”œโ”€ Facade                                  โ”‚
โ”‚  โ””โ”€ Proxy                                   โ”‚
โ”‚                                             โ”‚
โ”‚  ๐ŸŽฏ BEHAVIORAL (Object Interaction)         โ”‚
โ”‚  โ”œโ”€ Observer                                โ”‚
โ”‚  โ”œโ”€ Strategy                                โ”‚
โ”‚  โ”œโ”€ Command                                 โ”‚
โ”‚  โ””โ”€ Iterator                                โ”‚
โ”‚                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Real-World Examples ๐ŸŒ

Example 1: E-commerce Order System

Combining multiple OOP concepts and patterns:

from abc import ABC, abstractmethod
from datetime import datetime
from typing import List

# Strategy Pattern for payment
class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount: float) -> str:
        pass

class CreditCard(PaymentMethod):
    def process(self, amount: float) -> str:
        return f"Charged ${amount} to credit card"

class PayPal(PaymentMethod):
    def process(self, amount: float) -> str:
        return f"Charged ${amount} via PayPal"

# Observer Pattern for notifications
class OrderObserver(ABC):
    @abstractmethod
    def notify(self, order):
        pass

class EmailNotification(OrderObserver):
    def notify(self, order):
        print(f"Email: Order {order.order_id} placed for ${order.total}")

class SMSNotification(OrderObserver):
    def notify(self, order):
        print(f"SMS: Order {order.order_id} confirmed")

# Main business logic with encapsulation
class Order:
    _order_counter = 1000  # Class variable for order IDs
    
    def __init__(self, customer_name: str):
        Order._order_counter += 1
        self.order_id = Order._order_counter
        self.customer_name = customer_name
        self.__items = []  # Private
        self.__observers: List[OrderObserver] = []
        self.timestamp = datetime.now()
    
    def add_item(self, item: str, price: float):
        self.__items.append({"item": item, "price": price})
    
    @property
    def total(self) -> float:
        return sum(item["price"] for item in self.__items)
    
    def attach_observer(self, observer: OrderObserver):
        self.__observers.append(observer)
    
    def place_order(self, payment_method: PaymentMethod):
        payment_result = payment_method.process(self.total)
        print(payment_result)
        
        # Notify all observers
        for observer in self.__observers:
            observer.notify(self)
        
        return f"Order {self.order_id} placed successfully!"

# Usage
order = Order("Alice Johnson")
order.add_item("Laptop", 999.99)
order.add_item("Mouse", 29.99)

# Attach observers
order.attach_observer(EmailNotification())
order.attach_observer(SMSNotification())

# Process payment
result = order.place_order(CreditCard())
print(result)

Output:

Charged $1029.98 to credit card
Email: Order 1001 placed for $1029.98
SMS: Order 1001 confirmed
Order 1001 placed successfully!

Example 2: Plugin System with Abstract Base Classes

from abc import ABC, abstractmethod
from typing import Dict, Type

class Plugin(ABC):
    @abstractmethod
    def execute(self, data: str) -> str:
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        pass

class UpperCasePlugin(Plugin):
    def execute(self, data: str) -> str:
        return data.upper()
    
    def get_name(self) -> str:
        return "UpperCase"

class ReversePlugin(Plugin):
    def execute(self, data: str) -> str:
        return data[::-1]
    
    def get_name(self) -> str:
        return "Reverse"

class PluginManager:
    def __init__(self):
        self._plugins: Dict[str, Plugin] = {}
    
    def register(self, plugin: Plugin):
        self._plugins[plugin.get_name()] = plugin
        print(f"Registered plugin: {plugin.get_name()}")
    
    def execute_plugin(self, plugin_name: str, data: str):
        if plugin_name in self._plugins:
            return self._plugins[plugin_name].execute(data)
        return f"Plugin '{plugin_name}' not found"

# Usage
manager = PluginManager()
manager.register(UpperCasePlugin())
manager.register(ReversePlugin())

print(manager.execute_plugin("UpperCase", "hello"))  # HELLO
print(manager.execute_plugin("Reverse", "hello"))    # olleh

Example 3: Game Character System with Inheritance

class Character:
    def __init__(self, name: str, health: int):
        self.name = name
        self._health = health
        self._max_health = health
    
    @property
    def health(self):
        return self._health
    
    def take_damage(self, damage: int):
        self._health = max(0, self._health - damage)
        status = "alive" if self._health > 0 else "defeated"
        return f"{self.name} took {damage} damage. HP: {self._health}/{self._max_health} ({status})"
    
    def attack(self) -> int:
        return 10  # Base attack

class Warrior(Character):
    def __init__(self, name: str):
        super().__init__(name, health=150)
        self.armor = 20
    
    def attack(self) -> int:
        return 25  # Warriors hit harder
    
    def take_damage(self, damage: int):
        reduced_damage = max(0, damage - self.armor)
        return super().take_damage(reduced_damage)
    
    def shield_bash(self):
        return f"{self.name} performs a shield bash for 30 damage!"

class Mage(Character):
    def __init__(self, name: str):
        super().__init__(name, health=80)
        self.mana = 100
    
    def attack(self) -> int:
        return 15
    
    def cast_spell(self, spell_name: str):
        if self.mana >= 20:
            self.mana -= 20
            return f"{self.name} casts {spell_name}! Mana: {self.mana}/100"
        return f"{self.name} has insufficient mana!"

# Polymorphism in action
def battle(attacker: Character, defender: Character):
    damage = attacker.attack()
    result = defender.take_damage(damage)
    return f"{attacker.name} attacks! {result}"

warrior = Warrior("Thorin")
mage = Mage("Gandalf")

print(battle(warrior, mage))
print(mage.cast_spell("Fireball"))
print(warrior.shield_bash())

Example 4: Document Processing Pipeline

Demonstrating Decorator pattern and method chaining:

class Document:
    def __init__(self, content: str):
        self.content = content
    
    def get_content(self) -> str:
        return self.content

class DocumentProcessor(ABC):
    def __init__(self, document):
        self._document = document
    
    @abstractmethod
    def get_content(self) -> str:
        pass

class RemoveWhitespace(DocumentProcessor):
    def get_content(self) -> str:
        content = self._document.get_content()
        return ' '.join(content.split())

class ToUpperCase(DocumentProcessor):
    def get_content(self) -> str:
        content = self._document.get_content()
        return content.upper()

class AddTimestamp(DocumentProcessor):
    def get_content(self) -> str:
        content = self._document.get_content()
        return f"[{datetime.now()}] {content}"

# Chain decorators
doc = Document("  hello    world  ")
processed = AddTimestamp(ToUpperCase(RemoveWhitespace(doc)))
print(processed.get_content())
# Output: [2024-01-15 10:30:45.123456] HELLO WORLD

๐Ÿค” Did you know? The Gang of Four (GoF) book "Design Patterns" published in 1994 documented 23 design patterns that are still relevant today. Many modern frameworks are built on these foundational patterns!


Common Mistakes & How to Avoid Them โš ๏ธ

Mistake 1: God Objects (Violating SRP)

โŒ Wrong: One class doing everything

class Application:
    def __init__(self):
        self.users = []
        self.products = []
    
    def add_user(self, user): pass
    def send_email(self, to, subject): pass
    def process_payment(self, amount): pass
    def generate_report(self): pass
    def log_activity(self, message): pass
    # ... 50 more methods

โœ… Right: Separate concerns

class UserRepository:
    def add_user(self, user): pass

class EmailService:
    def send(self, to, subject): pass

class PaymentProcessor:
    def process(self, amount): pass

Mistake 2: Improper Use of Inheritance

โŒ Wrong: Inheritance for code reuse (should use composition)

class ArrayList:  # Has list functionality
    def add(self, item): pass

class Stack(ArrayList):  # Inherits list methods
    def push(self, item):
        self.add(item)  # But exposes ALL list methods!

โœ… Right: Composition over inheritance

class Stack:
    def __init__(self):
        self._items = []  # Composition
    
    def push(self, item):
        self._items.append(item)
    
    def pop(self):
        return self._items.pop()
    # Only expose stack-specific interface

Mistake 3: Forgetting self in Methods

โŒ Wrong:

class Counter:
    def __init__(self):
        self.count = 0
    
    def increment():  # Missing self!
        self.count += 1  # Error!

โœ… Right:

class Counter:
    def __init__(self):
        self.count = 0
    
    def increment(self):  # Include self
        self.count += 1

Mistake 4: Mutable Default Arguments

โŒ Wrong:

class ShoppingCart:
    def __init__(self, items=[]):  # DANGER!
        self.items = items

cart1 = ShoppingCart()
cart1.items.append("apple")
cart2 = ShoppingCart()  # Shares items with cart1!
print(cart2.items)  # ['apple'] - unexpected!

โœ… Right:

class ShoppingCart:
    def __init__(self, items=None):
        self.items = items if items is not None else []

Mistake 5: Overusing Singletons

Problem: Singletons are essentially global state, making testing difficult and creating hidden dependencies.

โœ… Better: Use dependency injection instead:

class Service:
    def __init__(self, config):  # Inject dependencies
        self.config = config

# In tests, inject mock config
test_service = Service(MockConfig())

Mistake 6: Not Using Abstract Base Classes

โŒ Wrong: Forgetting to implement required methods

class Animal:
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    pass  # Forgot to implement speak()!

dog = Dog()  # No error until runtime
dog.speak()  # Error only when called!

โœ… Right: Use ABC for compile-time checking

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):  # Error immediately if speak() missing
    def speak(self):
        return "Woof!"

๐Ÿ”ง Try this: Take an existing procedural program you've written and refactor it using OOP principles. Identify natural classes, apply encapsulation, and see how it improves maintainability!


Key Takeaways ๐ŸŽ“

โœ… Classes and objects provide structure for organizing related data and behavior

โœ… The four OOP pillars (Encapsulation, Abstraction, Inheritance, Polymorphism) work together to create flexible, maintainable code

โœ… SOLID principles guide you toward clean, professional OOP design:

  • Single Responsibility: One class, one purpose
  • Open/Closed: Extend, don't modify
  • Liskov Substitution: Subclasses must honor parent contracts
  • Interface Segregation: Many small interfaces beat one large one
  • Dependency Inversion: Depend on abstractions

โœ… Design patterns provide proven solutions to recurring problems:

  • Creational (Singleton, Factory): Object creation
  • Structural (Decorator, Adapter): Object composition
  • Behavioral (Observer, Strategy): Object interaction

โœ… Composition over inheritance: Prefer "has-a" relationships over "is-a" when possible

โœ… Abstract base classes enforce contracts and catch errors early

โœ… Properties and name mangling help maintain encapsulation in Python

โœ… Avoid common pitfalls: God objects, mutable defaults, excessive inheritance, Singleton overuse


๐Ÿ“š Further Study

Essential resources for deepening your OOP & patterns knowledge:

  1. Python's Official Documentation on Classes: https://docs.python.org/3/tutorial/classes.html - Comprehensive guide to Python's OOP features with excellent examples

  2. Refactoring.Guru Design Patterns: https://refactoring.guru/design-patterns/python - Interactive catalog of design patterns with Python implementations, visual diagrams, and real-world use cases

  3. Real Python OOP Tutorial: https://realpython.com/python3-object-oriented-programming/ - In-depth tutorial covering advanced OOP concepts, best practices, and Python-specific idioms


๐Ÿ“‹ Quick Reference Card

Concept Key Points Python Syntax
Class Definition Blueprint for objects class MyClass:
Constructor Initialize instances def __init__(self, params):
Encapsulation Private: __ prefix self.__private_attr
Inheritance Extend parent class class Child(Parent):
Abstract Class Cannot instantiate class Base(ABC):
Abstract Method Must implement in subclass @abstractmethod
Property Getter/setter methods @property
Singleton One instance only Override __new__
Factory Create objects dynamically Static method returns instances
Decorator Pattern Add behavior dynamically Wrapper classes
Observer Pattern Notify on state change attach/notify methods
Strategy Pattern Interchangeable algorithms Inject behavior via constructor

SOLID Quick Reference:

  • S: One class = one responsibility
  • O: Extend, don't modify
  • L: Subclasses honor parent contracts
  • I: Small, focused interfaces
  • D: Depend on abstractions

When to Use Which Pattern:

  • Need one instance? โ†’ Singleton
  • Need flexible object creation? โ†’ Factory
  • Need to add features dynamically? โ†’ Decorator
  • Need to notify multiple objects? โ†’ Observer
  • Need interchangeable algorithms? โ†’ Strategy
  • Need to make incompatible interfaces work? โ†’ Adapter