Factory Pattern
Factory Pattern: Creating Objects the Smart Way
Section titled “Factory Pattern: Creating Objects the Smart Way”Now let’s dive into the Factory Pattern - one of the most commonly used creational design patterns.
Why Factory Pattern?
Section titled “Why Factory Pattern?”Imagine you’re ordering a pizza. You don’t need to know how the kitchen makes it - you just tell them “I want a pizza” and they handle the rest. The Factory Pattern works the same way!
The Factory Pattern lets you create objects without specifying their exact class. Instead of directly creating objects with new ClassName(), you ask a factory to create them for you.
What’s the Use of Factory Pattern?
Section titled “What’s the Use of Factory Pattern?”The Factory Pattern is useful when:
- You don’t know the exact type of object you need until runtime
- Object creation is complex - Lots of setup or configuration needed
- You want to decouple object creation from object usage
- You need flexibility - Easy to add new types without changing existing code
- You want centralized control - All object creation happens in one place
What Happens If We Don’t Use Factory Pattern?
Section titled “What Happens If We Don’t Use Factory Pattern?”Without the Factory Pattern, you might:
- Scatter object creation throughout your code
- Tightly couple your code to specific classes
- Make it hard to add new types - Need to modify code everywhere
- Duplicate creation logic - Same setup code in multiple places
- Violate Open/Closed Principle - Need to modify code to extend functionality
Simple Example: The Pizza Shop
Section titled “Simple Example: The Pizza Shop”Let’s start with a super simple example that anyone can understand!
Visual Representation
Section titled “Visual Representation”Interaction Flow
Section titled “Interaction Flow”Here’s how the Factory Pattern works in practice - showing the sequence of interactions:
sequenceDiagram
participant Client
participant Factory as PizzaFactory
participant Interface as Pizza Interface
participant Concrete as ConcretePizza
Client->>Factory: create_pizza("margherita")
activate Factory
Factory->>Concrete: new MargheritaPizza()
activate Concrete
Concrete-->>Factory: Pizza instance
deactivate Concrete
Factory-->>Client: Returns Pizza instance
deactivate Factory
Client->>Interface: pizza.prepare()
activate Interface
Interface-->>Client: "Preparing Margherita pizza..."
deactivate Interface
Note over Client,Concrete: Client doesn't know<br/>about concrete classes!
The Problem
Section titled “The Problem”You’re building a pizza ordering system. Customers can order different types of pizzas (Margherita, Pepperoni, Veggie). Without a factory, you’d do this:
# ❌ Without Factory Pattern - Direct creation everywhere
class MargheritaPizza: def prepare(self): return "Preparing Margherita pizza..."
class PepperoniPizza: def prepare(self): return "Preparing Pepperoni pizza..."
class VeggiePizza: def prepare(self): return "Preparing Veggie pizza..."
# Problem: Object creation scattered everywheredef order_pizza(pizza_type: str): if pizza_type == "margherita": pizza = MargheritaPizza() # Direct creation elif pizza_type == "pepperoni": pizza = PepperoniPizza() # Direct creation elif pizza_type == "veggie": pizza = VeggiePizza() # Direct creation else: raise ValueError("Unknown pizza type")
return pizza.prepare()
# Every time you add a new pizza type, you need to modify this function!// ❌ Without Factory Pattern - Direct creation everywhere
public class MargheritaPizza { public String prepare() { return "Preparing Margherita pizza..."; }}
public class PepperoniPizza { public String prepare() { return "Preparing Pepperoni pizza..."; }}
public class VeggiePizza { public String prepare() { return "Preparing Veggie pizza..."; }}
// Problem: Object creation scattered everywherepublic class PizzaOrder { public static String orderPizza(String pizzaType) { if ("margherita".equals(pizzaType)) { MargheritaPizza pizza = new MargheritaPizza(); // Direct creation return pizza.prepare(); } else if ("pepperoni".equals(pizzaType)) { PepperoniPizza pizza = new PepperoniPizza(); // Direct creation return pizza.prepare(); } else if ("veggie".equals(pizzaType)) { VeggiePizza pizza = new VeggiePizza(); // Direct creation return pizza.prepare(); } else { throw new IllegalArgumentException("Unknown pizza type"); } }}
// Every time you add a new pizza type, you need to modify this function!Problems:
- Need to modify
order_pizza()every time you add a new pizza type - Creation logic is scattered
- Hard to test - can’t easily swap implementations
- Violates Open/Closed Principle
The Solution: Factory Pattern
Section titled “The Solution: Factory Pattern”Class Structure
Section titled “Class Structure”classDiagram
class Pizza {
<<abstract>>
+prepare() str
}
class MargheritaPizza {
+prepare() str
}
class PepperoniPizza {
+prepare() str
}
class VeggiePizza {
+prepare() str
}
class PizzaFactory {
+create_pizza(type) Pizza
}
class Client {
+order_pizza(type) str
}
Pizza <|-- MargheritaPizza : implements
Pizza <|-- PepperoniPizza : implements
Pizza <|-- VeggiePizza : implements
PizzaFactory ..> Pizza : creates
Client --> PizzaFactory : uses
Client ..> Pizza : uses
from abc import ABC, abstractmethod
# Step 1: Define the interface (what all pizzas can do)class Pizza(ABC): @abstractmethod def prepare(self) -> str: """Prepare the pizza""" pass
# Step 2: Create concrete pizza classesclass MargheritaPizza(Pizza): def prepare(self) -> str: return "Preparing Margherita pizza with tomato and mozzarella..."
class PepperoniPizza(Pizza): def prepare(self) -> str: return "Preparing Pepperoni pizza with pepperoni and cheese..."
class VeggiePizza(Pizza): def prepare(self) -> str: return "Preparing Veggie pizza with bell peppers and mushrooms..."
# Step 3: Create the Factoryclass PizzaFactory: """Factory that creates pizzas based on type"""
@staticmethod def create_pizza(pizza_type: str) -> Pizza: """Factory method - creates pizza based on type""" pizzas = { "margherita": MargheritaPizza, "pepperoni": PepperoniPizza, "veggie": VeggiePizza }
pizza_class = pizzas.get(pizza_type.lower()) if not pizza_class: raise ValueError(f"Unknown pizza type: {pizza_type}")
return pizza_class() # Create and return the pizza
# Step 4: Use the factorydef order_pizza(pizza_type: str) -> str: """Order pizza using factory - no need to know specific pizza classes!""" pizza = PizzaFactory.create_pizza(pizza_type) # Factory handles creation return pizza.prepare()
# Usageprint(order_pizza("margherita")) # Works!print(order_pizza("pepperoni")) # Works!print(order_pizza("veggie")) # Works!
# To add a new pizza type, just:# 1. Create the class# 2. Add it to the factory's dictionary# No need to modify order_pizza()!// Step 1: Define the interface (what all pizzas can do)public interface Pizza { // Prepare the pizza String prepare();}
// Step 2: Create concrete pizza classespublic class MargheritaPizza implements Pizza { @Override public String prepare() { return "Preparing Margherita pizza with tomato and mozzarella..."; }}
public class PepperoniPizza implements Pizza { @Override public String prepare() { return "Preparing Pepperoni pizza with pepperoni and cheese..."; }}
public class VeggiePizza implements Pizza { @Override public String prepare() { return "Preparing Veggie pizza with bell peppers and mushrooms..."; }}
// Step 3: Create the Factorypublic class PizzaFactory { // Factory method - creates pizza based on type public static Pizza createPizza(String pizzaType) { switch (pizzaType.toLowerCase()) { case "margherita": return new MargheritaPizza(); case "pepperoni": return new PepperoniPizza(); case "veggie": return new VeggiePizza(); default: throw new IllegalArgumentException("Unknown pizza type: " + pizzaType); } }}
// Step 4: Use the factorypublic class PizzaOrder { // Order pizza using factory - no need to know specific pizza classes! public static String orderPizza(String pizzaType) { Pizza pizza = PizzaFactory.createPizza(pizzaType); // Factory handles creation return pizza.prepare(); }
public static void main(String[] args) { // Usage System.out.println(orderPizza("margherita")); // Works! System.out.println(orderPizza("pepperoni")); // Works! System.out.println(orderPizza("veggie")); // Works!
// To add a new pizza type, just: // 1. Create the class implementing Pizza // 2. Add it to the factory's switch statement // No need to modify orderPizza()! }}Real-World Software Example: Payment Processing System
Section titled “Real-World Software Example: Payment Processing System”Now let’s see a realistic software example - a payment processing system that needs to handle different payment methods.
The Problem
Section titled “The Problem”You’re building an e-commerce system that needs to process payments through different gateways (Stripe, PayPal, Square). Without Factory Pattern:
# ❌ Without Factory Pattern
class StripePayment: def __init__(self, api_key: str): self.api_key = api_key # Complex initialization...
def process(self, amount: float) -> dict: # Stripe-specific processing logic return {"status": "success", "gateway": "stripe", "amount": amount}
class PayPalPayment: def __init__(self, client_id: str, secret: str): self.client_id = client_id self.secret = secret # Complex initialization...
def process(self, amount: float) -> dict: # PayPal-specific processing logic return {"status": "success", "gateway": "paypal", "amount": amount}
class SquarePayment: def __init__(self, access_token: str): self.access_token = access_token # Complex initialization...
def process(self, amount: float) -> dict: # Square-specific processing logic return {"status": "success", "gateway": "square", "amount": amount}
# Problem: Complex creation logic scattered everywheredef process_payment(gateway: str, amount: float, config: dict) -> dict: """Process payment - but creation logic is messy!""" if gateway == "stripe": payment = StripePayment(config["stripe_api_key"]) # Complex creation elif gateway == "paypal": payment = PayPalPayment( config["paypal_client_id"], config["paypal_secret"] ) # Different parameters! elif gateway == "square": payment = SquarePayment(config["square_access_token"]) # Yet another way! else: raise ValueError(f"Unknown gateway: {gateway}")
return payment.process(amount)
# Problems:# - Different initialization for each gateway# - Need to know all gateway details# - Hard to add new gateways# - Configuration management is messyimport java.util.Map;import java.util.HashMap;
// ❌ Without Factory Pattern
public class StripePayment { private String apiKey;
public StripePayment(String apiKey) { this.apiKey = apiKey; // Complex initialization... }
public Map<String, Object> process(double amount) { // Stripe-specific processing logic Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "stripe"); result.put("amount", amount); return result; }}
public class PayPalPayment { private String clientId; private String secret;
public PayPalPayment(String clientId, String secret) { this.clientId = clientId; this.secret = secret; // Complex initialization... }
public Map<String, Object> process(double amount) { // PayPal-specific processing logic Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "paypal"); result.put("amount", amount); return result; }}
public class SquarePayment { private String accessToken;
public SquarePayment(String accessToken) { this.accessToken = accessToken; // Complex initialization... }
public Map<String, Object> process(double amount) { // Square-specific processing logic Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "square"); result.put("amount", amount); return result; }}
// Problem: Complex creation logic scattered everywherepublic class PaymentService { // Process payment - but creation logic is messy! public static Map<String, Object> processPayment(String gateway, double amount, Map<String, String> config) { if ("stripe".equals(gateway)) { StripePayment payment = new StripePayment(config.get("stripe_api_key")); // Complex creation return payment.process(amount); } else if ("paypal".equals(gateway)) { PayPalPayment payment = new PayPalPayment( config.get("paypal_client_id"), config.get("paypal_secret") ); // Different parameters! return payment.process(amount); } else if ("square".equals(gateway)) { SquarePayment payment = new SquarePayment(config.get("square_access_token")); // Yet another way! return payment.process(amount); } else { throw new IllegalArgumentException("Unknown gateway: " + gateway); } }}
// Problems:// - Different initialization for each gateway// - Need to know all gateway details// - Hard to add new gateways// - Configuration management is messyProblems:
- Complex creation logic scattered throughout code
- Need to know different initialization requirements for each gateway
- Hard to add new payment gateways
- Configuration management is messy
- Tight coupling to specific payment classes
The Solution: Factory Pattern
Section titled “The Solution: Factory Pattern”from abc import ABC, abstractmethodfrom typing import Dict, Any
# Step 1: Define the payment interfaceclass PaymentProcessor(ABC): """Interface for all payment processors"""
@abstractmethod def process(self, amount: float) -> Dict[str, Any]: """Process a payment""" pass
# Step 2: Create concrete payment processorsclass StripePayment(PaymentProcessor): def __init__(self, api_key: str): self.api_key = api_key # Initialize Stripe SDK, validate API key, etc. print(f"Initializing Stripe with API key: {api_key[:10]}...")
def process(self, amount: float) -> Dict[str, Any]: # Stripe-specific processing return { "status": "success", "gateway": "stripe", "amount": amount, "transaction_id": f"stripe_{amount}" }
class PayPalPayment(PaymentProcessor): def __init__(self, client_id: str, secret: str): self.client_id = client_id self.secret = secret # Initialize PayPal SDK, authenticate, etc. print(f"Initializing PayPal with client ID: {client_id[:10]}...")
def process(self, amount: float) -> Dict[str, Any]: # PayPal-specific processing return { "status": "success", "gateway": "paypal", "amount": amount, "transaction_id": f"paypal_{amount}" }
class SquarePayment(PaymentProcessor): def __init__(self, access_token: str): self.access_token = access_token # Initialize Square SDK, validate token, etc. print(f"Initializing Square with access token: {access_token[:10]}...")
def process(self, amount: float) -> Dict[str, Any]: # Square-specific processing return { "status": "success", "gateway": "square", "amount": amount, "transaction_id": f"square_{amount}" }
# Step 3: Create the Factoryclass PaymentProcessorFactory: """Factory for creating payment processors"""
@staticmethod def create_processor(gateway: str, config: Dict[str, str]) -> PaymentProcessor: """ Factory method - creates payment processor based on gateway type. Handles all the complex initialization logic! """ gateway = gateway.lower()
if gateway == "stripe": if "stripe_api_key" not in config: raise ValueError("Missing stripe_api_key in config") return StripePayment(config["stripe_api_key"])
elif gateway == "paypal": if "paypal_client_id" not in config or "paypal_secret" not in config: raise ValueError("Missing PayPal credentials in config") return PayPalPayment( config["paypal_client_id"], config["paypal_secret"] )
elif gateway == "square": if "square_access_token" not in config: raise ValueError("Missing square_access_token in config") return SquarePayment(config["square_access_token"])
else: raise ValueError(f"Unknown payment gateway: {gateway}")
# Step 4: Use the factory - clean and simple!def process_payment(gateway: str, amount: float, config: Dict[str, str]) -> Dict[str, Any]: """ Process payment using factory. No need to know about specific payment classes or their initialization! """ processor = PaymentProcessorFactory.create_processor(gateway, config) return processor.process(amount)
# Usage - Clean and simple!config = { "stripe_api_key": "sk_test_1234567890", "paypal_client_id": "paypal_client_123", "paypal_secret": "paypal_secret_456", "square_access_token": "sq_token_789"}
# Process payments - factory handles all the complexity!result1 = process_payment("stripe", 100.0, config)print(result1)
result2 = process_payment("paypal", 50.0, config)print(result2)
result3 = process_payment("square", 75.0, config)print(result3)import java.util.Map;import java.util.HashMap;
// Step 1: Define the payment interfacepublic interface PaymentProcessor { // Process a payment Map<String, Object> process(double amount);}
// Step 2: Create concrete payment processorspublic class StripePayment implements PaymentProcessor { private String apiKey;
public StripePayment(String apiKey) { this.apiKey = apiKey; // Initialize Stripe SDK, validate API key, etc. System.out.println("Initializing Stripe with API key: " + apiKey.substring(0, Math.min(10, apiKey.length())) + "..."); }
@Override public Map<String, Object> process(double amount) { // Stripe-specific processing Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "stripe"); result.put("amount", amount); result.put("transaction_id", "stripe_" + amount); return result; }}
public class PayPalPayment implements PaymentProcessor { private String clientId; private String secret;
public PayPalPayment(String clientId, String secret) { this.clientId = clientId; this.secret = secret; // Initialize PayPal SDK, authenticate, etc. System.out.println("Initializing PayPal with client ID: " + clientId.substring(0, Math.min(10, clientId.length())) + "..."); }
@Override public Map<String, Object> process(double amount) { // PayPal-specific processing Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "paypal"); result.put("amount", amount); result.put("transaction_id", "paypal_" + amount); return result; }}
public class SquarePayment implements PaymentProcessor { private String accessToken;
public SquarePayment(String accessToken) { this.accessToken = accessToken; // Initialize Square SDK, validate token, etc. System.out.println("Initializing Square with access token: " + accessToken.substring(0, Math.min(10, accessToken.length())) + "..."); }
@Override public Map<String, Object> process(double amount) { // Square-specific processing Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "square"); result.put("amount", amount); result.put("transaction_id", "square_" + amount); return result; }}
// Step 3: Create the Factorypublic class PaymentProcessorFactory { // Factory method - creates payment processor based on gateway type. // Handles all the complex initialization logic! public static PaymentProcessor createProcessor(String gateway, Map<String, String> config) { gateway = gateway.toLowerCase();
switch (gateway) { case "stripe": if (!config.containsKey("stripe_api_key")) { throw new IllegalArgumentException("Missing stripe_api_key in config"); } return new StripePayment(config.get("stripe_api_key"));
case "paypal": if (!config.containsKey("paypal_client_id") || !config.containsKey("paypal_secret")) { throw new IllegalArgumentException("Missing PayPal credentials in config"); } return new PayPalPayment( config.get("paypal_client_id"), config.get("paypal_secret") );
case "square": if (!config.containsKey("square_access_token")) { throw new IllegalArgumentException("Missing square_access_token in config"); } return new SquarePayment(config.get("square_access_token"));
default: throw new IllegalArgumentException("Unknown payment gateway: " + gateway); } }}
// Step 4: Use the factory - clean and simple!public class PaymentService { // Process payment using factory. // No need to know about specific payment classes or their initialization! public static Map<String, Object> processPayment(String gateway, double amount, Map<String, String> config) { PaymentProcessor processor = PaymentProcessorFactory.createProcessor(gateway, config); return processor.process(amount); }
public static void main(String[] args) { // Usage - Clean and simple! Map<String, String> config = new HashMap<>(); config.put("stripe_api_key", "sk_test_1234567890"); config.put("paypal_client_id", "paypal_client_123"); config.put("paypal_secret", "paypal_secret_456"); config.put("square_access_token", "sq_token_789");
// Process payments - factory handles all the complexity! Map<String, Object> result1 = processPayment("stripe", 100.0, config); System.out.println(result1);
Map<String, Object> result2 = processPayment("paypal", 50.0, config); System.out.println(result2);
Map<String, Object> result3 = processPayment("square", 75.0, config); System.out.println(result3); }}Adding a New Payment Gateway
Section titled “Adding a New Payment Gateway”With Factory Pattern, adding a new gateway is super easy:
# Step 1: Create the new payment classclass ApplePayPayment(PaymentProcessor): def __init__(self, merchant_id: str): self.merchant_id = merchant_id print(f"Initializing Apple Pay with merchant ID: {merchant_id}")
def process(self, amount: float) -> Dict[str, Any]: return { "status": "success", "gateway": "apple_pay", "amount": amount, "transaction_id": f"apple_{amount}" }
# Step 2: Add it to the factory (only change needed!)class PaymentProcessorFactory: @staticmethod def create_processor(gateway: str, config: Dict[str, str]) -> PaymentProcessor: gateway = gateway.lower()
# ... existing code ...
elif gateway == "apple_pay": # Just add this! if "apple_pay_merchant_id" not in config: raise ValueError("Missing apple_pay_merchant_id in config") return ApplePayPayment(config["apple_pay_merchant_id"])
# ... rest of code ...
# That's it! No changes needed to process_payment() function!# It automatically works with the new gateway!// Step 1: Create the new payment classpublic class ApplePayPayment implements PaymentProcessor { private String merchantId;
public ApplePayPayment(String merchantId) { this.merchantId = merchantId; System.out.println("Initializing Apple Pay with merchant ID: " + merchantId); }
@Override public Map<String, Object> process(double amount) { Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("gateway", "apple_pay"); result.put("amount", amount); result.put("transaction_id", "apple_" + amount); return result; }}
// Step 2: Add it to the factory (only change needed!)public class PaymentProcessorFactory { public static PaymentProcessor createProcessor(String gateway, Map<String, String> config) { gateway = gateway.toLowerCase();
// ... existing code ...
switch (gateway) { // ... existing cases ... case "apple_pay": // Just add this! if (!config.containsKey("apple_pay_merchant_id")) { throw new IllegalArgumentException("Missing apple_pay_merchant_id in config"); } return new ApplePayPayment(config.get("apple_pay_merchant_id")); // ... rest of code ... } }}
// That's it! No changes needed to processPayment() function!// It automatically works with the new gateway!Factory Pattern Variants
Section titled “Factory Pattern Variants”There are several variations of the Factory Pattern:
1. Simple Factory (Static Factory Method)
Section titled “1. Simple Factory (Static Factory Method)”The simplest form - a single method that creates objects:
class PizzaFactory: @staticmethod def create_pizza(pizza_type: str) -> Pizza: """Simple factory method""" if pizza_type == "margherita": return MargheritaPizza() elif pizza_type == "pepperoni": return PepperoniPizza() else: raise ValueError("Unknown pizza type")public class PizzaFactory { // Simple factory method public static Pizza createPizza(String pizzaType) { if ("margherita".equals(pizzaType)) { return new MargheritaPizza(); } else if ("pepperoni".equals(pizzaType)) { return new PepperoniPizza(); } else { throw new IllegalArgumentException("Unknown pizza type"); } }}2. Factory Method Pattern
Section titled “2. Factory Method Pattern”Each product has its own factory:
from abc import ABC, abstractmethod
class PizzaFactory(ABC): """Abstract factory""" @abstractmethod def create_pizza(self) -> Pizza: pass
class MargheritaPizzaFactory(PizzaFactory): def create_pizza(self) -> Pizza: return MargheritaPizza()
class PepperoniPizzaFactory(PizzaFactory): def create_pizza(self) -> Pizza: return PepperoniPizza()
# Usagefactory = MargheritaPizzaFactory()pizza = factory.create_pizza()// Abstract factorypublic abstract class PizzaFactory { public abstract Pizza createPizza();}
public class MargheritaPizzaFactory extends PizzaFactory { @Override public Pizza createPizza() { return new MargheritaPizza(); }}
public class PepperoniPizzaFactory extends PizzaFactory { @Override public Pizza createPizza() { return new PepperoniPizza(); }}
// Usagepublic class Main { public static void main(String[] args) { PizzaFactory factory = new MargheritaPizzaFactory(); Pizza pizza = factory.createPizza(); }}3. Abstract Factory Pattern
Section titled “3. Abstract Factory Pattern”Creates families of related objects:
from abc import ABC, abstractmethod
class UIFactory(ABC): """Abstract factory for UI components""" @abstractmethod def create_button(self): pass
@abstractmethod def create_dialog(self): pass
class WindowsUIFactory(UIFactory): def create_button(self): return WindowsButton()
def create_dialog(self): return WindowsDialog()
class MacUIFactory(UIFactory): def create_button(self): return MacButton()
def create_dialog(self): return MacDialog()// Abstract factory for UI componentspublic interface UIFactory { Button createButton(); Dialog createDialog();}
public class WindowsUIFactory implements UIFactory { @Override public Button createButton() { return new WindowsButton(); }
@Override public Dialog createDialog() { return new WindowsDialog(); }}
public class MacUIFactory implements UIFactory { @Override public Button createButton() { return new MacButton(); }
@Override public Dialog createDialog() { return new MacDialog(); }}When to Use Factory Pattern?
Section titled “When to Use Factory Pattern?”Use Factory Pattern when:
✅ Object creation is complex - Lots of setup, configuration, or validation
✅ You don’t know the exact type until runtime
✅ You want to decouple creation from usage
✅ You need flexibility - Easy to add new types
✅ You want centralized control - All creation in one place
✅ You’re following Open/Closed Principle - Open for extension, closed for modification
When NOT to Use Factory Pattern?
Section titled “When NOT to Use Factory Pattern?”Don’t use Factory Pattern when:
❌ Simple object creation - If new Class() is enough, don’t overcomplicate
❌ Only one type - If you only ever create one type, factory is unnecessary
❌ Creation logic is trivial - Don’t add abstraction for simple cases
❌ Performance is critical - Factory adds a small overhead (usually negligible)
Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Mistake 1: Over-Complicating Simple Cases
Section titled “Mistake 1: Over-Complicating Simple Cases”# ❌ Don't do this for simple cases!class SimpleObjectFactory: @staticmethod def create(): return SimpleObject() # Just use SimpleObject() directly!
# ✅ Better: Direct instantiation for simple casesobj = SimpleObject()// ❌ Don't do this for simple cases!public class SimpleObjectFactory { public static SimpleObject create() { return new SimpleObject(); // Just use new SimpleObject() directly! }}
// ✅ Better: Direct instantiation for simple casespublic class Main { public static void main(String[] args) { SimpleObject obj = new SimpleObject(); }}Mistake 2: Factory Creating Wrong Types
Section titled “Mistake 2: Factory Creating Wrong Types”# ❌ Factory returning inconsistent typesclass BadFactory: @staticmethod def create(type: str): if type == "a": return "string" # Returns string elif type == "b": return 42 # Returns int - inconsistent! else: return None # Returns None - even worse!
# ✅ Better: Factory should return consistent typesclass GoodFactory: @staticmethod def create(type: str) -> BaseClass: # Always returns BaseClass if type == "a": return ClassA() elif type == "b": return ClassB() else: raise ValueError("Unknown type")// ❌ Factory returning inconsistent typespublic class BadFactory { public static Object create(String type) { if ("a".equals(type)) { return "string"; // Returns string } else if ("b".equals(type)) { return 42; // Returns int - inconsistent! } else { return null; // Returns null - even worse! } }}
// ✅ Better: Factory should return consistent typespublic class GoodFactory { public static BaseClass create(String type) { // Always returns BaseClass if ("a".equals(type)) { return new ClassA(); } else if ("b".equals(type)) { return new ClassB(); } else { throw new IllegalArgumentException("Unknown type"); } }}Mistake 3: Factory with Too Many Responsibilities
Section titled “Mistake 3: Factory with Too Many Responsibilities”# ❌ Factory doing too muchclass GodFactory: @staticmethod def create(type: str): # Creates object obj = SomeClass() # Validates it obj.validate() # Saves to database obj.save() # Sends notification obj.notify() # Logs everything obj.log() return obj # Factory should only create, not do everything!
# ✅ Better: Factory only creates, other classes handle other concernsclass GoodFactory: @staticmethod def create(type: str) -> BaseClass: return SomeClass() # Just create and return!// ❌ Factory doing too muchpublic class GodFactory { public static SomeClass create(String type) { // Creates object SomeClass obj = new SomeClass(); // Validates it obj.validate(); // Saves to database obj.save(); // Sends notification obj.notify(); // Logs everything obj.log(); return obj; // Factory should only create, not do everything! }}
// ✅ Better: Factory only creates, other classes handle other concernspublic class GoodFactory { public static BaseClass create(String type) { return new SomeClass(); // Just create and return! }}Benefits of Factory Pattern
Section titled “Benefits of Factory Pattern”- Decoupling - Client code doesn’t depend on concrete classes
- Flexibility - Easy to add new types without changing existing code
- Centralized Control - All creation logic in one place
- Hides Complexity - Client doesn’t need to know creation details
- Testability - Easy to mock factories for testing
- Follows SOLID Principles - Especially Open/Closed and Dependency Inversion
Revision: Quick Catch-Up
Section titled “Revision: Quick Catch-Up”What is Factory Pattern?
Section titled “What is Factory Pattern?”Factory Pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. The factory decides which class to instantiate based on the input.
Why Use It?
Section titled “Why Use It?”- ✅ Decouple object creation from usage
- ✅ Centralize creation logic
- ✅ Simplify adding new types
- ✅ Hide complex initialization
- ✅ Follow Open/Closed Principle
How It Works?
Section titled “How It Works?”- Define an interface - What all products can do
- Create concrete classes - Specific implementations
- Create a factory - Handles object creation
- Use the factory - Client asks factory to create objects
Key Components
Section titled “Key Components”Client → Factory → Interface → Concrete Classes- Client - Uses the factory to get objects
- Factory - Creates objects based on input
- Interface - Defines what products can do
- Concrete Classes - Specific implementations
Simple Example
Section titled “Simple Example”# Interfaceclass Pizza(ABC): @abstractmethod def prepare(self): pass
# Concrete classesclass MargheritaPizza(Pizza): ...class PepperoniPizza(Pizza): ...
# Factoryclass PizzaFactory: @staticmethod def create(type: str) -> Pizza: if type == "margherita": return MargheritaPizza() # ...
# Usagepizza = PizzaFactory.create("margherita")When to Use?
Section titled “When to Use?”✅ Complex object creation
✅ Runtime type determination
✅ Need flexibility to add new types
✅ Want to decouple creation from usage
✅ Following Open/Closed Principle
When NOT to Use?
Section titled “When NOT to Use?”❌ Simple object creation
❌ Only one type ever created
❌ Trivial creation logic
❌ Performance is critical
Key Takeaways
Section titled “Key Takeaways”- Factory Pattern = Create objects without knowing exact class
- Factory = Centralized object creation
- Interface = Common contract for all products
- Benefit = Easy to extend, hard to break
- Principle = Open for extension, closed for modification
Common Pattern Structure
Section titled “Common Pattern Structure”# 1. Interfaceclass Product(ABC): @abstractmethod def do_something(self): pass
# 2. Concrete Productsclass ProductA(Product): ...class ProductB(Product): ...
# 3. Factoryclass Factory: @staticmethod def create(type: str) -> Product: # Creation logic here pass
# 4. Usageproduct = Factory.create("type_a")product.do_something()Remember
Section titled “Remember”- Factory Pattern simplifies object creation
- It decouples client from concrete classes
- It makes code more flexible and easier to extend
- Use it when creation is complex or you need flexibility
- Don’t use it for simple cases - avoid over-engineering!
Interview Focus: Factory Pattern
Section titled “Interview Focus: Factory Pattern”Key Points to Remember
Section titled “Key Points to Remember”1. Core Concept
Section titled “1. Core Concept”What to say:
“Factory Pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. The factory decides which class to instantiate based on the input.”
Why it matters:
- Shows you understand the fundamental purpose
- Demonstrates knowledge of creational patterns category
- Indicates you can explain concepts clearly
2. When to Use Factory Pattern
Section titled “2. When to Use Factory Pattern”Must mention:
- ✅ Complex object creation - When initialization involves multiple steps
- ✅ Runtime type determination - When you don’t know the type until runtime
- ✅ Decoupling - When you want to separate creation from usage
- ✅ Extensibility - When you need to easily add new types
- ✅ Open/Closed Principle - When you want to extend without modifying
Example scenario to give:
“I’d use Factory Pattern when building a payment processing system where I need to support multiple payment gateways (Stripe, PayPal, Square). Each gateway has different initialization requirements, and I want to add new gateways without modifying existing code.”
3. Structure and Components
Section titled “3. Structure and Components”Must explain:
- Product Interface - Common interface for all products
- Concrete Products - Specific implementations
- Factory - Creates products based on input
- Client - Uses factory to get products
Visual explanation:
Client → Factory.create(type) → Returns Product → Concrete Implementation4. Benefits and Trade-offs
Section titled “4. Benefits and Trade-offs”Benefits to mention:
- Decoupling - Client doesn’t depend on concrete classes
- Flexibility - Easy to add new types
- Centralized Control - All creation logic in one place
- Testability - Easy to mock factories for testing
- Follows SOLID - Especially Open/Closed Principle
Trade-offs to acknowledge:
- Complexity - Adds abstraction layer (may be overkill for simple cases)
- Performance - Small overhead (usually negligible)
- Over-engineering risk - Don’t use for trivial cases
5. Common Interview Questions
Section titled “5. Common Interview Questions”Q: “What’s the difference between Factory Pattern and Factory Method Pattern?”
A:
“Factory Pattern (Simple Factory) uses a single method to create objects based on input. Factory Method Pattern is more advanced - each product has its own factory class. Factory Method provides more flexibility and follows the Open/Closed Principle better, but Simple Factory is often sufficient for most cases.”
Q: “When would you NOT use Factory Pattern?”
A:
“I wouldn’t use Factory Pattern when object creation is trivial - like creating a simple object with
new Class(). Also, if I only ever create one type of object, a factory is unnecessary. I’d avoid it if performance is critical and the overhead matters, though usually the overhead is negligible.”
Q: “How does Factory Pattern relate to SOLID principles?”
A:
“Factory Pattern primarily supports the Open/Closed Principle - you can add new product types without modifying existing code. It also supports Dependency Inversion Principle by making clients depend on abstractions (the interface) rather than concrete classes. Additionally, it can help with Single Responsibility Principle by separating creation logic from business logic.”
6. Implementation Details
Section titled “6. Implementation Details”Key implementation points:
-
Use Abstract Base Class (ABC) for the product interface
from abc import ABC, abstractmethodclass Product(ABC):@abstractmethoddef do_something(self): pass -
Factory method should return the interface type
def create(type: str) -> Product: # Return interface, not concrete class -
Handle error cases - Invalid types should raise exceptions
if not product_class:raise ValueError(f"Unknown type: {type}") -
Consider making factory static - If no state needed
@staticmethoddef create(type: str) -> Product:
7. Real-World Examples
Section titled “7. Real-World Examples”Good examples to mention:
- Payment Processing - Different payment gateways
- Database Connections - Different database types (MySQL, PostgreSQL)
- UI Components - Different button types (Windows, Mac, Linux)
- Logging Systems - Different log handlers (File, Console, Database)
- Notification Systems - Different channels (Email, SMS, Push)
8. Common Mistakes to Avoid
Section titled “8. Common Mistakes to Avoid”Mistakes interviewers watch for:
-
Over-engineering - Using Factory for simple cases
- ❌ Bad: Factory for creating simple objects
- ✅ Good: Factory for complex, multi-step creation
-
Returning inconsistent types - Factory should return interface type
- ❌ Bad: Returning different types (string, int, object)
- ✅ Good: Always return the interface type
-
Factory doing too much - Factory should only create, not validate/save/log
- ❌ Bad: Factory creates, validates, saves, and logs
- ✅ Good: Factory only creates and returns
-
Not handling errors - Should validate input and raise exceptions
- ❌ Bad: Returning None for invalid types
- ✅ Good: Raising ValueError with clear message
9. Comparison with Other Patterns
Section titled “9. Comparison with Other Patterns”Factory vs Builder:
- Factory - Creates objects of different types (Margherita vs Pepperoni pizza)
- Builder - Creates objects with complex construction (Pizza with many toppings)
Factory vs Singleton:
- Factory - Creates multiple instances of different types
- Singleton - Ensures only one instance exists
Factory vs Abstract Factory:
- Factory - Creates one type of product
- Abstract Factory - Creates families of related products
10. Code Quality Points
Section titled “10. Code Quality Points”What interviewers look for:
✅ Clean code - Readable, well-structured
✅ Type hints - Proper type annotations
✅ Error handling - Validates input, raises exceptions
✅ Documentation - Clear docstrings
✅ SOLID principles - Follows design principles
✅ Testability - Easy to test and mock
Example of good code:
from abc import ABC, abstractmethodfrom typing import Dict, Any
class PaymentProcessor(ABC): """Interface for payment processors""" @abstractmethod def process(self, amount: float) -> Dict[str, Any]: """Process a payment""" pass
class PaymentFactory: """Factory for creating payment processors"""
@staticmethod def create(gateway: str, config: Dict[str, str]) -> PaymentProcessor: """ Creates a payment processor based on gateway type.
Args: gateway: Type of payment gateway config: Configuration dictionary
Returns: PaymentProcessor instance
Raises: ValueError: If gateway type is unknown or config is invalid """ # Implementation...import java.util.Map;
public interface PaymentProcessor { /** * Interface for payment processors */ Map<String, Object> process(double amount);}
public class PaymentFactory { /** * Factory for creating payment processors */ public static PaymentProcessor create(String gateway, Map<String, String> config) { /** * Creates a payment processor based on gateway type. * * @param gateway Type of payment gateway * @param config Configuration map * @return PaymentProcessor instance * @throws IllegalArgumentException If gateway type is unknown or config is invalid */ // Implementation... return null; }}Interview Checklist
Section titled “Interview Checklist”Before your interview, make sure you can:
- Define Factory Pattern clearly in one sentence
- Explain when to use it (with examples)
- Describe the structure and components
- List benefits and trade-offs
- Compare with other creational patterns
- Implement Factory Pattern from scratch
- Connect to SOLID principles
- Identify when NOT to use it
- Give 2-3 real-world examples
- Discuss common mistakes and how to avoid them
Remember: Factory Pattern is about delegating object creation to a specialized factory, making your code more flexible and maintainable! 🏭