Marcell Ciszek Druzynski

Factory Method Design Pattern

Learn about the Factory Method design pattern, its advantages, and practical examples.

February 16, 2024

The Factory Method is a design pattern used for creating objects. It employs an interface within a superclass to instantiate other objects, permitting subclasses to specify the type of objects to be created. In essence, it establishes an interface for object creation while delegating the implementation details to subclasses. This pattern encourages loose coupling by prioritizing dependency on abstractions such as interfaces or abstract classes over concrete implementations. Rather than directly creating objects with the new keyword, the responsibility is delegated to the factory method.

Understanding the Factory Method Design Pattern

The Factory Method design pattern is a creational approach that facilitates object creation within a superclass while empowering subclasses to modify the types of objects generated. Put simply, it delegates the task of object instantiation to subclasses.

This pattern operates on the principle of creating objects through a unified interface, shielding the client code from direct exposure to concrete classes. This abstraction enhances modularity and simplifies maintenance. If you're familiar with Clean Code, you've likely encountered the open/closed principle, and the factory method pattern aligns with that principle.

The Factory Method pattern comprises two key elements: the abstract factory class, defining the object creation interface, and the concrete factory classes, implementing creation logic for specific object types. Client code interacts with the abstract factory class, oblivious to the specific implementation details of concrete factories.

Through the Factory Method pattern, developers can generate related objects without explicitly specifying their types. This flexibility enables runtime determination of object types based on varying conditions or requirements.

Advantages

The Factory Method design pattern brings several benefits that can make your code easier to manage and maintain. Let's take a look at some of the main advantages:

  1. Loose Connections: With the Factory Method pattern, your code becomes less tied to specific classes. Instead of directly making objects with new, your code talks to a common factory interface. This means if you need to change or update classes later, you can do it without messing up the rest of your code. It keeps things tidy and modular.
  2. Easy Expansion: By using separate factory classes for creating objects, you can easily add new features without messing with existing code. As long as the new classes follow the rules set by the factory interface, you can plug them in without causing a fuss. For example, in a pizza-making scenario, adding a new type of pizza just means adding a new class and making a small tweak to the existing code.
  3. Stay Abstract: The Factory Method pattern encourages you to think in terms of interfaces rather than specific implementations. By sticking to the factory interface, your code becomes more flexible and easier to update in the future. It's like building with LEGO bricks – you can swap pieces in and out without breaking the whole structure.

Practical Examples

The Factory Method pattern comes in handy when you're dealing with situations where you need to create different kinds of things based on different situations.

  • Making Your Menu Bigger: Picture a restaurant menu. If you want to add a new dish without messing up the kitchen's workflow, the Factory Method can be helpful. Instead of rewiring the whole setup, you just add a new dish by creating a new class and tweaking the factory method inside it.
  • User's Choice, Your Creation: Imagine an app where users can order pizzas. With the Factory Method, you can organize the pizza-making process into separate factory classes. This way, when a user places an order, the app knows just how to whip up the right kind of pizza without needing to know all the nitty-gritty details. We are hiding the complexity from the client code.

Pizza Ordering Made Easy

Imagine we're running a pizza joint where customers can order all sorts of pizzas—Hawaiian, Margherita, you name it. Now, let's see how the factory method makes it a breeze to set up our pizza-making factory:

1// Define an interface for the Pizza
2interface Pizza {
3 prepare(): void;
4 bake(): void;
5 cut(): void;
6 box(): void;
7}
8
9// Concrete implementation of Pizza
10class MargheritaPizza implements Pizza {
11 prepare(): void {
12 console.log("Preparing Margherita Pizza...");
13 }
14
15 bake(): void {
16 console.log("Baking Margherita Pizza...");
17 }
18
19 cut(): void {
20 console.log("Cutting Margherita Pizza...");
21 }
22
23 box(): void {
24 console.log("Boxing Margherita Pizza...");
25 }
26}
27
28// Concrete implementation of Pizza
29class PepperoniPizza implements Pizza {
30 prepare(): void {
31 console.log("Preparing Pepperoni Pizza...");
32 }
33
34 bake(): void {
35 console.log("Baking Pepperoni Pizza...");
36 }
37
38 cut(): void {
39 console.log("Cutting Pepperoni Pizza...");
40 }
41
42 box(): void {
43 console.log("Boxing Pepperoni Pizza...");
44 }
45}
46
47// Define the Pizza Factory interface
48interface PizzaFactory {
49 createPizza(): Pizza;
50}
51
52// Concrete implementation of Pizza Factory for Margherita Pizza
53class MargheritaPizzaFactory implements PizzaFactory {
54 createPizza(): Pizza {
55 return new MargheritaPizza();
56 }
57}
58
59// Concrete implementation of Pizza Factory for Pepperoni Pizza
60class PepperoniPizzaFactory implements PizzaFactory {
61 createPizza(): Pizza {
62 return new PepperoniPizza();
63 }
64}
65
66// Client code
67function orderPizza(factory: PizzaFactory): void {
68 const pizza = factory.createPizza();
69 pizza.prepare();
70 pizza.bake();
71 pizza.cut();
72 pizza.box();
73}
74
75// Usage
76console.log("Ordering Margherita Pizza:");
77orderPizza(new MargheritaPizzaFactory()); // Output: Preparing Margherita Pizza... Baking Margherita Pizza... Cutting Margherita Pizza... Boxing Margherita Pizza...
78
79console.log("\nOrdering Pepperoni Pizza:");
80orderPizza(new PepperoniPizzaFactory()); // Output: Preparing Pepperoni Pizza... Baking Pepperoni Pizza... Cutting Pepperoni Pizza... Boxing Pepperoni Pizza...
1// Define an interface for the Pizza
2interface Pizza {
3 prepare(): void;
4 bake(): void;
5 cut(): void;
6 box(): void;
7}
8
9// Concrete implementation of Pizza
10class MargheritaPizza implements Pizza {
11 prepare(): void {
12 console.log("Preparing Margherita Pizza...");
13 }
14
15 bake(): void {
16 console.log("Baking Margherita Pizza...");
17 }
18
19 cut(): void {
20 console.log("Cutting Margherita Pizza...");
21 }
22
23 box(): void {
24 console.log("Boxing Margherita Pizza...");
25 }
26}
27
28// Concrete implementation of Pizza
29class PepperoniPizza implements Pizza {
30 prepare(): void {
31 console.log("Preparing Pepperoni Pizza...");
32 }
33
34 bake(): void {
35 console.log("Baking Pepperoni Pizza...");
36 }
37
38 cut(): void {
39 console.log("Cutting Pepperoni Pizza...");
40 }
41
42 box(): void {
43 console.log("Boxing Pepperoni Pizza...");
44 }
45}
46
47// Define the Pizza Factory interface
48interface PizzaFactory {
49 createPizza(): Pizza;
50}
51
52// Concrete implementation of Pizza Factory for Margherita Pizza
53class MargheritaPizzaFactory implements PizzaFactory {
54 createPizza(): Pizza {
55 return new MargheritaPizza();
56 }
57}
58
59// Concrete implementation of Pizza Factory for Pepperoni Pizza
60class PepperoniPizzaFactory implements PizzaFactory {
61 createPizza(): Pizza {
62 return new PepperoniPizza();
63 }
64}
65
66// Client code
67function orderPizza(factory: PizzaFactory): void {
68 const pizza = factory.createPizza();
69 pizza.prepare();
70 pizza.bake();
71 pizza.cut();
72 pizza.box();
73}
74
75// Usage
76console.log("Ordering Margherita Pizza:");
77orderPizza(new MargheritaPizzaFactory()); // Output: Preparing Margherita Pizza... Baking Margherita Pizza... Cutting Margherita Pizza... Boxing Margherita Pizza...
78
79console.log("\nOrdering Pepperoni Pizza:");
80orderPizza(new PepperoniPizzaFactory()); // Output: Preparing Pepperoni Pizza... Baking Pepperoni Pizza... Cutting Pepperoni Pizza... Boxing Pepperoni Pizza...

In this example:

  • We set up an interface called Pizza to describe what a pizza is and what it can do.
  • We create two kinds of pizzas: MargheritaPizza and PepperoniPizza, both following the rules of the Pizza interface.
  • We establish a PizzaFactory interface with a function called createPizza that gives us back a pizza.
  • We make two concrete pizza factories: MargheritaPizzaFactory and PepperoniPizzaFactory, each focused on making its own kind of pizza.
  • Lastly, our customer code uses these factory objects to order the pizzas they want.

Best Practices

  1. Keep it Simple: Each concrete factory should do one thing well—create one type of object. It's best to avoid factories that try to handle multiple types of objects.
  2. Use Composition: Instead of relying solely on inheritance, consider using composition. This gives you more flexibility and makes it easier to mix and match different factory classes to create new types of objects.
  3. Choose Clear Names: Naming things well is crucial in programming. Aim for names that clearly describe what your variables, functions, and classes do. This makes your code easier to understand and more friendly to other developers.
  4. Separate Creation from Initialization: Don't overload your factory classes with complex initialization tasks. Instead, let the created objects handle their own setup, or use separate initialization classes. This keeps your code organized and easier to maintain.

Other Design Patterns to Explore

While the Factory Method pattern is handy for making objects, there are other patterns that can work just as well in similar situations. Here are a few to check out:

Helpful Resources