diff --git a/docs/creational/factory-method.mdx b/docs/creational/factory-method.mdx index 1683f87..19686fa 100644 --- a/docs/creational/factory-method.mdx +++ b/docs/creational/factory-method.mdx @@ -1,5 +1,449 @@ --- title: Factory Method +hide_title: true --- -This is Factory Method Pattern page +import { CreationalPatternTag, BehavioralPatternTag, StructuralPatternTag } from "@site/src/components/pattern-page-features/pattern-alias-and-tag"; +import TwoColumnProsCons from "@site/src/components/pattern-page-features/pros-and-cons"; +import TOCInline from '@theme/TOCInline'; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import Applicability from "@site/src/components/pattern-page-features/applicability"; + + + +--- + +In [object-oriented programming (OOP)](https://en.wikipedia.org/wiki/Object-oriented_programming), the **factory pattern** is a pattern that guides in the creation of objects without specifying their exact classes. Rather than calling a constructor directly you can invoke a factory method to create an object. This allows for more flexibility and decoupling in the code, as the specific class of the object being created can be determined at runtime based on certain conditions or configurations. + +## 🧩The problem + +Let's say you have a website that sells toys. When you create your company you think to yourself "I sure love selling toys and will only ever sell large, bulky toys", therefore you ship these in boxes. After a while though it becomes clear that kids really want small-form toys as well. You would love to save some costs on shipping by sending them in envelopes instead of boxes. However because you initially tightly coupled your shipping system to shipping in boxes that it is now quite hard to change this in the present. Sure you can add a case that'll check whether it needs to be send using a box or an envelope, but it'll create quite messy and clunky code. Moreover what'll you do when you decide to add insured boxes or local pick-up, instead of delivering? + +## 🛠️Solutions + +The factory method pattern suggests that you create a separate method (the factory method) that is responsible for creating the objects you need. This method can then be overridden in subclasses to create different types of objects based on the specific requirements. This way, it'll become much easier to add new types of shipping methods in the future without having to modify the existing codebase. Instead you can just create a new subclass that implements the factory method to create the new shipping method. As long as all shipping methods adhere to the same interface, the rest of your codebase can remain unchanged, whilst still allowing for additional flexibility and extensibility. + +### 🏛️Metaphors + +Imagine a toy factory that produces different types of toys. Instead of having a single assembly line that creates all toys, the factory has multiple specialized assembly lines for different toy types (e.g., action figures, dolls, puzzles). Each assembly line is responsible for creating its specific type of toy. When a customer places an order, the factory uses the appropriate assembly line to produce the requested toy type. This way, the factory can easily add new toy types in the future by simply introducing new assembly lines without disrupting the existing ones. + +### 💡Real-world examples +Common practical scenarios for applying the Factory Method pattern include: + +- A logistics application that needs to create different types of transport vehicles (e.g., trucks, ships, planes) based on the cargo being shipped. Here all vehicles adhere to a common 'vehicle' (interface) type. +- A document generation system that produces different types of documents (e.g., invoices, reports, letters) based on user input. They still all rely on one 'type' (interface) of document. + +## ⚖️ Pros and Cons + + + + +## 🔍Applicability + + + + +## 🧭Implementation Plan + +To implement a Factory Method manually: + +1. Define a common interface, these should declare methods that all created objects must implement. E.g. `ShippingObject` with `shipProduct()` and `package()` methods, so that all shipping methods adhere to the same interface. +2. Add an empty factory method to a base class, e.g. `createShippingMethod()`. The return type should match the common interface defined in step 1. +3. Create the concrete creator (sub-) classes, e.g. `BoxShipping` and `EnvelopeShipping`, that override the factory method to instantiate and return specific types of objects that implement the common interface. +4. Client code should call the factory method to create objects, instead of directly instantiating them. This way the client code remains decoupled from the specific classes being created. + + +### 💻Code samples + + + + +```ts +// Product interface - defines what all shipping methods must do +interface ShippingMethod { + calculateCost(weight: number): number; + package(item: string): string; + deliver(): string; +} + +// Concrete Products - different shipping implementations +class BoxShipping implements ShippingMethod { + calculateCost(weight: number): number { + return weight * 2.5; // $2.50 per kg for box shipping + } + + package(item: string): string { + return `${item} securely packed in a protective box with bubble wrap`; + } + + deliver(): string { + return "Delivered via ground transport (3-5 business days)"; + } +} + +// Concrete Products - different shipping implementations +class EnvelopeShipping implements ShippingMethod { + calculateCost(weight: number): number { + return Math.max(5, weight * 1.2); kg + } + + package(item: string): string { + return `${item} placed in a padded envelope`; + } + + deliver(): string { + return "Delivered via express mail (1-2 business days)"; + } +} + +// Concrete Products - different shipping implementations +class InsuredShipping implements ShippingMethod { + calculateCost(weight: number): number { + return weight * 3.5 + 15; + } + + package(item: string): string { + return `${item} packed in reinforced box with tracking and insurance`; + } + + deliver(): string { + return "Delivered with signature required and full insurance coverage"; + } +} + +// Creator - defines the factory method +abstract class ShippingService { + // Factory method - subclasses decide which shipping method to create + abstract createShippingMethod(): ShippingMethod; + + // Template method - uses the factory method + processOrder(item: string, weight: number): void { + const shipping = this.createShippingMethod(); + + console.log(`📦 ${shipping.package(item)}`); + console.log(`💰 Cost: $${shipping.calculateCost(weight).toFixed(2)}`); + console.log(`🚚 ${shipping.deliver()}`); + } +} + +// Concrete Creators - implement the factory method +class StandardShippingService extends ShippingService { + createShippingMethod(): ShippingMethod { + return new BoxShipping(); + } +} + +class ExpressShippingService extends ShippingService { + createShippingMethod(): ShippingMethod { + return new EnvelopeShipping(); + } +} + +class PremiumShippingService extends ShippingService { + createShippingMethod(): ShippingMethod { + return new InsuredShipping(); + } +} + +// Client code - works with any shipping service without knowing the details +function processCustomerOrder(service: ShippingService, item: string, weight: number): void { + service.processOrder(item, weight); +} + +// Usage - easily switch between different shipping strategies +const toyOrder = "Limited Edition Action Figure"; +const weight = 0.5; // 0.5 kg + +processCustomerOrder(new StandardShippingService(), toyOrder, weight); +processCustomerOrder(new ExpressShippingService(), toyOrder, weight); +processCustomerOrder(new PremiumShippingService(), toyOrder, weight); + +``` + + + + +```python +from abc import ABC, abstractmethod + +# Product interface - defines what all shipping methods must do +class ShippingMethod(ABC): + @abstractmethod + def calculate_cost(self, weight: float) -> float: + pass + + @abstractmethod + def package(self, item: str) -> str: + pass + + @abstractmethod + def deliver(self) -> str: + pass + +# Concrete Products - different shipping implementations +class BoxShipping(ShippingMethod): + def calculate_cost(self, weight: float) -> float: + return weight * 2.5 # $2.50 per kg for box shipping + + def package(self, item: str) -> str: + return f"{item} securely packed in a protective box with bubble wrap" + + def deliver(self) -> str: + return "Delivered via ground transport (3-5 business days)" + +class EnvelopeShipping(ShippingMethod): + def calculate_cost(self, weight: float) -> float: + return max(5, weight * 1.2) # Minimum $5 or $1.20 per kg + + def package(self, item: str) -> str: + return f"{item} placed in a padded envelope" + + def deliver(self) -> str: + return "Delivered via express mail (1-2 business days)" + +class InsuredShipping(ShippingMethod): + def calculate_cost(self, weight: float) -> float: + return weight * 3.5 + 15 # Higher cost + insurance fee + + def package(self, item: str) -> str: + return f"{item} packed in reinforced box with tracking and insurance" + + def deliver(self) -> str: + return "Delivered with signature required and full insurance coverage" + +# Creator - defines the factory method +class ShippingService(ABC): + # Factory method - subclasses decide which shipping method to create + @abstractmethod + def create_shipping_method(self) -> ShippingMethod: + pass + + # Template method - uses the factory method + def process_order(self, item: str, weight: float) -> None: + shipping = self.create_shipping_method() + + print(f"📦 {shipping.package(item)}") + print(f"💰 Cost: ${shipping.calculate_cost(weight):.2f}") + print(f"🚚 {shipping.deliver()}") + +# Concrete Creators - implement the factory method +class StandardShippingService(ShippingService): + def create_shipping_method(self) -> ShippingMethod: + return BoxShipping() + +class ExpressShippingService(ShippingService): + def create_shipping_method(self) -> ShippingMethod: + return EnvelopeShipping() + +class PremiumShippingService(ShippingService): + def create_shipping_method(self) -> ShippingMethod: + return InsuredShipping() + +# Client code - works with any shipping service without knowing the details +def process_customer_order(service: ShippingService, item: str, weight: float) -> None: + service.process_order(item, weight) + +# Usage - easily switch between different shipping strategies +if __name__ == "__main__": + toy_order = "Limited Edition Action Figure" + weight = 0.5 # 0.5 kg + + process_customer_order(StandardShippingService(), toy_order, weight) + process_customer_order(ExpressShippingService(), toy_order, weight) + process_customer_order(PremiumShippingService(), toy_order, weight) +``` + + + +### 🎮Playground + +:::note + +This sample is to get a 'feel' for the pattern. The code itself may not reflect a correct implementation of the pattern. + +::: + +```jsx live +function FactoryMethodDemo() { + // State for the order form and shipping log + const [item, setItem] = React.useState("Action Figure"); + const [weight, setWeight] = React.useState(0.5); + const [shippingType, setShippingType] = React.useState("standard"); + const [orders, setOrders] = React.useState([]); + + // Factory functions - each creates a different shipping method + const createStandardShipping = () => ({ + type: "Standard Box", + calculateCost: (w) => w * 2.5, + deliveryTime: "3-5 business days", + icon: "📦" + }); + + const createExpressShipping = () => ({ + type: "Express Envelope", + calculateCost: (w) => Math.max(5, w * 1.2), + deliveryTime: "1-2 business days", + icon: "✉️" + }); + + const createPremiumShipping = () => ({ + type: "Premium Insured", + calculateCost: (w) => w * 3.5 + 15, + deliveryTime: "Next day + signature", + icon: "🎁" + }); + + // Factory Method - returns the appropriate shipping method + const getShippingMethod = (type) => { + switch(type) { + case "standard": return createStandardShipping(); + case "express": return createExpressShipping(); + case "premium": return createPremiumShipping(); + default: return createStandardShipping(); + } + }; + + // Process order using the factory method + const handleProcessOrder = () => { + const shipping = getShippingMethod(shippingType); + const cost = shipping.calculateCost(weight); + + const order = { + id: Date.now(), + item, + weight, + shipping: shipping.type, + cost: cost.toFixed(2), + delivery: shipping.deliveryTime, + icon: shipping.icon + }; + + setOrders([order, ...orders]); + }; + + const handleClearOrders = () => setOrders([]); + + return ( +
+

Factory Method — Shipping Demo

+ + {/* Order Form */} +
+
+ +
+ +
+ +
+ +
+ +
+ + + +
+ + {/* Orders Display */} +
+

Processed Orders:

+ {orders.length === 0 ? ( +

No orders yet. Create one above!

+ ) : ( +
    + {orders.map((order) => ( +
  • +
    + {order.icon} {order.item} ({order.weight} kg) +
    +
    + Method: {order.shipping}
    + Cost: ${order.cost}
    + Delivery: {order.delivery} +
    +
  • + ))} +
+ )} +
+ +
+ 💡 Factory Method in Action: Each shipping type is created by a + different factory function. The getShippingMethod() decides which factory + to use, keeping the order processing code decoupled from specific shipping implementations. +
+
+ ); +} +``` + +## 🔗Relations to other patterns +- Lots of people start with the [Factory Method](/docs/creational/factory-method) pattern prior to implementing something such as [Abstract Factory](/docs/creational/abstract-factory), [Builder](/docs/creational/builder) or [Prototype](/docs/creational/prototype) patterns. The latter patterns are more flexible, but also come with increased complexity. +- [Prototype](/docs/creational/prototype) isn't based on inheritance, so it does not have its drawbacks. On the other hand, [Prototype](/docs/creational/prototype) requires a complicated initialization of the cloned object. [Factory Method](/docs/creational/factory-method) is based on inheritance but doesn't require an initialization step. +- [Factory Method](/docs/creational/factory-method) is a specialization of [Template Method](/docs/behavioral/template-method). At the same time, a Factory Method may serve as a step in a large Template Method. + +--- + +## 📚Sources +Information used in this page was collected from various reliable sources: + +- [Refactoring Guru - Factory Method](https://refactoring.guru/design-patterns/factory-method) +- [Wikipedia - Factory Method Pattern](https://en.wikipedia.org/wiki/Factory_method_pattern) +- [SourceMaking - Factory Method](https://sourcemaking.com/design_patterns/factory_method)