OOP and SOLID Principles
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