Programming Paradigms
Structured, Object-Oriented & Functional Programming
Why Paradigms Matter
Programming paradigms are fundamental approaches to structuring code and solving problems. They're not just academic concepts, they shape how you think about design, influence the maintainability of your systems, and determine which patterns are natural vs. awkward in your codebase. Understanding multiple paradigms makes you a better architect because you can choose the right tool for each problem rather than forcing every problem into one way of thinking.
The Three Major Paradigms
Each paradigm offers a different mental model for organizing and expressing computation:
Structured
Focus: Control flow and procedures
Key concept: Sequential execution with functions
When to use: Scripts, utilities, imperative algorithms
Object-Oriented
Focus: Objects and their interactions
Key concept: Encapsulation and polymorphism
When to use: Large systems, domain modeling, frameworks
Functional
Focus: Pure functions and immutability
Key concept: Transformation of data
When to use: Data pipelines, concurrent systems, transformations
Structured Programming: Control Flow First
Structured programming emerged as a reaction to "goto spaghetti code." It emphasizes clear control flow through sequences, conditionals, and loops, organized into reusable functions.
Core Principles: The Three Control Structures
1. Sequence
def process_order(order):
"""Sequential steps executed in order"""
validate_order(order) # Step 1
charge_payment(order) # Step 2
ship_order(order) # Step 3
send_confirmation(order) # Step 4
# Each step happens in sequence2. Selection (Conditionals)
def handle_order(order):
"""Branching logic based on conditions"""
if validate_order(order):
# Path A: valid order
process_payment(order)
ship_order(order)
else:
# Path B: invalid order
notify_customer(order)
refund_payment(order)3. Iteration (Loops)
def process_orders(orders):
"""Iterate through collection"""
total_revenue = 0
for order in orders:
total_revenue += order.amount
return total_revenue
# Or with while loop
def process_queue(queue):
"""Repeat until condition met"""
while not queue.is_empty():
item = queue.pop()
process(item)All Three Structures Combined
def process_orders(orders):
"""Combining sequence, selection, and iteration"""
total_revenue = 0
failed_orders = []
# ITERATION: Loop through orders
for order in orders:
# SELECTION: Choose path based on validation
if validate_order(order):
# SEQUENCE: Execute steps in order
total_revenue += process_payment(order)
ship_order(order)
else:
failed_orders.append(order)
notify_customer(order)
# SEQUENCE: Final steps
generate_report(total_revenue, failed_orders)
return total_revenue
# Clear, top-down structure with reusable functions
def validate_order(order):
return order.has_payment_info() and order.items_in_stock()
def process_payment(order):
return order.total
def ship_order(order):
passWhen Structured Programming Shines
- Scripts and utilities: One-off tasks with clear beginning and end
- Algorithms: Step-by-step procedures like sorting or searching
- Simple programs: When object modeling would be overkill
- Performance-critical code: Direct, minimal abstraction overhead
Object-Oriented Programming: Modeling Reality
OOP organizes code around objects that encapsulate both data and behavior. It excels at modeling real-world (business) entities and complex systems with many interacting components.
The Four Pillars
1. Encapsulation
class BankAccount:
def __init__(self, account_number, balance=0):
self._account_number = account_number # Protected
self._balance = balance
def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
@property
def balance(self):
return self._balance # Read-only access to internal state2. Inheritance
class SavingsAccount(BankAccount):
"""Inherits from BankAccount and extends with interest functionality"""
def __init__(self, account_number, balance=0, interest_rate=0.02):
super().__init__(account_number, balance)
self.interest_rate = interest_rate
def apply_interest(self):
"""New behavior specific to SavingsAccount"""
interest = self._balance * self.interest_rate
self.deposit(interest)3. Polymorphism
class CheckingAccount(BankAccount):
"""Same interface, different implementation of withdraw"""
def __init__(self, account_number, balance=0, overdraft_limit=100):
super().__init__(account_number, balance)
self.overdraft_limit = overdraft_limit
def withdraw(self, amount):
# Different implementation allowing overdraft
if amount <= self._balance + self.overdraft_limit:
self._balance -= amount
return True
return False
# Polymorphism in action: same method name, different behavior
accounts = [SavingsAccount("001", 1000), CheckingAccount("002", 500)]
for account in accounts:
account.withdraw(100) # Calls appropriate withdraw method4. Abstraction
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract interface - defines what, not how"""
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def refund(self, transaction_id):
pass
class StripeProcessor(PaymentProcessor):
"""Concrete implementation for Stripe"""
def process_payment(self, amount):
return stripe.charge(amount)
def refund(self, transaction_id):
return stripe.refund(transaction_id)
# Client code depends on abstraction, not concrete implementation
def checkout(cart, processor: PaymentProcessor):
total = cart.calculate_total()
return processor.process_payment(total)Composition vs. Inheritance
The Trade-offs at a Glance
❌ Inheritance Problems
- Tight coupling to parent classes
- Changes to parent break children
- Deep hierarchies hard to understand
- Can't change behavior at runtime
- Forced to inherit unwanted methods
✓ Composition Benefits
- Loose coupling between components
- Easy to modify independently
- Flat, simple structure
- Change behavior at runtime
- Use only what you need
Composition Example
# Simple, independent components
class EmailNotifier:
def send(self, message, recipient):
print(f"Email to {recipient}: {message}")
class SMSNotifier:
def send(self, message, recipient):
print(f"SMS to {recipient}: {message}")
class PushNotifier:
def send(self, message, recipient):
print(f"Push notification to {recipient}: {message}")
# Compose behaviors by combining objects
class NotificationService:
def __init__(self):
self.notifiers = [] # Contains other objects
def add_notifier(self, notifier):
"""Add notification strategy at runtime"""
self.notifiers.append(notifier)
def notify_all(self, message, recipient):
"""Delegate to composed objects"""
for notifier in self.notifiers:
notifier.send(message, recipient)
# Usage: Flexible composition at runtime
service = NotificationService()
service.add_notifier(EmailNotifier()) # Add email
service.add_notifier(SMSNotifier()) # Add SMS
# Can add/remove notifiers dynamically
service.notify_all("Your order shipped!", "user@example.com")When to Use Inheritance
Inheritance is the right tool when a true "is-a" relationship exists, the subclass is a more specific version of the parent, and that relationship is stable over time.
Use inheritance when:
- A clear "is-a" relationship exists (Dog IS-A Animal)
- The parent class is stable and unlikely to change
- You need polymorphism through a shared interface
- Extending a framework's base class (Django views, SQLAlchemy models)
- Subclasses share the majority of the parent's behavior
Avoid inheritance when:
- You only want to reuse code (use a utility function instead)
- The relationship is "has-a", not "is-a" (Car HAS-A Engine)
- Depth grows without each level adding a clear, single responsibility
- The subclass needs to override most parent methods
- You need to mix behaviors from multiple sources
from abc import ABC, abstractmethod
# Good use of inheritance: true is-a relationship, shared contract
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.radius
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)
# Polymorphism works cleanly - all Shapes share the same interface
def print_shape_info(shape: Shape):
print(f"Area: {shape.area():.2f}, Perimeter: {shape.perimeter():.2f}")Exception: Layered Inheritance Done Right
Deep hierarchies are not inherently bad. When each layer adds exactly one responsibility and concrete implementations pick the level that matches their requirements, deep inheritance enforces SRP and Interface Segregation simultaneously. This is the Template Method pattern applied in layers.
A classic example is an ETL pipeline where each layer builds on the last:
from abc import ABC, abstractmethod
# Layer 1: Template Method - defines the ETL contract and flow (single responsibility)
class ETLBase(ABC):
def run(self):
"""Fixed skeleton: every ETL runs in this order"""
data = self.extract()
data = self.transform(data)
self.load(data)
@abstractmethod
def extract(self): ...
@abstractmethod
def transform(self, data): ...
@abstractmethod
def load(self, data): ...
# Layer 2: Adds HTTP extraction capability (single responsibility)
class HTTPExtractor(ETLBase):
def __init__(self, base_url: str):
self.base_url = base_url
def extract(self):
return requests.get(self.base_url).json()
# Layer 3: Adds caching on top of HTTP extraction (single responsibility)
class CachedHTTPExtractor(HTTPExtractor):
def __init__(self, base_url: str, cache):
super().__init__(base_url)
self.cache = cache
def extract(self):
cached = self.cache.get(self.base_url)
if cached:
return cached
data = super().extract()
self.cache.set(self.base_url, data)
return data
# Layer 4: Adds database persistence (single responsibility)
class DBPersistedExtractor(CachedHTTPExtractor):
def __init__(self, base_url: str, cache, db):
super().__init__(base_url, cache)
self.db = db
def load(self, data):
self.db.bulk_save(data)
# --- Concrete implementations choose their entry point ---
class SimpleSync(HTTPExtractor):
"""Only needs HTTP - no cache, no DB"""
def transform(self, data): return data
def load(self, data): print(data)
class CachedSync(CachedHTTPExtractor):
"""Needs HTTP + cache, but writes to a file"""
def transform(self, data): return [clean(r) for r in data]
def load(self, data): write_to_file(data)
class FullSync(DBPersistedExtractor):
"""Full stack: HTTP + cache + DB"""
def transform(self, data): return [enrich(r) for r in data]- Each layer adds exactly one responsibility
- Concretes pick the level that matches their needs (ISP)
- Adding a new layer never touches existing concretes
- The flow contract is enforced once, at the top
- A layer adds more than one responsibility
- A layer exists for convenience, not a distinct concern
- Concretes are forced to inherit capabilities they never use
When to Use Composition
Composition shines when you need to assemble behavior from independent parts, swap implementations at runtime, or combine capabilities that don't fit a clean hierarchy.
Use composition when:
- A "has-a" relationship fits (Order HAS-A PaymentMethod)
- Behavior needs to be swappable at runtime
- You need to mix behaviors from multiple sources
- The component parts are useful on their own
- You want classes to stay small and focused
Watch out for:
- Too many tiny objects making flow hard to follow
- Forwarding many methods just to delegate behavior
- Over-abstracting simple, one-off behavior into components
- Losing the clarity of a straightforward class hierarchy
class Logger:
def log(self, message: str):
print(f"[LOG] {message}")
class PaymentValidator:
def validate(self, payment: dict) -> bool:
return payment.get("amount", 0) > 0
class FileStorage:
def save(self, data: dict):
pass # Write to disk
class CloudStorage:
def save(self, data: dict):
pass # Upload to S3
# OrderService HAS-A storage, HAS-A logger, HAS-A validator
# Each dependency is injected - easy to swap implementations
class OrderService:
def __init__(self, storage, logger: Logger, validator: PaymentValidator):
self.storage = storage # FileStorage or CloudStorage - doesn't matter
self.logger = logger
self.validator = validator
def process(self, order: dict) -> bool:
if not self.validator.validate(order["payment"]):
self.logger.log("Invalid payment")
return False
self.storage.save(order)
self.logger.log(f"Order {order['id']} saved")
return True
# Swap CloudStorage for FileStorage with zero changes to OrderService
service = OrderService(CloudStorage(), Logger(), PaymentValidator())Where Each Truly Belongs
Rather than asking "composition or inheritance?", ask "what domain am I in?" Both patterns have natural homes, and forcing the wrong one creates friction that compounds over time.
Inheritance-first domains
- ETL and data pipelines - layered capabilities, fixed flow contract
- Framework extension - Django views, SQLAlchemy models, Celery tasks
- Protocol hierarchies - abstract base classes defining contracts
- Game entities - Character → Enemy → Boss (stable is-a tree)
Composition-first domains
- UI components - Button HAS-A icon, HAS-A style, HAS-A handler
- Service layers - OrderService HAS-A storage, HAS-A validator
- Physical entities - Car HAS-A Engine, HAS-A Transmission
- Runtime behavior injection - strategy, decorator, plugin patterns
UI Components - composition is the natural fit
A Button is not a subtype of Icon or Label. It has an icon, a label, a style, and a handler. Inheriting from any of those creates a false semantic relationship and forces every Button variant to carry all ancestor state. Composition keeps each concern independent and reusable.
# Bad: inheritance forces false relationships
class Icon: ...
class Label(Icon): ... # Label IS-A Icon? No.
class Button(Label): ... # Button IS-A Label? No.
# Button now drags in all of Icon's and Label's internals
# Good: composition - each part stays independent and reusable
class Icon:
def __init__(self, name: str):
self.name = name
class Label:
def __init__(self, text: str):
self.text = text
class ClickHandler:
def __init__(self, callback):
self.callback = callback
def on_click(self, event):
self.callback(event)
class Button:
"""Button HAS-A icon, HAS-A label, HAS-A handler"""
def __init__(self, label: Label, icon: Icon = None, handler: ClickHandler = None):
self.label = label
self.icon = icon
self.handler = handler
# Build any Button variant without touching the class hierarchy
save_btn = Button(Label("Save"), Icon("floppy-disk"), ClickHandler(save_record))
cancel_btn = Button(Label("Cancel"), ClickHandler(dismiss))
icon_only = Button(Label(""), Icon("trash"), ClickHandler(delete_record))Physical entities - Car HAS-A Engine
A Car is not a type of Engine or Transmission. Modeling it with inheritance would be semantically wrong and would make swapping components impossible. Composition lets you replace a GasEngine with an ElectricMotor without touching the Car class at all.
from abc import ABC, abstractmethod
class Engine(ABC):
@abstractmethod
def start(self) -> str: ...
@abstractmethod
def stop(self) -> str: ...
class GasEngine(Engine):
def start(self) -> str: return "Vroom"
def stop(self) -> str: return "Engine off"
class ElectricMotor(Engine):
def start(self) -> str: return "Whirr"
def stop(self) -> str: return "Silent"
class Transmission:
def shift(self, gear: int): ...
class Car:
"""Car HAS-A engine, HAS-A transmission - not IS-A either of them"""
def __init__(self, engine: Engine, transmission: Transmission):
self.engine = engine
self.transmission = transmission
def drive(self):
self.engine.start()
self.transmission.shift(1)
# Swap engine type with zero changes to Car
gas_car = Car(GasEngine(), Transmission())
electric_car = Car(ElectricMotor(), Transmission())Quick Decision Guide
Ask yourself:
- Is it a true "is-a" relationship? (No - lean toward composition)
- Is the domain pipeline or flow-based? (Yes - favor inheritance)
- Will behavior change or be injected at runtime? (Yes - use composition)
- Does each layer in the hierarchy add one clear responsibility? (No - use composition)
- Are the parts independently useful and reusable? (Yes - use composition)
The practical rule:
Match the tool to the domain. In ETL and pipeline architectures, inheritance with layered responsibilities is almost always the cleaner design. In UI, services, and entity modeling, composition is the natural fit.
Neither is a default. Ask: does this domain have a stable, sequential contract (inherit), or does it assemble independent parts at runtime (compose)?
Common Inheritance Anti-Patterns
The Banana-Gorilla Problem
"You wanted a banana but got a gorilla holding the banana and the entire jungle." Deep hierarchies pull in far more than you need.
Inheriting to Reuse Code
Extending a class just to access its utility methods creates false semantic relationships. Use a utility module or composition instead.
The Yo-Yo Problem
Tracing logic requires jumping up and down the hierarchy constantly. This is a symptom of layers without clear responsibilities, not simply of depth. A well-layered ETL hierarchy can be deep without causing this problem.
When Composition Works Against You
Composition is not always the cleaner answer. Two patterns signal it's the wrong tool:
1. All responsibilities collapse into one class
When composition requires injecting HTTP, cache, and DB dependencies into every flow regardless of whether that flow needs them, you have violated Interface Segregation. Worse, every update to the shared class propagates risk to all flows, even unrelated ones.
# Anti-pattern: composition forces one class to carry everything
class ETLService:
def __init__(self, http_client, cache, db, transformer, loader):
self.http_client = http_client # What if this flow doesn't need cache?
self.cache = cache # What if this flow doesn't write to DB?
self.db = db # Every flow carries all of this
self.transformer = transformer
self.loader = loader
def run(self, url: str):
data = self.http_client.get(url)
data = self.transformer.transform(data)
self.loader.load(data)
# cache and db always injected, sometimes unused
# A bug fix in cache logic triggers re-testing ALL flows2. Per-record classes - the granularity trap
If your design forces you to instantiate a class per record - one object per row, one object per event - composition has driven the granularity to the wrong level. A class should represent a capability or a service, not a single unit of work. Processing 10 million records means 10 million object allocations, and all the dependency overhead that comes with each one.
# Anti-pattern: class instantiated per record - does not scale
class RecordProcessor:
def __init__(self, record: dict, validator, transformer, loader):
self.record = record # One instance per record
self.validator = validator
self.transformer = transformer
self.loader = loader
def process(self):
if self.validator.validate(self.record):
self.loader.load(self.transformer.transform(self.record))
# 10 million records = 10 million object allocations with full dependency overhead
for record in load_from_source():
RecordProcessor(record, validator, transformer, loader).process()
# Better: the processor IS the service, records are just data passed through
class RecordProcessor:
def __init__(self, validator, transformer, loader):
self.validator = validator # Set once
self.transformer = transformer
self.loader = loader
def process_batch(self, records: list[dict]):
for record in records:
if self.validator.validate(record):
self.loader.load(self.transformer.transform(record))When OOP Shines
- Domain modeling: When your code mirrors real-world entities (users, orders, products)
- Large systems: Managing complexity through encapsulation and modularity
- Frameworks: Providing extension points through inheritance and interfaces
- GUI applications: Widgets and components naturally map to objects
- Stateful systems: When entities maintain state over time
Functional Programming: Transformation Over Mutation
Functional programming treats computation as the evaluation of mathematical functions. It emphasizes immutability, pure functions, and declarative transformations.
Core Principles
1. Pure Functions
# Impure (bad) - has side effects
total = 0
def add_to_total(x):
global total
total += x # Side effect: modifies external state
return total
# Pure (good) - deterministic, no side effects
def add(x, y):
return x + y # Same inputs always give same output2. Immutability
# Mutable approach (modifies original)
def update_user(user, new_email):
user['email'] = new_email # Modifies original
return user
# Immutable approach (creates new data)
def update_user(user, new_email):
return {**user, 'email': new_email} # New dict, original unchanged3. Higher-Order Functions
def retry(times, delay=1):
"""Decorator that retries a function on failure"""
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == times - 1:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=3, delay=2)
def fetch_data(url):
return requests.get(url).json()4. Declarative Transformations
# Imperative (how) - explicit steps
def get_adult_names(users):
result = []
for user in users:
if user['age'] >= 18:
result.append(user['name'])
return result
# Declarative (what) - expresses intent
def get_adult_names(users):
return [user['name'] for user in users if user['age'] >= 18]
# Functional style with map/filter
get_adult_names = lambda users: list(map(
lambda u: u['name'],
filter(lambda u: u['age'] >= 18, users)
))Functional Patterns in Practice
Function Composition & Data Pipelines
from functools import reduce
# Individual transformation functions
def parse_csv(data): pass
def filter_valid_transactions(data): pass
def calculate_totals(data): pass
def group_by_region(data): pass
def format_report(data): pass
# Compose pattern: build complex pipelines
def compose(*functions):
"""Compose functions right to left"""
return reduce(lambda f, g: lambda x: f(g(x)), functions)
# Build reusable pipeline
process_pipeline = compose(
format_report,
group_by_region,
calculate_totals,
filter_valid_transactions,
parse_csv
)
# Execute pipeline
result = process_pipeline(raw_data)Map-Reduce Pattern
map transforms each element independently, and reduce aggregates results into a single value, enabling scalable computation.from functools import reduce
orders = [
{'id': 1, 'amount': 100, 'region': 'US'},
{'id': 2, 'amount': 150, 'region': 'EU'},
{'id': 3, 'amount': 200, 'region': 'US'},
]
# Map: Transform each item independently
def get_amount(order):
return order['amount']
# Reduce: Aggregate all results
def sum_amounts(acc, amount):
return acc + amount
# Combine: map then reduce
total = reduce(sum_amounts, map(get_amount, orders), 0)
print(total) # 450Currying & Partial Application
# Simple currying example
def multiply(x):
return lambda y: x * y
double = multiply(2)
triple = multiply(3)
print(double(5)) # 10
print(triple(5)) # 15
# Practical use: Pre-configured functions
def create_logger(level):
"""Returns a logging function configured for specific level"""
def log(message):
if level == 'DEBUG':
print(f"[DEBUG] {message}")
elif level == 'INFO':
print(f"[INFO] {message}")
return log
# Create specialized loggers
debug = create_logger('DEBUG')
info = create_logger('INFO')
debug("Starting process") # [DEBUG] Starting process
info("Process complete") # [INFO] Process completeMonads for Error Handling
Result) that handles errors gracefully without exceptions, enabling clean functional error handling.class Result:
"""Result monad: wraps success value or error"""
def __init__(self, value=None, error=None):
self.value = value
self.error = error
def is_ok(self):
return self.error is None
def map(self, func):
"""Transform value if ok, pass error through"""
if self.is_ok():
try:
return Result(value=func(self.value))
except Exception as e:
return Result(error=str(e))
return self
def flat_map(self, func):
"""Like map but func returns Result"""
if self.is_ok():
return func(self.value)
return self
# Usage: Chain operations that might fail
def divide(x, y):
if y == 0:
return Result(error="Division by zero")
return Result(value=x / y)
def square(x):
return x * x
# Chain operations - errors propagate automatically
result = (
divide(10, 2) # Result(5)
.map(square) # Result(25)
.flat_map(lambda x: divide(x, 5)) # Result(5.0)
)
print(result.value if result.is_ok() else result.error)When Functional Programming Shines
- Data transformations: ETL pipelines, data processing, analytics
- Concurrent systems: Immutability eliminates race conditions
- Event processing: Streaming data, reactive systems
- Mathematical computations: When operations are naturally functional
- Testing: Pure functions are trivial to test
Comparing Paradigms: The Same Problem
Let's solve the same problem using all three paradigms: Calculate total revenue from orders, applying discounts and filtering invalid orders.
# STRUCTURED APPROACH
def calculate_revenue_structured(orders):
total = 0
for order in orders:
if order['status'] == 'valid':
subtotal = order['amount']
if order['discount']:
subtotal = subtotal * (1 - order['discount'])
total += subtotal
return total
# OBJECT-ORIENTED APPROACH
class Order:
def __init__(self, amount, discount=0, status='valid'):
self.amount = amount
self.discount = discount
self.status = status
def is_valid(self):
return self.status == 'valid'
def calculate_total(self):
return self.amount * (1 - self.discount)
class RevenueCalculator:
def __init__(self, orders):
self.orders = orders
def calculate(self):
return sum(
order.calculate_total()
for order in self.orders
if order.is_valid()
)
# FUNCTIONAL APPROACH - Pythonic
def calculate_revenue_functional(orders):
return sum(
order['amount'] * (1 - order.get('discount', 0))
for order in orders
if order['status'] == 'valid'
)Multi-Paradigm Programming: The Pragmatic Approach
Modern languages like Python support multiple paradigms. The best code uses the right paradigm for each problem rather than forcing everything into one model.
# Mixing paradigms effectively
class DataProcessor:
"""OOP for state management and interface"""
def __init__(self, config):
self.config = config
self.cache = {}
def process(self, data_path):
"""Process data through functional pipeline"""
data = self._load_data(data_path)
validated = self._validate(data)
transformed = self._transform(validated)
return self._aggregate(transformed)
# Structured for step-by-step logic
def _load_data(self, data_path):
if data_path in self.cache:
return self.cache[data_path]
result = []
with open(data_path) as f:
for line in f:
result.append(self._parse_line(line))
self.cache[data_path] = result
return result
# Functional for transformations
def _validate(self, records):
return [r for r in records if self._is_valid(r)]
def _transform(self, records):
return [self._apply_business_rules(r) for r in records]
# Functional utilities as pure functions
@staticmethod
def _is_valid(record):
return record.get('amount', 0) > 0
@staticmethod
def _apply_business_rules(record):
return {
**record,
'processed': True,
'tax': record['amount'] * 0.1
}
def _aggregate(self, records):
"""Structured aggregation"""
totals = {}
for record in records:
region = record['region']
totals[region] = totals.get(region, 0) + record['amount']
return totalsGuidelines for Mixing Paradigms
- Use OOP for: System structure, managing state, defining interfaces
- Use FP for: Data transformations, business logic, validation
- Use structured for: Algorithms, I/O operations, glue code
- Keep paradigms separated: Don't mix within a single function
- Be consistent: Similar problems should use similar approaches
Real-World Trade-offs
| Aspect | Structured | Object-Oriented | Functional |
|---|---|---|---|
| Learning Curve | Easy - linear flow | Moderate - requires OOP thinking | Steep - different mental model |
| Maintainability | Good for small programs | Excellent for large systems | Excellent when understood |
| Testability | Moderate - side effects | Good - can mock objects | Excellent - pure functions |
| Concurrency | Difficult - shared state | Moderate - need locking | Easy - immutable data |
| Performance | Fast - direct | Good - some overhead | Variable - depends on language |
| Code Reuse | Functions only | Inheritance & composition | Higher-order functions |
Decision Framework: Which Paradigm?
Ask yourself these questions: 1. "Is this modeling a real-world (business) entity with behavior?" → YES: Use OOP (User, Order, PaymentProcessor) → NO: Continue... 2. "Is this primarily a data transformation?" → YES: Use Functional (parsing, filtering, aggregating) → NO: Continue... 3. "Is this a step-by-step algorithm or I/O operation?" → YES: Use Structured (file processing, API calls) 4. "Does this need to maintain state over time?" → YES: Use OOP (database connection, cache, session) → NO: Prefer Functional or Structured 5. "Will this be used concurrently?" → YES: Prefer Functional (immutability = thread-safe) → NO: Any paradigm works
Example Decisions
- E-commerce checkout flow: OOP (models User, Cart, Order with state and behavior)
- ETL data pipeline: Functional (chains of transformations on immutable data)
- Deployment script: Structured (sequential steps: backup, deploy, verify)
- Web framework: OOP (routes, middleware, controllers with extension points)
- Analytics query: Functional (map-reduce over datasets)
- Game engine: Multi-paradigm (OOP for entities, functional for physics calculations, structured for game loop)
Common Anti-Patterns to Avoid
❌ OOP Overuse: The "Everything is a Class" Disease
Not every piece of functionality needs to be wrapped in a class. Use functions for stateless operations.
# BAD: Unnecessary abstraction
class MathOperations:
@staticmethod
def add(a, b):
return a + b
result = MathOperations.add(2, 3) # Just use: 2 + 3
# BAD: Manager/Helper/Util classes
class UserManager:
def get_user(self, id): pass
def save_user(self, user): pass
def delete_user(self, id): pass
# This is just a namespace, not an object with state!
# GOOD: Use functions when there's no state
def add(a, b):
return a + b❌ Functional Overuse: Unreadable One-Liners
Clever one-liners might be impressive, but code is read far more often than it's written. Prioritize clarity.
# BAD: Clever but incomprehensible
result = reduce(lambda a, x: a + [x] if x not in a else a,
filter(lambda x: x > 0,
map(lambda x: x['value'], data)), [])
# GOOD: Clear and maintainable
def get_positive_unique_values(data):
values = [item['value'] for item in data]
positive = [v for v in values if v > 0]
return list(set(positive)) # Remove duplicates❌ Mixing Paradigms Chaotically
Keep paradigms separated within functions. Don't mix side effects with functional style.
# BAD: Mixing mutation with functional style
def process_items(items):
result = []
return [result.append(x * 2) or x * 2 for x in items if x > 0]
# Side effects in list comprehension!
# GOOD: Choose one approach per function
def process_items(items):
return [x * 2 for x in items if x > 0]Key Takeaways
- No paradigm is universally superior - each excels in different contexts
- Structured: Best for algorithms, scripts, and sequential operations
- OOP: Best for modeling entities, managing state, and large systems
- Functional: Best for transformations, concurrency, and testability
- Multi-paradigm is pragmatic - use the right tool for each job
- Consistency matters - keep similar problems solved similarly
- Avoid over-engineering - simple solutions beat clever ones
- Understanding all three paradigms makes you a better architect who can choose the right approach for each problem rather than forcing every problem into one mental model