Factory

Factories define an interface for creating an object, but allow subclasses to alter the type of object created. It centralizes object creation while allowing flexibility for different object types.

TL;DR

Factories provide a way to delegate insanitation logic to child classes, used when the client doesn’t know what exact subclass it might need.

Problem

Suppose an application handles the creation of employees for company records. If we create a class for each type of employee, the code will quickly become verbose and redundant, since each department will calculate payment structures differently and have different mechanisms for doing the same thing. As a result, a base class of Department would not be enough since Devs, Marketing, Accounts etc. would be tightly coupled to the base class itself and would have to implement much of the same logic.

Real-world analogy

Imagine you’re running a food delivery service from different specialized restaurants. In this scenario we have:

  • The Customer (Client) that orders food through an app. They don’t care about the details of the kitchen itself, just that the food is delivered.
  • The Restaurant Manager (Factory) who knows exactly how to prepare food based on what restaurant they manage.
  • The Meal (Product) is created by different restaurants in different ways, but they are all packaged and delivered the same way.

In this case, the process of getting food delivered is the same (place order get meal), but each manager (factory) will create a different type of meal appropriate for the request. this works because the client does not need to know anything about the details of the kitchen they simply ask for something and get the correct product. Nonetheless, each restaurant is specialized in making a specific type of product, and each implements the same interface.

Solution

from abc import ABC, abstractmethod
 
# The Meal (Product)
class Meal(ABC):
    @abstractmethod
    def serve(self) -> str:
        pass
 
class Pasta(Meal):
    def serve(self) -> str:
        return "🍝 Serving delicious pasta!"
 
class Sushi(Meal):
    def serve(self) -> str:
        return "🍣 Serving fresh sushi!"
 
# The Restaurant (Factory)
class Restaurant(ABC):
    @abstractmethod
    def prepare_lunch(self) -> Meal:
        """Factory method - each restaurant makes their specialty"""
        pass
    
    def serve_customer(self) -> str:
        """Template method - same process for all restaurants"""
        meal = self.prepare_lunch()  # Use factory method
        return f"Order ready! {meal.serve()}"
 
class ItalianRestaurant(Restaurant):
    def prepare_lunch(self) -> Meal:
        return Pasta()
 
class SushiRestaurant(Restaurant):
    def prepare_lunch(self) -> Meal:
        return Sushi()
 
# Customer code
def order_lunch(restaurant: Restaurant):
    """Customer doesn't know what meal they'll get - just orders lunch"""
    print(restaurant.serve_customer())
 
# Demo
if __name__ == "__main__":
    print("=== Ordering lunch from different restaurants ===\n")
    
    order_lunch(ItalianRestaurant())  # Gets pasta
    order_lunch(SushiRestaurant())    # Gets sushi
    
    print("\n✨ Same order process, different meals based on restaurant type!")

We can also use the __new__ method to override what gets created (see Python new):

# 4. CONDITIONAL SUBCLASS CREATION
class DatabaseConnection:
    """Creates different connection types based on URL"""
    
    def __new__(cls, url):
        if url.startswith('postgresql://'):
            return PostgreSQLConnection(url)
        elif url.startswith('mysql://'):
            return MySQLConnection(url)
        elif url.startswith('sqlite://'):
            return SQLiteConnection(url)
        else:
            raise ValueError(f"Unsupported database URL: {url}")
 
class PostgreSQLConnection:
    def __init__(self, url):
        self.url = url
        self.type = "PostgreSQL"
    
    def __repr__(self):
        return f"PostgreSQLConnection({self.url})"
 
class MySQLConnection:
    def __init__(self, url):
        self.url = url
        self.type = "MySQL"
    
    def __repr__(self):
        return f"MySQLConnection({self.url})"
 
class SQLiteConnection:
    def __init__(self, url):
        self.url = url
        self.type = "SQLite"
    
    def __repr__(self):
        return f"SQLiteConnection({self.url})"
 
# Usage
pg_conn = DatabaseConnection('postgresql://localhost/mydb')
mysql_conn = DatabaseConnection('mysql://localhost/mydb')
sqlite_conn = DatabaseConnection('sqlite:///mydb.db')