Structural patterns focus on how classes and objects are composed to form larger structures, ensuring that if one part changes, the entire structure doesn’t need to.
Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by converting the interface of a class into another interface clients expect.
Use Cases in FastAPI
- Third-party Service Integration: Adapting third-party libraries to fit your application’s interface.
- Database Abstraction Layers: Providing a uniform interface over different database technologies.
- API Version Compatibility: Allowing different API versions to coexist with uniform access.
Advantages
- Enhances code reusability by allowing classes with incompatible interfaces to work together.
- Promotes flexibility and interchangeability of components.
Disadvantages
- Can increase the complexity of the codebase.
- May lead to over-abstraction if not used judiciously.
Adapter Pattern with FastAPI: Third-party Service Integration
Suppose you want to integrate a third-party payment service with a different interface.
from abc import ABC, abstractmethod
# External Payment Service with its own interface
class ExternalPaymentService:
def make_payment(self, amount: float, currency: str):
return f"Paid {amount} {currency} using ExternalPaymentService"
# Abstract Payment Interface
class PaymentInterface(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
pass
# Adapter
class ExternalPaymentAdapter(PaymentInterface):
def __init__(self, external_service: ExternalPaymentService):
self.external_service = external_service
def pay(self, amount: float) -> str:
# Assume default currency is USD
return self.external_service.make_payment(amount, "USD")
# FastAPI Route
app = FastAPI()
@app.post("/pay/")
async def make_payment(amount: float):
external_service = ExternalPaymentService()
payment_adapter = ExternalPaymentAdapter(external_service)
result = payment_adapter.pay(amount)
return {"status": "success", "message": result}
Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem, making it easier to use.
Use Cases in FastAPI
- Simplifying Complex Subsystems: Providing a unified API over multiple services or modules.
- Service Layer Implementation: Encapsulating business logic and exposing it through a simplified interface.
- External API Integration: Abstracting interactions with multiple external APIs.
Advantages
- Simplifies interactions with complex subsystems.
- Reduces dependencies between clients and subsystems.
- Improves code readability and maintainability.
Disadvantages
- Can become a god object if not carefully designed.
- May hide essential details, making debugging harder.
Facade Pattern with FastAPI: Service Layer
Assume you have multiple services (e.g., UserService, EmailService) and you want to provide a unified interface.
# Improved version with better adherence to SRP and DIP
from typing import List
from fastapi import FastAPI, Depends
# Services
class UserService:
async def create_user(self, user_data: dict) -> dict:
# Logic to create user
return {"id": 1, "name": user_data["name"], "email": user_data["email"]}
class EmailService:
async def send_welcome_email(self, user: dict):
# Logic to send email
print(f"Sending welcome email to {user['email']}")
class NotificationService:
def __init__(self, email_service: EmailService):
self.email_service = email_service
async def notify_user_creation(self, user: dict):
await self.email_service.send_welcome_email(user)
# Facade with dependency injection
class UserFacade:
def __init__(self, user_service: UserService, notification_service: NotificationService):
self.user_service = user_service
self.notification_service = notification_service
async def register_user(self, user_data: dict) -> dict:
user = await self.user_service.create_user(user_data)
await self.notification_service.notify_user_creation(user)
return user
# FastAPI Route with dependency injection
app = FastAPI()
@app.post("/register/")
async def register(user_data: dict, facade: UserFacade = Depends(UserFacade)):
user = await facade.register_user(user_data)
return {"status": "success", "user": user}
Decorator Pattern
The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class.
Use Cases in FastAPI
- Route Middleware: Adding pre-processing or post-processing to routes.
- Authentication/Authorization: Wrapping routes with security checks.
- Request/Response Transformation: Modifying or enriching incoming and outgoing data.
Advantages
- Enhances functionality without modifying existing code.
- Promotes code reuse and separation of concerns.
- Flexible addition and removal of behaviors.
Disadvantages
- Can lead to complex and hard-to-follow code if overused.
- May obscure the core logic of functions.
Decorator Pattern with FastAPI: Authentication
Creating a decorator to enforce authentication on specific routes.
from fastapi import FastAPI, HTTPException, Request
from functools import wraps
from typing import Callable
app = FastAPI()
def authenticate(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs):
request: Request = kwargs.get('request')
token = request.headers.get("Authorization")
if token != "Bearer validtoken":
raise HTTPException(status_code=401, detail="Unauthorized")
return await func(*args, **kwargs)
return wrapper
@app.get("/protected/")
@authenticate
async def protected_route(request: Request):
return {"message": "You are authenticated"}
Using FastAPI Dependencies as Decorators
While FastAPI’s dependency injection system often replaces the need for traditional decorators, you can still combine both for more complex scenarios.
from fastapi import Depends
def check_permissions(required_role: str):
def decorator(func: Callable):
async def wrapper(*args, **kwargs):
user = kwargs.get('current_user')
if user.get("role") != required_role:
raise HTTPException(status_code=403, detail="Forbidden")
return await func(*args, **kwargs)
return wrapper
return decorator
def get_current_user():
# Dummy implementation
return {"username": "johndoe", "role": "admin"}
@app.get("/admin/")
@check_permissions("admin")
async def admin_route(current_user: dict = Depends(get_current_user)):
return {"message": "Welcome, admin!"}