Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. They abstract the instantiation process, making a system independent of how its objects are created.

Singleton Pattern

  • The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
  • Use Cases:
    • Database Connections: Managing a single database connection pool.
    • Configuration Management: Ensuring a single configuration instance is used throughout the application.

Basic Singleton in Python

class Singleton:
  _instance = None

  def __new__(cls, *args, **kwargs):
      if not cls._instance:
          cls._instance = super(Singleton, cls).__new__(cls)
      return cls._instance

# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # Output: True

Singleton with Thread Safety For multi-threaded applications, ensure thread safety to prevent multiple instances.

import threading

class Singleton:
  _instance = None
  _lock = threading.Lock()

  def __new__(cls, *args, **kwargs):
      with cls._lock:
          if not cls._instance:
              cls._instance = super(Singleton, cls).__new__(cls)
      return cls._instance

# Usage remains the same

Practical Example in FastAPI: Database Connection Management

from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
from sqlalchemy.orm import sessionmaker

class Database:
  _engine: AsyncEngine = None
  _SessionLocal = None

  @classmethod
  def initialize(cls, DATABASE_URL: str):
      if not cls._engine:
          cls._engine = create_async_engine(DATABASE_URL, echo=True)
          cls._SessionLocal = sessionmaker(
              bind=cls._engine,
              class_=AsyncSession,
              expire_on_commit=False
          )

  @classmethod
  def get_engine(cls):
      if not cls._engine:
          raise Exception("Database not initialized.")
      return cls._engine

  @classmethod
  def get_session(cls):
      if not cls._SessionLocal:
          raise Exception("Database not initialized.")
      return cls._SessionLocal()
from fastapi import FastAPI, Depends

app = FastAPI()

# Initialize Database at startup
@app.on_event("startup")
async def startup():
  Database.initialize("postgresql+asyncpg://user:password@localhost/dbname")

# Dependency
async def get_db():
  async with Database.get_session() as session:
      yield session

@app.get("/users/")
async def get_users(db: AsyncSession = Depends(get_db)):
  result = await db.execute(select(UserModel))
  users = result.scalars().all()
  return users

Factory Pattern

The Factory pattern defines an interface for creating objects but allows subclasses to alter the type of objects that will be created. It promotes loose coupling by eliminating the need to bind application-specific classes into the code.

Use Cases in FastAPI

  • Creating Service Classes: Depending on the context, instantiate different service implementations.
  • Database Model Factories: Dynamically creating different database models based on configuration.
  • API Response Factories: Generating responses based on various conditions or formats.

Advantages

  • Encapsulates object creation.
  • Promotes code reusability and flexibility.
  • Facilitates easy addition of new types without modifying existing code.

Disadvantages

  • Can introduce complexity if overused.
  • Might obscure the code flow, making it harder to trace object creations.

Factory Pattern with FastAPI: Service Creation

from abc import ABC, abstractmethod

# Abstract Base Class
class PaymentGateway(ABC):
  @abstractmethod
  async def process_payment(self, amount: float):
      pass

# Concrete Implementations
class PayPalGateway(PaymentGateway):
  async def process_payment(self, amount: float):
      # Implement PayPal payment processing
      return f"Processed {amount} via PayPal"

class StripeGateway(PaymentGateway):
  async def process_payment(self, amount: float):
      # Implement Stripe payment processing
      return f"Processed {amount} via Stripe"

# Factory
class PaymentGatewayFactory:
  @staticmethod
  def get_gateway(gateway_name: str) -> PaymentGateway:
      if gateway_name == "paypal":
          return PayPalGateway()
      elif gateway_name == "stripe":
          return StripeGateway()
      else:
          raise ValueError("Unsupported payment gateway")

# FastAPI Route
from fastapi import FastAPI, HTTPException, Query

app = FastAPI()

@app.post("/pay/")
async def make_payment(amount: float, gateway: str = Query(...)):
  try:
      payment_gateway = PaymentGatewayFactory.get_gateway(gateway)
      result = await payment_gateway.process_payment(amount)
      return {"status": "success", "message": result}
  except ValueError as e:
      raise HTTPException(status_code=400, detail=str(e))

Builder Pattern

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

Use Cases in FastAPI

  • Complex Object Construction: Building objects with numerous optional parameters.
  • Request/Response Builders: Dynamically assembling responses based on various conditions.
  • Query Builders: Creating complex database queries programmatically.

Advantages

  • Provides precise control over the construction process.
  • Simplifies the creation of complex objects.
  • Enhances code readability and maintainability.

Disadvantages

  • Can introduce additional classes and complexity.
  • Might be overkill for simple object creation scenarios.

Builder Pattern with FastAPI: Request Filtering Imagine an API that allows users to filter items based on various optional parameters.

from typing import Optional
from fastapi import FastAPI, Query
from sqlalchemy.orm import Session

app = FastAPI()

# Assume ItemModel is a SQLAlchemy model

class QueryBuilder:
  def __init__(self, base_query):
      self.query = base_query

  def filter_by_name(self, name: Optional[str]):
      if name:
          self.query = self.query.filter(ItemModel.name == name)
      return self

  def filter_by_price_range(self, min_price: Optional[float], max_price: Optional[float]):
      if min_price is not None:
          self.query = self.query.filter(ItemModel.price >= min_price)
      if max_price is not None:
          self.query = self.query.filter(ItemModel.price <= max_price)
      return self

  def order_by_price(self, ascending: bool = True):
      if ascending:
          self.query = self.query.order_by(ItemModel.price)
      else:
          self.query = self.query.order_by(ItemModel.price.desc())
      return self

  def build(self):
      return self.query

@app.get("/items/")
async def get_items(
  name: Optional[str] = Query(None),
  min_price: Optional[float] = Query(None, ge=0),
  max_price: Optional[float] = Query(None, ge=0),
  sort_ascending: bool = True,
  db: Session = Depends(get_db)
):
  builder = QueryBuilder(db.query(ItemModel))
  items_query = (
      builder
      .filter_by_name(name)
      .filter_by_price_range(min_price, max_price)
      .order_by_price(sort_ascending)
      .build()
  )
  items = items_query.all()
  return items