Olox Olox

Theme

Documentation
Back to Home

Factory Patterns

7 min read

Factory Patterns

📚 Overview

Factory patterns deal with object creation, hiding the instantiation logic from the client.

  1. Simple Factory - Not a GoF pattern but commonly used
  2. Factory Method - Define interface, let subclasses decide what to create
  3. Abstract Factory - Create families of related objects

1️⃣ Simple Factory

from enum import Enum
from abc import ABC, abstractmethod

class VehicleType(Enum):
    CAR = "car"
    BIKE = "bike"
    TRUCK = "truck"

class Vehicle(ABC):
    @abstractmethod
    def drive(self) -> str:
        pass

class Car(Vehicle):
    def drive(self) -> str:
        return "Driving a car 🚗"

class Bike(Vehicle):
    def drive(self) -> str:
        return "Riding a bike 🏍️"

class Truck(Vehicle):
    def drive(self) -> str:
        return "Driving a truck 🚚"


class VehicleFactory:
    """Simple Factory - centralized creation logic"""
    
    @staticmethod
    def create(vehicle_type: VehicleType) -> Vehicle:
        factories = {
            VehicleType.CAR: Car,
            VehicleType.BIKE: Bike,
            VehicleType.TRUCK: Truck,
        }
        
        if vehicle_type not in factories:
            raise ValueError(f"Unknown vehicle type: {vehicle_type}")
        
        return factories[vehicle_type]()


# Usage
car = VehicleFactory.create(VehicleType.CAR)
print(car.drive())  # "Driving a car 🚗"

2️⃣ Factory Method Pattern

Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate.

from abc import ABC, abstractmethod

# Product hierarchy
class Document(ABC):
    @abstractmethod
    def create(self) -> str:
        pass
    
    @abstractmethod
    def save(self) -> str:
        pass

class PDFDocument(Document):
    def create(self) -> str:
        return "Creating PDF document"
    
    def save(self) -> str:
        return "Saving as .pdf"

class WordDocument(Document):
    def create(self) -> str:
        return "Creating Word document"
    
    def save(self) -> str:
        return "Saving as .docx"

class ExcelDocument(Document):
    def create(self) -> str:
        return "Creating Excel spreadsheet"
    
    def save(self) -> str:
        return "Saving as .xlsx"


# Creator hierarchy
class Application(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        """Factory Method - subclasses override this"""
        pass
    
    def new_document(self) -> str:
        """Template method using factory method"""
        doc = self.create_document()
        result = doc.create()
        return result
    
    def save_document(self) -> str:
        doc = self.create_document()
        return doc.save()


class PDFApplication(Application):
    def create_document(self) -> Document:
        return PDFDocument()

class WordApplication(Application):
    def create_document(self) -> Document:
        return WordDocument()

class ExcelApplication(Application):
    def create_document(self) -> Document:
        return ExcelDocument()


# Usage
apps = [PDFApplication(), WordApplication(), ExcelApplication()]
for app in apps:
    print(app.new_document())

Factory Method with Registration

class DocumentFactory:
    """Extensible factory using registration"""
    _creators = {}
    
    @classmethod
    def register(cls, doc_type: str, creator):
        cls._creators[doc_type] = creator
    
    @classmethod
    def create(cls, doc_type: str, **kwargs) -> Document:
        creator = cls._creators.get(doc_type)
        if not creator:
            raise ValueError(f"Unknown document type: {doc_type}")
        return creator(**kwargs)


# Register creators
DocumentFactory.register("pdf", PDFDocument)
DocumentFactory.register("word", WordDocument)
DocumentFactory.register("excel", ExcelDocument)

# Usage
doc = DocumentFactory.create("pdf")

3️⃣ Abstract Factory Pattern

Intent: Create families of related objects without specifying concrete classes.

from abc import ABC, abstractmethod

# Abstract Products
class Button(ABC):
    @abstractmethod
    def render(self) -> str:
        pass
    
    @abstractmethod
    def on_click(self, callback) -> str:
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self) -> str:
        pass
    
    @abstractmethod
    def toggle(self) -> str:
        pass

class TextInput(ABC):
    @abstractmethod
    def render(self) -> str:
        pass
    
    @abstractmethod
    def get_value(self) -> str:
        pass


# Windows Family
class WindowsButton(Button):
    def render(self) -> str:
        return "[====Windows Button====]"
    
    def on_click(self, callback) -> str:
        return f"Windows click: {callback}"

class WindowsCheckbox(Checkbox):
    def render(self) -> str:
        return "[☑] Windows Checkbox"
    
    def toggle(self) -> str:
        return "Windows checkbox toggled"

class WindowsTextInput(TextInput):
    def render(self) -> str:
        return "|____Windows Input____|"
    
    def get_value(self) -> str:
        return "Windows input value"


# Mac Family
class MacButton(Button):
    def render(self) -> str:
        return "(  Mac Button  )"
    
    def on_click(self, callback) -> str:
        return f"Mac click: {callback}"

class MacCheckbox(Checkbox):
    def render(self) -> str:
        return "(✓) Mac Checkbox"
    
    def toggle(self) -> str:
        return "Mac checkbox toggled"

class MacTextInput(TextInput):
    def render(self) -> str:
        return "(___Mac Input___)"
    
    def get_value(self) -> str:
        return "Mac input value"


# Linux Family
class LinuxButton(Button):
    def render(self) -> str:
        return "<Linux Button>"
    
    def on_click(self, callback) -> str:
        return f"Linux click: {callback}"

class LinuxCheckbox(Checkbox):
    def render(self) -> str:
        return "<X> Linux Checkbox"
    
    def toggle(self) -> str:
        return "Linux checkbox toggled"

class LinuxTextInput(TextInput):
    def render(self) -> str:
        return "<__Linux Input__>"
    
    def get_value(self) -> str:
        return "Linux input value"


# Abstract Factory
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass
    
    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass
    
    @abstractmethod
    def create_text_input(self) -> TextInput:
        pass


# Concrete Factories
class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()
    
    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()
    
    def create_text_input(self) -> TextInput:
        return WindowsTextInput()


class MacFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacButton()
    
    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()
    
    def create_text_input(self) -> TextInput:
        return MacTextInput()


class LinuxFactory(GUIFactory):
    def create_button(self) -> Button:
        return LinuxButton()
    
    def create_checkbox(self) -> Checkbox:
        return LinuxCheckbox()
    
    def create_text_input(self) -> TextInput:
        return LinuxTextInput()


# Client code - works with any factory
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory
        self.button = None
        self.checkbox = None
        self.text_input = None
    
    def create_ui(self):
        self.button = self.factory.create_button()
        self.checkbox = self.factory.create_checkbox()
        self.text_input = self.factory.create_text_input()
    
    def render(self) -> str:
        return "\n".join([
            self.button.render(),
            self.checkbox.render(),
            self.text_input.render()
        ])


# Factory selector
def get_factory(os_type: str) -> GUIFactory:
    factories = {
        "windows": WindowsFactory(),
        "mac": MacFactory(),
        "linux": LinuxFactory()
    }
    return factories.get(os_type.lower(), LinuxFactory())


# Usage
import platform
os_type = platform.system().lower()
factory = get_factory(os_type)

app = Application(factory)
app.create_ui()
print(app.render())

🏢 Real-World Examples

Database Connection Factory

from abc import ABC, abstractmethod
from typing import Optional

class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self) -> str:
        pass
    
    @abstractmethod
    def execute(self, query: str) -> list:
        pass
    
    @abstractmethod
    def close(self) -> None:
        pass

class MySQLConnection(DatabaseConnection):
    def __init__(self, host: str, port: int, database: str):
        self.host = host
        self.port = port
        self.database = database
        self._connection = None
    
    def connect(self) -> str:
        self._connection = f"MySQL://{self.host}:{self.port}/{self.database}"
        return f"Connected to MySQL at {self.host}"
    
    def execute(self, query: str) -> list:
        return [f"MySQL result for: {query}"]
    
    def close(self) -> None:
        self._connection = None

class PostgreSQLConnection(DatabaseConnection):
    def __init__(self, host: str, port: int, database: str):
        self.host = host
        self.port = port
        self.database = database
        self._connection = None
    
    def connect(self) -> str:
        self._connection = f"PostgreSQL://{self.host}:{self.port}/{self.database}"
        return f"Connected to PostgreSQL at {self.host}"
    
    def execute(self, query: str) -> list:
        return [f"PostgreSQL result for: {query}"]
    
    def close(self) -> None:
        self._connection = None

class MongoDBConnection(DatabaseConnection):
    def __init__(self, host: str, port: int, database: str):
        self.host = host
        self.port = port
        self.database = database
    
    def connect(self) -> str:
        return f"Connected to MongoDB at {self.host}"
    
    def execute(self, query: str) -> list:
        return [f"MongoDB result for: {query}"]
    
    def close(self) -> None:
        pass


class DatabaseFactory:
    @staticmethod
    def create(db_type: str, host: str, port: int, database: str) -> DatabaseConnection:
        factories = {
            "mysql": MySQLConnection,
            "postgresql": PostgreSQLConnection,
            "postgres": PostgreSQLConnection,
            "mongodb": MongoDBConnection,
            "mongo": MongoDBConnection,
        }
        
        creator = factories.get(db_type.lower())
        if not creator:
            raise ValueError(f"Unsupported database: {db_type}")
        
        return creator(host, port, database)


# Usage
config = {"type": "postgresql", "host": "localhost", "port": 5432, "database": "myapp"}
db = DatabaseFactory.create(**config)
print(db.connect())

Payment Processor Factory

class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount: float) -> dict:
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str, amount: float) -> dict:
        pass

class StripeProcessor(PaymentProcessor):
    def __init__(self, api_key: str):
        self.api_key = api_key
    
    def process(self, amount: float) -> dict:
        return {"provider": "stripe", "amount": amount, "status": "success"}
    
    def refund(self, transaction_id: str, amount: float) -> dict:
        return {"provider": "stripe", "refund": amount, "status": "refunded"}

class PayPalProcessor(PaymentProcessor):
    def __init__(self, client_id: str, client_secret: str):
        self.client_id = client_id
        self.client_secret = client_secret
    
    def process(self, amount: float) -> dict:
        return {"provider": "paypal", "amount": amount, "status": "success"}
    
    def refund(self, transaction_id: str, amount: float) -> dict:
        return {"provider": "paypal", "refund": amount, "status": "refunded"}

class SquareProcessor(PaymentProcessor):
    def __init__(self, access_token: str):
        self.access_token = access_token
    
    def process(self, amount: float) -> dict:
        return {"provider": "square", "amount": amount, "status": "success"}
    
    def refund(self, transaction_id: str, amount: float) -> dict:
        return {"provider": "square", "refund": amount, "status": "refunded"}


class PaymentFactory:
    _processors = {}
    
    @classmethod
    def register(cls, name: str, processor_cls, **default_kwargs):
        cls._processors[name] = (processor_cls, default_kwargs)
    
    @classmethod
    def create(cls, name: str, **kwargs) -> PaymentProcessor:
        if name not in cls._processors:
            raise ValueError(f"Unknown payment processor: {name}")
        
        processor_cls, defaults = cls._processors[name]
        merged_kwargs = {**defaults, **kwargs}
        return processor_cls(**merged_kwargs)


# Register processors
PaymentFactory.register("stripe", StripeProcessor, api_key="sk_test_xxx")
PaymentFactory.register("paypal", PayPalProcessor, client_id="xxx", client_secret="yyy")
PaymentFactory.register("square", SquareProcessor, access_token="sq_xxx")

# Usage
processor = PaymentFactory.create("stripe")
result = processor.process(99.99)

🔄 Comparison

AspectSimple FactoryFactory MethodAbstract Factory
ComplexityLowMediumHigh
ExtensibilityModify factoryAdd new subclassAdd new factory
ProductsOne typeOne typeFamily of types
CreationStatic methodInheritanceComposition
When to useSimple needsNeed variationsRelated products

✅ When to Use

Factory Method

  • Class can’t anticipate the objects it must create
  • Class wants subclasses to specify created objects
  • Need to localize object creation

Abstract Factory

  • System should be independent of product creation
  • System needs to work with multiple product families
  • Related products must be used together

  • Singleton - Factories often are singletons
  • Prototype - Alternative to factory when classes vary at runtime
  • Builder - For complex object construction

Last Updated: 2024