ποΈ Factory Pattern: Smart Coupon System
π Overview
The Factory Method Pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It centralizes object creation logic, making the code more flexible and easier to extend without modifying the existing business logic.
Core Concepts
- Creation Decoupling: The code that uses the object (the Client) is separated from the code that creates it (the Factory).
- Polymorphic Creation: The factory can return any object that implements a specific interface (e.g.,
Coupon). - Null Object Fallback: Handling unrecognized inputs by returning a safe "do-nothing" object instead of
None.
π The Engineering Story & Problem
π‘ The Villain (The Problem)
You're building an e-commerce checkout system. Marketing keeps inventing new coupon types: PercentageOff, FlatAmount, BuyOneGetOne, FirstPurchaseBonus.
In the "Hardcoded Switch" version, your DiscountEngine is a mess:
class DiscountEngine:
def apply_coupon(self, code_data, total):
# π‘ Maintenance nightmare: logic mixed with creation
if code_data['type'] == 'PERCENT':
coupon = PercentageDiscount(code_data['value'])
elif code_data['type'] == 'FLAT':
coupon = FlatDiscount(code_data['value'])
# Adding a new type requires editing this engine!
return coupon.apply(total)
DiscountEngine. This violates the Open/Closed Principle and makes the engine risky to change.
π¦Έ The Hero (The Solution)
The Factory Pattern introduces the "Specialized Creator."
We pull the "How to create a coupon" logic out into a CouponFactory.
The DiscountEngine now just says: coupon = CouponFactory.get_coupon(code_data).
It doesn't care if the factory returns a PercentageDiscount or a BOGODiscount. It just knows the result is a Coupon that has an apply() method.
You can now add 50 new coupon types by just updating the Factory. The DiscountEngine remains clean and never needs to change.
π Requirements & Constraints
- (Functional): Create different discount objects (Percentage, Flat) based on raw input data.
- (Technical): The main engine must depend only on the
Couponinterface. - (Technical): Return a
NoDiscountobject if the input is invalid (Null Object pattern).
ποΈ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class Coupon {
<<interface>>
+apply(price) float
}
class PercentageCoupon {
+apply(price)
}
class FlatCoupon {
+apply(price)
}
class CouponFactory {
+get_coupon(data) Coupon
}
Coupon <|.. PercentageCoupon
Coupon <|.. FlatCoupon
CouponFactory ..> Coupon : creates
Runtime Context (Sequence)
sequenceDiagram
participant Engine as DiscountEngine
participant Factory as CouponFactory
participant Coupon as PercentCoupon
Engine->>Factory: get_coupon({"type": "PERCENT", "val": 10})
Factory-->>Engine: PercentCoupon Instance
Engine->>Coupon: apply(100.0)
Coupon-->>Engine: 90.0
π» Implementation & Code
π§ SOLID Principles Applied
- Single Responsibility: The Factory handles creation; the Engine handles discount calculation.
- Open/Closed: Add a
HolidayDiscountclass without changing theDiscountEngine.
π The Code
The Villain's Code (Without Pattern)
The Hero's Code (With Pattern)
from abc import ABC, abstractmethod
class Coupon(ABC):
def __init__(self, code: str, value: float) -> None:
self.code : str = code
self.value : float = value
@abstractmethod
def applyDiscount(self, price: float) -> float:
pass
class PercentageCoupon(Coupon):
def applyDiscount(self, price: float) -> float:
print(f"Applying Percentage discount of {self.value}% on the price {price}")
return price * (1 - self.value)
class FlatCoupon(Coupon):
def applyDiscount(self, price: float) -> float:
print(f"Applying Flat discount of {self.value}")
if price > self.value:
return price - self.value
return 0.0
class NoDiscountCoupon(Coupon):
def applyDiscount(self, price: float) -> float:
print("No Discount Applied")
return price
class CouponFactory:
def getCoupon(self, code: str, type: str, value: float) -> Coupon:
if type == "PERCENT":
return PercentageCoupon(code=code, value=value)
elif type == "FLAT":
return FlatCoupon(code=code, value=value)
return NoDiscountCoupon(code="INVALID", value=0.0)
class DiscountEnginer:
def __init__(self) -> None:
self.couponFactory : CouponFactory = CouponFactory()
def generateCoupons(self, raw_coupon_data: list[dict]) -> list[Coupon]:
coupons: list[Coupon] = []
for data in raw_coupon_data:
coupons.append(self.couponFactory.getCoupon(code=data.get("code", ""), type=data.get("type", ""), value=data.get("value", 0.0)))
return coupons
# Simulate database records
raw_coupon_data = [
{"code": "SAVE10", "type": "PERCENT", "value": 0.10},
{"code": "CASH20", "type": "FLAT", "value": 20.0},
{"code": "SAVER2", "type": "APPROX", "value": 4.5},
]
engine = DiscountEnginer()
coupons = engine.generateCoupons(raw_coupon_data=raw_coupon_data)
for coupon in coupons:
print(coupon.applyDiscount(price=60))
βοΈ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Decoupling: Engine doesn't know about concrete classes. | Boilerplate: Can be overkill for simple systems. |
| Extensibility: Easy to add new types. | Complexity: Adds an extra layer of abstraction. |
| Safety: Centralized error handling for bad inputs. | Testing: Might need to mock the factory in some tests. |
π§ͺ Testing Strategy
- Unit Test Factory: Pass in various JSON strings and verify the correct class is instantiated.
- Test Null Case: Verify that an unknown type returns
NoDiscount. - Mock Test Engine: Verify that the engine correctly uses the object returned by the factory without knowing its type.
π€ Interview Toolkit
- Interview Signal: mastery of creational decoupling and interface-driven design.
- When to Use:
- "Don't know the exact types of objects your code will work with..."
- "Consolidate complex creation logic in one place..."
- "Allow users of your library to extend its components..."
- Scalability Probe: "How to handle 1,000 types?" (Answer: Use a Registryβa dictionary mapping type strings to classesβinstead of a long
if/elifinside the factory.) - Design Alternatives:
- Abstract Factory: If you're creating families of related objects.
- Builder: If the object construction involves many steps/parts.
π Related Patterns
- Abstract Factory β Factory Method is for one object; Abstract Factory is for a family.
- Null Object β Factories often return a Null Object as a safe default.
- Prototype β A factory can use a "Registry of Prototypes" to clone objects instead of instantiating them.