================================================================================
PYTHON DESIGN PATTERNS & SOLID PRINCIPLES - INTERMEDIATE DEVELOPER INTERVIEW
QUESTIONS
================================================================================
Q1. SINGLETON PATTERN
---------------------
Question: Implement a thread-safe Singleton pattern for a database connection
manager.
Solution:
```python
import threading
from typing import Optional
class DatabaseConnection:
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.is_connected = False
def connect(self):
self.is_connected = True
print(f"Connected to database: {self.connection_string}")
def disconnect(self):
self.is_connected = False
print("Disconnected from database")
class DatabaseManager:
_instance: Optional['DatabaseManager'] = None
_lock = threading.Lock()
def __new__(cls, connection_string: str = "default"):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, connection_string: str = "default"):
if not self._initialized:
self.connection_string = connection_string
self.connection = DatabaseConnection(connection_string)
self._initialized = True
def get_connection(self):
if not self.connection.is_connected:
self.connection.connect()
return self.connection
# Usage
manager1 = DatabaseManager("postgresql://localhost:5432/mydb")
manager2 = DatabaseManager("mysql://localhost:3306/mydb")
print(manager1 is manager2) # True - same instance
print(manager1.connection_string) # postgresql://localhost:5432/mydb
```
Q2. FACTORY PATTERN
-------------------
Question: Create a factory pattern for different types of payment processors.
Solution:
```python
from abc import ABC, abstractmethod
from typing import Dict, Any
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float, currency: str) -> bool:
pass
@abstractmethod
def get_processing_fee(self) -> float:
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float, currency: str) -> bool:
print(f"Processing credit card payment: {amount} {currency}")
return True
def get_processing_fee(self) -> float:
return 0.025 # 2.5%
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount: float, currency: str) -> bool:
print(f"Processing PayPal payment: {amount} {currency}")
return True
def get_processing_fee(self) -> float:
return 0.029 # 2.9%
class CryptoProcessor(PaymentProcessor):
def process_payment(self, amount: float, currency: str) -> bool:
print(f"Processing crypto payment: {amount} {currency}")
return True
def get_processing_fee(self) -> float:
return 0.01 # 1%
class PaymentProcessorFactory:
_processors = {
'credit_card': CreditCardProcessor,
'paypal': PayPalProcessor,
'crypto': CryptoProcessor
}
@classmethod
def create_processor(cls, processor_type: str) -> PaymentProcessor:
if processor_type not in cls._processors:
raise ValueError(f"Unknown processor type: {processor_type}")
return cls._processors[processor_type]()
@classmethod
def register_processor(cls, name: str, processor_class: type):
cls._processors[name] = processor_class
# Usage
factory = PaymentProcessorFactory()
credit_processor = factory.create_processor('credit_card')
paypal_processor = factory.create_processor('paypal')
credit_processor.process_payment(100.0, 'USD')
paypal_processor.process_payment(50.0, 'EUR')
```
Q3. OBSERVER PATTERN
--------------------
Question: Implement an observer pattern for a stock price monitoring system.
Solution:
```python
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from dataclasses import dataclass
@dataclass
class StockUpdate:
symbol: str
price: float
change: float
timestamp: str
class StockObserver(ABC):
@abstractmethod
def update(self, stock_update: StockUpdate):
pass
class EmailNotifier(StockObserver):
def __init__(self, email: str):
self.email = email
def update(self, stock_update: StockUpdate):
print(f"Email to {self.email}: {stock_update.symbol} is now $
{stock_update.price}")
class SMSNotifier(StockObserver):
def __init__(self, phone: str):
self.phone = phone
def update(self, stock_update: StockUpdate):
print(f"SMS to {self.phone}: {stock_update.symbol} changed by
{stock_update.change}")
class StockPriceMonitor:
def __init__(self):
self._observers: List[StockObserver] = []
self._stock_prices: Dict[str, float] = {}
def add_observer(self, observer: StockObserver):
self._observers.append(observer)
def remove_observer(self, observer: StockObserver):
self._observers.remove(observer)
def update_stock_price(self, symbol: str, new_price: float, timestamp: str):
old_price = self._stock_prices.get(symbol, new_price)
change = new_price - old_price
self._stock_prices[symbol] = new_price
update = StockUpdate(symbol, new_price, change, timestamp)
self._notify_observers(update)
def _notify_observers(self, update: StockUpdate):
for observer in self._observers:
observer.update(update)
# Usage
monitor = StockPriceMonitor()
email_notifier = EmailNotifier("user@example.com")
sms_notifier = SMSNotifier("+1234567890")
monitor.add_observer(email_notifier)
monitor.add_observer(sms_notifier)
monitor.update_stock_price("AAPL", 150.25, "2024-01-15 10:30:00")
monitor.update_stock_price("GOOGL", 2800.50, "2024-01-15 10:31:00")
```
Q4. STRATEGY PATTERN
--------------------
Question: Implement a strategy pattern for different sorting algorithms.
Solution:
```python
from abc import ABC, abstractmethod
from typing import List, TypeVar, Generic
T = TypeVar('T')
class SortStrategy(ABC, Generic[T]):
@abstractmethod
def sort(self, data: List[T]) -> List[T]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BubbleSortStrategy(SortStrategy[T]):
def sort(self, data: List[T]) -> List[T]:
arr = data.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def get_name(self) -> str:
return "Bubble Sort"
class QuickSortStrategy(SortStrategy[T]):
def sort(self, data: List[T]) -> List[T]:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
def get_name(self) -> str:
return "Quick Sort"
class MergeSortStrategy(SortStrategy[T]):
def sort(self, data: List[T]) -> List[T]:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: List[T], right: List[T]) -> List[T]:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def get_name(self) -> str:
return "Merge Sort"
class Sorter:
def __init__(self, strategy: SortStrategy[T]):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy[T]):
self._strategy = strategy
def sort(self, data: List[T]) -> List[T]:
return self._strategy.sort(data)
def get_strategy_name(self) -> str:
return self._strategy.get_name()
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
print(f"Using {sorter.get_strategy_name()}: {sorter.sort(data)}")
sorter.set_strategy(QuickSortStrategy())
print(f"Using {sorter.get_strategy_name()}: {sorter.sort(data)}")
sorter.set_strategy(MergeSortStrategy())
print(f"Using {sorter.get_strategy_name()}: {sorter.sort(data)}")
```
Q5. DECORATOR PATTERN
---------------------
Question: Implement a decorator pattern for adding functionality to text
processing.
Solution:
```python
from abc import ABC, abstractmethod
from typing import List
class TextProcessor(ABC):
@abstractmethod
def process(self, text: str) -> str:
pass
class BasicTextProcessor(TextProcessor):
def process(self, text: str) -> str:
return text
class TextProcessorDecorator(TextProcessor):
def __init__(self, processor: TextProcessor):
self._processor = processor
def process(self, text: str) -> str:
return self._processor.process(text)
class UppercaseDecorator(TextProcessorDecorator):
def process(self, text: str) -> str:
return self._processor.process(text).upper()
class LowercaseDecorator(TextProcessorDecorator):
def process(self, text: str) -> str:
return self._processor.process(text).lower()
class StripWhitespaceDecorator(TextProcessorDecorator):
def process(self, text: str) -> str:
return self._processor.process(text).strip()
class ReplaceDecorator(TextProcessorDecorator):
def __init__(self, processor: TextProcessor, old: str, new: str):
super().__init__(processor)
self.old = old
self.new = new
def process(self, text: str) -> str:
return self._processor.process(text).replace(self.old, self.new)
class ReverseDecorator(TextProcessorDecorator):
def process(self, text: str) -> str:
return self._processor.process(text)[::-1]
# Usage
text = " Hello, World! "
# Basic processing
processor = BasicTextProcessor()
print(f"Basic: '{processor.process(text)}'")
# Add decorators
processor = StripWhitespaceDecorator(processor)
processor = UppercaseDecorator(processor)
processor = ReplaceDecorator(processor, "WORLD", "PYTHON")
print(f"Decorated: '{processor.process(text)}'")
# Chain multiple decorators
processor = BasicTextProcessor()
processor = StripWhitespaceDecorator(processor)
processor = LowercaseDecorator(processor)
processor = ReplaceDecorator(processor, "hello", "hi")
processor = ReverseDecorator(processor)
print(f"Chained: '{processor.process(text)}'")
```
Q6. COMMAND PATTERN
-------------------
Question: Implement a command pattern for a text editor with undo/redo
functionality.
Solution:
```python
from abc import ABC, abstractmethod
from typing import List, Optional
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class TextEditor:
def __init__(self):
self.text = ""
self.cursor_position = 0
def insert_text(self, text: str, position: int) -> None:
self.text = self.text[:position] + text + self.text[position:]
self.cursor_position = position + len(text)
def delete_text(self, start: int, end: int) -> str:
deleted_text = self.text[start:end]
self.text = self.text[:start] + self.text[end:]
self.cursor_position = start
return deleted_text
def get_text(self) -> str:
return self.text
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str, position: int):
self.editor = editor
self.text = text
self.position = position
def execute(self) -> None:
self.editor.insert_text(self.text, self.position)
def undo(self) -> None:
self.editor.delete_text(self.position, self.position + len(self.text))
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, start: int, end: int):
self.editor = editor
self.start = start
self.end = end
self.deleted_text = ""
def execute(self) -> None:
self.deleted_text = self.editor.delete_text(self.start, self.end)
def undo(self) -> None:
self.editor.insert_text(self.deleted_text, self.start)
class TextEditorInvoker:
def __init__(self):
self.editor = TextEditor()
self.command_history: List[Command] = []
self.undo_stack: List[Command] = []
def execute_command(self, command: Command) -> None:
command.execute()
self.command_history.append(command)
self.undo_stack.clear() # Clear redo stack
def undo(self) -> None:
if self.command_history:
command = self.command_history.pop()
command.undo()
self.undo_stack.append(command)
def redo(self) -> None:
if self.undo_stack:
command = self.undo_stack.pop()
command.execute()
self.command_history.append(command)
def get_text(self) -> str:
return self.editor.get_text()
# Usage
invoker = TextEditorInvoker()
# Execute commands
insert_cmd1 = InsertCommand(invoker.editor, "Hello", 0)
insert_cmd2 = InsertCommand(invoker.editor, " World", 5)
delete_cmd = DeleteCommand(invoker.editor, 0, 5)
invoker.execute_command(insert_cmd1)
print(f"After insert 'Hello': {invoker.get_text()}")
invoker.execute_command(insert_cmd2)
print(f"After insert ' World': {invoker.get_text()}")
invoker.execute_command(delete_cmd)
print(f"After delete: {invoker.get_text()}")
invoker.undo()
print(f"After undo: {invoker.get_text()}")
invoker.undo()
print(f"After second undo: {invoker.get_text()}")
invoker.redo()
print(f"After redo: {invoker.get_text()}")
```
Q7. ADAPTER PATTERN
-------------------
Question: Create an adapter pattern to make incompatible payment systems work
together.
Solution:
```python
from abc import ABC, abstractmethod
from typing import Dict, Any
# Legacy payment system
class LegacyPaymentSystem:
def make_payment(self, amount: float, card_number: str, expiry: str) ->
Dict[str, Any]:
return {
"status": "success",
"transaction_id": f"LEG_{hash(card_number)}",
"amount": amount,
"message": "Payment processed by legacy system"
}
# Modern payment system
class ModernPaymentSystem:
def process_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "success",
"transaction_id": f"MOD_{hash(str(payment_data))}",
"amount": payment_data.get("amount", 0),
"message": "Payment processed by modern system"
}
# Target interface
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float, **kwargs) -> Dict[str, Any]:
pass
# Adapter for legacy system
class LegacyPaymentAdapter(PaymentProcessor):
def __init__(self, legacy_system: LegacyPaymentSystem):
self.legacy_system = legacy_system
def process_payment(self, amount: float, **kwargs) -> Dict[str, Any]:
card_number = kwargs.get("card_number")
expiry = kwargs.get("expiry")
if not card_number or not expiry:
raise ValueError("Legacy system requires card_number and expiry")
return self.legacy_system.make_payment(amount, card_number, expiry)
# Adapter for modern system
class ModernPaymentAdapter(PaymentProcessor):
def __init__(self, modern_system: ModernPaymentSystem):
self.modern_system = modern_system
def process_payment(self, amount: float, **kwargs) -> Dict[str, Any]:
payment_data = {
"amount": amount,
"currency": kwargs.get("currency", "USD"),
"payment_method": kwargs.get("payment_method", "card"),
"metadata": kwargs
}
return self.modern_system.process_payment(payment_data)
# Payment service that works with any adapter
class PaymentService:
def __init__(self, processor: PaymentProcessor):
self.processor = processor
def pay(self, amount: float, **kwargs) -> Dict[str, Any]:
return self.processor.process_payment(amount, **kwargs)
# Usage
legacy_system = LegacyPaymentSystem()
modern_system = ModernPaymentSystem()
legacy_adapter = LegacyPaymentAdapter(legacy_system)
modern_adapter = ModernPaymentAdapter(modern_system)
# Use legacy system through adapter
legacy_service = PaymentService(legacy_adapter)
result1 = legacy_service.pay(100.0, card_number="1234-5678-9012-3456",
expiry="12/25")
print(f"Legacy payment: {result1}")
# Use modern system through adapter
modern_service = PaymentService(modern_adapter)
result2 = modern_service.pay(200.0, currency="EUR", payment_method="paypal")
print(f"Modern payment: {result2}")
```
Q8. SOLID PRINCIPLES IMPLEMENTATION
-----------------------------------
Question: Refactor a poorly designed class to follow SOLID principles.
Solution:
```python
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime
# Before (violating SOLID principles)
class BadUserManager:
def __init__(self):
self.users = []
def add_user(self, name: str, email: str, password: str):
# Violates SRP - handles validation, storage, and notification
if not self._validate_email(email):
raise ValueError("Invalid email")
user = {"name": name, "email": email, "password": password}
self.users.append(user)
# Violates OCP - hardcoded notification
self._send_email_notification(email)
def _validate_email(self, email: str) -> bool:
return "@" in email
def _send_email_notification(self, email: str):
print(f"Welcome email sent to {email}")
def get_user_by_email(self, email: str):
return next((user for user in self.users if user["email"] == email), None)
# After (following SOLID principles)
@dataclass
class User:
name: str
email: str
password: str
created_at: datetime = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
# Single Responsibility Principle (SRP)
class EmailValidator:
def validate(self, email: str) -> bool:
return "@" in email and "." in email
class PasswordValidator:
def validate(self, password: str) -> bool:
return len(password) >= 8
class UserRepository:
def __init__(self):
self.users: List[User] = []
def save(self, user: User) -> None:
self.users.append(user)
def find_by_email(self, email: str) -> User:
return next((user for user in self.users if user.email == email), None)
# Open/Closed Principle (OCP)
class NotificationService(ABC):
@abstractmethod
def send_notification(self, user: User) -> None:
pass
class EmailNotificationService(NotificationService):
def send_notification(self, user: User) -> None:
print(f"Welcome email sent to {user.email}")
class SMSNotificationService(NotificationService):
def send_notification(self, user: User) -> None:
print(f"Welcome SMS sent to {user.name}")
# Liskov Substitution Principle (LSP)
class UserService:
def __init__(self,
repository: UserRepository,
email_validator: EmailValidator,
password_validator: PasswordValidator,
notification_service: NotificationService):
self.repository = repository
self.email_validator = email_validator
self.password_validator = password_validator
self.notification_service = notification_service
def create_user(self, name: str, email: str, password: str) -> User:
if not self.email_validator.validate(email):
raise ValueError("Invalid email")
if not self.password_validator.validate(password):
raise ValueError("Password too short")
user = User(name=name, email=email, password=password)
self.repository.save(user)
self.notification_service.send_notification(user)
return user
def get_user(self, email: str) -> User:
return self.repository.find_by_email(email)
# Interface Segregation Principle (ISP)
class ReadableRepository(ABC):
@abstractmethod
def find_by_email(self, email: str) -> User:
pass
class WritableRepository(ABC):
@abstractmethod
def save(self, user: User) -> None:
pass
class FullRepository(ReadableRepository, WritableRepository):
pass
# Dependency Inversion Principle (DIP)
class UserManager:
def __init__(self, user_service: UserService):
self.user_service = user_service
def register_user(self, name: str, email: str, password: str) -> User:
return self.user_service.create_user(name, email, password)
def find_user(self, email: str) -> User:
return self.user_service.get_user(email)
# Usage
repository = UserRepository()
email_validator = EmailValidator()
password_validator = PasswordValidator()
notification_service = EmailNotificationService()
user_service = UserService(repository, email_validator, password_validator,
notification_service)
user_manager = UserManager(user_service)
# Create user
user = user_manager.register_user("John Doe", "john@example.com", "securepass123")
print(f"Created user: {user}")
# Find user
found_user = user_manager.find_user("john@example.com")
print(f"Found user: {found_user}")
```
KEY SOLID PRINCIPLES:
=====================
1. **Single Responsibility Principle (SRP)**: A class should have only one reason
to change
2. **Open/Closed Principle (OCP)**: Open for extension, closed for modification
3. **Liskov Substitution Principle (LSP)**: Subtypes must be substitutable for
their base types
4. **Interface Segregation Principle (ISP)**: Clients shouldn't depend on
interfaces they don't use
5. **Dependency Inversion Principle (DIP)**: Depend on abstractions, not
concretions
BEST PRACTICES:
===============
1. **Use Composition over Inheritance**: Favor object composition over class
inheritance
2. **Program to Interfaces**: Code to interfaces, not implementations
3. **Keep Classes Small**: Single responsibility and focused functionality
4. **Use Dependency Injection**: Inject dependencies rather than creating them
5. **Follow Naming Conventions**: Clear, descriptive names for classes and methods
6. **Write Tests**: Design patterns should be testable