Creational Pattern Series | Factory Method
In this series we will take a look at the creational design patterns:
- Singleton
- Factory Method
- Abstract Factory
- Prototype
- Builder
🤓 What is it?
The Factory Method is a design pattern that provides an interface for producing objects. It keeps the client code from having to know how to make objects. This lets the client code work with objects without having to know what kind they are.
TL;DR
It provides an interface for creating objects in a superclass but allows the subclasses to alter the type of objects that will be created.
🫡 The why…
Assume you’ve already created a game with simply one sort of adversary (the mighty dragon). Because the game was a huge success, we decided to add more enemy types to it. That’s fantastic, isn’t it? What about the coding, though? At the moment, our code is tied to a single enemy type in MightyDragon; adding a vampire or a zombie will necessitate changes to the entire software. As a result, we’ll wind up having a lot of conditionals and switches in our code.
Fortunately, we have the Factory pattern, which advises that we replace object building with a custom factory function; this factory will return “products” (the enemies). You may assume that this modification is unnecessary, but note that we can now override the factory method in subclasses and change the class of products made by the method.
A uniform interface is also required to ensure that the factory method always returns the same product.
interface Enemey {
// ...
attack(): void;
}
class Mobs {
createEnemy(): Enemy {
// ...
}
}
class AirMobs extends Mobs {
// ..
createEnemy(): Enemy {
// ...
return new MightyDragon();
}
}
class SandMobs extends Mobs {
// ..
createEnemy(): Enemy {
// ...
return new Zombie();
}
}
class MightyDragon implements Enemy {
// ...
attack(): void {
// ...
}
}
class Zombie implements Enemy {
// ...
attack(): void {
// ...
}
}
For example, both MightyDragon and Zombie classes should implement the Enemy interface, which declares a method called attack. Each class implements this method differently: Zombie attack by hand, MightyDragon throw fire at you. The factory method in the SandMobs class returns Zombie, Goblins …etc objects, whereas the factory method in the AirMobs class returns MightyDragon, Eagles ..etc.
The code that calls the factory method (commonly referred to as the client code) makes no distinction between the actual goods returned by various subclasses. The client regards all products as an abstract Adversary.
The client is aware that all enemy objects are meant to have the attack technique, but the client is unconcerned with how it works.
💻 The structure
🏋🏻 How it works
The Factory Method pattern is made up of two parts: a base interface or abstract class for creating objects and concrete classes that implement that interface. Concrete classes are in charge of constructing specific objects. The concrete classes are then used to generate objects based on the input received by a factory class.
🥲 Let’s take another example:
Let’s say we are building an e-commerce website and we need to create different types of products (e.g. books, electronics, clothing). Each type of product has its own set of properties and creation logic. We can use the Factory Method pattern to abstract away the creation of these products and provide a centralized place for their creation.
interface Product {
name: string;
price: number;
description: string;
}
interface ProductCreator {
createProduct(): Product;
}
class Book implements Product {
name: string;
price: number;
description: string;
constructor(name: string, price: number, description: string) {
this.name = name;
this.price = price;
this.description = description;
}
}
class BookCreator implements ProductCreator {
createProduct(): Product {
const name = 'Book';
const price = Math.floor(Math.random() * 50) + 10;
const description = 'This is a book.';
return new Book(name, price, description);
}
}
class Electronic implements Product {
name: string;
price: number;
description: string;
constructor(name: string, price: number, description: string) {
this.name = name;
this.price = price;
this.description = description;
}
}
class ElectronicCreator implements ProductCreator {
createProduct(): Product {
const name = 'Electronic';
const price = Math.floor(Math.random() * 200) + 100;
const description = 'This is an electronic.';
return new Electronic(name, price, description);
}
}
class ProductFactory {
private creators: Map<string, ProductCreator> = new Map();
registerCreator(type: string, creator: ProductCreator) {
this.creators.set(type, creator);
}
createProduct(type: string): Product {
const creator = this.creators.get(type);
if (!creator) {
throw new Error(`Product type ${type} not found.`);
}
return creator.createProduct();
}
}
const factory = new ProductFactory();
factory.registerCreator('book', new BookCreator());
factory.registerCreator('electronic', new ElectronicCreator());
const product1 = factory.createProduct('book');
const product2 = factory.createProduct('electronic');
console.log(product1);
// {name: 'Book', price: 23, description: 'This is a book.'}
console.log(product2);
// {name: 'Electronic', price: 273, description: 'This is an electronic.'}
In this example, we have a ProductCreator
interface and concrete classes that implement this interface for each type of product (i.e. BookCreator
and ElectronicCreator
). We also have a ProductFactory
class that creates products based on the input it receives. We register the creators for each type of product with the factory and then use the factory to create the products.
✅ Benefits of using the Factory Method pattern
- Decouples the client code from the creation of objects.
- Allows for easy addition of new types of objects without changing existing code.
- Provides a centralized place for object creation.
- With the Factory Method pattern, you can create objects based on different criteria, such as user input, configuration settings, or runtime conditions.
- It make testing easier by allowing you to substitute mock objects for the actual objects created by the factory.
❌ Drawbacks of using the Factory Method pattern
- The Factory Method pattern can add extra complexity to your code, especially if you need to create many different types of objects. This can make the code harder to understand and maintain, especially if you’re working with a large codebase.
- The Factory Method pattern requires you to create a separate factory class or method, which can add overhead to your code. This can affect performance, especially if you need to create many objects in a short amount of time.
- The Factory Method pattern can result in a larger code volume, especially if you need to create many subclasses. This can make the codebase more difficult to navigate and understand, and can also increase the risk of bugs and errors.
- The Factory Method pattern can be inflexible if you need to create objects that have complex dependencies or configurations. In these cases, you may need to use other patterns, such as the Abstract Factory pattern, to create objects with more complex structures.
- The Factory Method pattern can create tighter coupling between the factory and the objects it creates. This can make it harder to change the factory or the objects without affecting the other, which can make the codebase less adaptable over time.
🤔 When to use the Factory Method pattern
The Factory Method pattern is useful when client code wants to work with objects of diverse types but does not know their precise types. It’s also handy when object creation is complicated and needs to be abstracted away from client code.
When you want to give your library or framework’s users the ability to extend its internal components, use the Factory Method.
🍔 Conclusion
The Factory Method pattern is a strong pattern that allows you to abstract object generation from client code. It makes it simple to create new types of objects and provides a single location for object creation. It is useful when the client code needs to operate with objects of diverse kinds without knowing their precise types, or when object generation is difficult and needs to be abstracted away from the client code.
You can find more explanations and code examples in my repository:
Hope you enjoyed the article ❤️