Object-Oriented Programming and SOLID Principles
š Summary
This guide covers the four pillars of OOP (Encapsulation, Abstraction, Inheritance, Polymorphism) and the five SOLID principles for writing maintainable, extensible code.
1ļøā£ Four Pillars of OOP
Encapsulation
Bundling data and methods that operate on that data within a single unit (class), and restricting direct access to some components.
class BankAccount:
"""Encapsulation example: internal state is protected."""
def __init__(self, owner: str, initial_balance: float = 0):
self._owner = owner # Protected (convention)
self.__balance = initial_balance # Private (name mangling)
@property
def balance(self) -> float:
"""Read-only access to balance."""
return self.__balance
@property
def owner(self) -> str:
return self._owner
def deposit(self, amount: float) -> None:
"""Controlled modification of balance."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
def withdraw(self, amount: float) -> bool:
"""Controlled modification with validation."""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
return False
self.__balance -= amount
return True
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.balance) # 1500 (via property)
# account.__balance = 999999 # Won't work - name mangling
Abstraction
Hiding complex implementation details and exposing only necessary interfaces.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract base class defining payment interface."""
@abstractmethod
def process_payment(self, amount: float) -> bool:
"""Process a payment. Must be implemented by subclasses."""
pass
@abstractmethod
def refund(self, transaction_id: str) -> bool:
"""Refund a transaction. Must be implemented by subclasses."""
pass
class StripeProcessor(PaymentProcessor):
"""Concrete implementation hiding Stripe API complexity."""
def __init__(self, api_key: str):
self._api_key = api_key
def process_payment(self, amount: float) -> bool:
# Complex Stripe API interaction hidden here
print(f"Processing ${amount} via Stripe...")
return True
def refund(self, transaction_id: str) -> bool:
print(f"Refunding transaction {transaction_id} via Stripe...")
return True
class PayPalProcessor(PaymentProcessor):
"""Concrete implementation for PayPal."""
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via PayPal...")
return True
def refund(self, transaction_id: str) -> bool:
print(f"Refunding {transaction_id} via PayPal...")
return True
# Client code doesn't need to know implementation details
def checkout(processor: PaymentProcessor, amount: float) -> None:
if processor.process_payment(amount):
print("Payment successful!")
Inheritance
Creating new classes based on existing classes, inheriting their attributes and methods.
class Animal:
"""Base class for all animals."""
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def speak(self) -> str:
return "Some sound"
def info(self) -> str:
return f"{self.name} is {self.age} years old"
class Dog(Animal):
"""Dog inherits from Animal."""
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # Call parent constructor
self.breed = breed
def speak(self) -> str: # Override parent method
return "Woof!"
def fetch(self) -> str: # Dog-specific method
return f"{self.name} is fetching!"
class Cat(Animal):
"""Cat inherits from Animal."""
def speak(self) -> str:
return "Meow!"
def scratch(self) -> str:
return f"{self.name} is scratching!"
# Usage
dog = Dog("Buddy", 3, "Golden Retriever")
print(dog.speak()) # "Woof!"
print(dog.info()) # "Buddy is 3 years old" (inherited)
Polymorphism
Objects of different types can be treated uniformly through a common interface.
from typing import List
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
# Polymorphism in action
def total_area(shapes: List[Shape]) -> float:
"""Works with any Shape subclass."""
return sum(shape.area() for shape in shapes)
shapes = [Rectangle(4, 5), Circle(3), Rectangle(2, 3)]
print(total_area(shapes)) # Works regardless of specific shape types
2ļøā£ SOLID Principles
S - Single Responsibility Principle (SRP)
A class should have only one reason to change.
# ā BAD: Class has multiple responsibilities
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def save_to_database(self):
# Database logic
pass
def send_email(self, message: str):
# Email logic
pass
def generate_report(self):
# Report generation logic
pass
# ā
GOOD: Separate responsibilities into different classes
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class UserRepository:
def save(self, user: User) -> None:
# Database logic only
pass
def find_by_email(self, email: str) -> User:
pass
class EmailService:
def send(self, to: str, subject: str, body: str) -> None:
# Email logic only
pass
class UserReportGenerator:
def generate(self, user: User) -> str:
# Report logic only
pass
O - Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
# ā BAD: Need to modify class to add new discount types
class DiscountCalculator:
def calculate(self, order_total: float, discount_type: str) -> float:
if discount_type == "percentage":
return order_total * 0.1
elif discount_type == "fixed":
return 10.0
elif discount_type == "bogo": # Added later - modifies existing class
return order_total * 0.5
return 0
# ā
GOOD: Extend behavior without modifying existing code
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order_total: float) -> float:
pass
class PercentageDiscount(DiscountStrategy):
def __init__(self, percentage: float):
self.percentage = percentage
def calculate(self, order_total: float) -> float:
return order_total * self.percentage
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def calculate(self, order_total: float) -> float:
return min(self.amount, order_total)
class BOGODiscount(DiscountStrategy):
"""New discount type - no modification to existing code!"""
def calculate(self, order_total: float) -> float:
return order_total * 0.5
class Order:
def __init__(self, total: float, discount: DiscountStrategy = None):
self.total = total
self.discount = discount
def final_price(self) -> float:
if self.discount:
return self.total - self.discount.calculate(self.total)
return self.total
L - Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
# ā BAD: Square violates LSP when inheriting from Rectangle
class Rectangle:
def __init__(self, width: float, height: float):
self._width = width
self._height = height
@property
def width(self) -> float:
return self._width
@width.setter
def width(self, value: float) -> None:
self._width = value
@property
def height(self) -> float:
return self._height
@height.setter
def height(self, value: float) -> None:
self._height = value
def area(self) -> float:
return self._width * self._height
class Square(Rectangle): # Problematic inheritance
def __init__(self, side: float):
super().__init__(side, side)
@Rectangle.width.setter
def width(self, value: float) -> None:
self._width = value
self._height = value # Forces height to change too!
@Rectangle.height.setter
def height(self, value: float) -> None:
self._width = value
self._height = value
# This breaks expectations:
def resize_rectangle(rect: Rectangle):
rect.width = 5
rect.height = 10
assert rect.area() == 50 # Fails for Square!
# ā
GOOD: Use composition or separate hierarchies
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don’t use.
# ā BAD: Fat interface forces implementations to have unused methods
class Worker(ABC):
@abstractmethod
def work(self) -> None:
pass
@abstractmethod
def eat(self) -> None:
pass
@abstractmethod
def sleep(self) -> None:
pass
class Robot(Worker): # Robot doesn't eat or sleep!
def work(self) -> None:
print("Robot working")
def eat(self) -> None:
pass # Forced to implement useless method
def sleep(self) -> None:
pass # Forced to implement useless method
# ā
GOOD: Segregated interfaces
class Workable(ABC):
@abstractmethod
def work(self) -> None:
pass
class Eatable(ABC):
@abstractmethod
def eat(self) -> None:
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self) -> None:
pass
class Human(Workable, Eatable, Sleepable):
def work(self) -> None:
print("Human working")
def eat(self) -> None:
print("Human eating")
def sleep(self) -> None:
print("Human sleeping")
class Robot(Workable): # Only implements what it needs
def work(self) -> None:
print("Robot working")
D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
# ā BAD: High-level module depends on low-level module
class MySQLDatabase:
def query(self, sql: str) -> list:
# MySQL specific implementation
pass
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Tight coupling to MySQL
def get_users(self) -> list:
return self.db.query("SELECT * FROM users")
# ā
GOOD: Depend on abstractions
class Database(ABC):
@abstractmethod
def query(self, sql: str) -> list:
pass
class MySQLDatabase(Database):
def query(self, sql: str) -> list:
print("MySQL query")
return []
class PostgreSQLDatabase(Database):
def query(self, sql: str) -> list:
print("PostgreSQL query")
return []
class UserService:
def __init__(self, database: Database): # Depends on abstraction
self.db = database
def get_users(self) -> list:
return self.db.query("SELECT * FROM users")
# Easy to swap implementations
mysql_service = UserService(MySQLDatabase())
postgres_service = UserService(PostgreSQLDatabase())
3ļøā£ Design Patterns Quick Reference
Creational Patterns
# Singleton
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Factory Method
class AnimalFactory:
@staticmethod
def create(animal_type: str) -> Animal:
if animal_type == "dog":
return Dog("Buddy", 3, "Lab")
elif animal_type == "cat":
return Cat("Whiskers", 2)
raise ValueError(f"Unknown animal type: {animal_type}")
# Builder
class Pizza:
def __init__(self):
self.size = None
self.cheese = False
self.pepperoni = False
self.mushrooms = False
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_size(self, size: str) -> 'PizzaBuilder':
self.pizza.size = size
return self
def add_cheese(self) -> 'PizzaBuilder':
self.pizza.cheese = True
return self
def add_pepperoni(self) -> 'PizzaBuilder':
self.pizza.pepperoni = True
return self
def build(self) -> Pizza:
return self.pizza
# Usage: pizza = PizzaBuilder().set_size("large").add_cheese().add_pepperoni().build()
Structural Patterns
# Adapter
class LegacyPrinter:
def print_document(self, text: str) -> None:
print(f"Legacy: {text}")
class ModernPrinter(ABC):
@abstractmethod
def print(self, content: str) -> None:
pass
class PrinterAdapter(ModernPrinter):
def __init__(self, legacy_printer: LegacyPrinter):
self.legacy = legacy_printer
def print(self, content: str) -> None:
self.legacy.print_document(content)
# Decorator
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0
class MilkDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost() + 0.5
# Usage: coffee = MilkDecorator(SimpleCoffee()) # cost = 2.5
Behavioral Patterns
# Strategy
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list:
pass
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
# Quick sort implementation
return sorted(data)
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data)
# Observer
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer) -> None:
self._observers.append(observer)
def notify(self, message: str) -> None:
for observer in self._observers:
observer.update(message)
class Observer(ABC):
@abstractmethod
def update(self, message: str) -> None:
pass
š Interview Questions
Common OOP Questions
- What are the four pillars of OOP?
- Explain the difference between abstraction and encapsulation.
- What is the diamond problem in multiple inheritance?
- When would you use composition over inheritance?
- What is method overloading vs method overriding?
SOLID Questions
- Explain each SOLID principle with examples.
- How does DIP help with testing?
- Give an example of LSP violation.
- How would you refactor a god class using SRP?
- What’s the relationship between OCP and Strategy pattern?
š Key Takeaways
- Encapsulation: Hide internal state, expose controlled access
- Abstraction: Define interfaces, hide complexity
- Inheritance: Reuse code, establish “is-a” relationships
- Polymorphism: Program to interfaces, not implementations
- SOLID: Write maintainable, extensible, testable code
Last Updated: 2024