Mastering the Factory Pattern with Practical Examples

Nikolay Nikolov
6 min readOct 22, 2023

--

What is it?

The Factory Pattern is a creational design pattern widely utilized in software development to encapsulate the process of creating objects. It introduces an interface for object creation within a superclass while allowing subclasses to modify the types of objects to be created. This enhances code flexibility, maintainability, and scalability.

Where is it used

- Object Creation Decoupling: It is particularly useful when a system needs to be independent of its object creation, allowing for flexibility in choosing object types.

- Dynamic Object Creation: Ideal for scenarios where object creation involves complex configurations or when different variations of objects need to be created.

Advantages

- Flexibility: Enables easy switching between different implementations or types of objects without altering the client code.

- Maintainability: Centralizes object creation logic, simplifying management and updates.

- Scalability: Supports the addition of new object types or variations without modifying existing code.

- Decoupling: Reduces dependencies between client code and concrete classes, promoting a modular and adaptable design.

Example

Creating Office Furniture:

First, let’s consider an example of poorly constructed code:

<?php

class Furniture
{
public function __construct(
private string $type,
private string $material
){}

public function assemble(): string
{
return sprintf("Assemble %s, Material: %s",
$this->type,
$this->material
);
}
}

class FurnitureCreationClient
{
private Furniture $furniture;

public function __construct(
private string $type,
private string $material
){
$this->furniture = new Furniture($type, $material);
}

public function assembleFurniture(): void
{
echo $this->furniture->assemble() . PHP_EOL;
}
}

// Client code
$clientForDesk = new FurnitureCreationClient('Desk', 'Wood');
$clientForChair = new FurnitureCreationClient('Chair', 'Metal');
$clientForBookshelf = new FurnitureCreationClient('Bookshelf', 'Glass');

// Output: Assemble: Desk, Material: Wood
$clientForDesk->assembleFurniture();

// Output: Assemble: Chair, Material: Metal
$clientForChair->assembleFurniture();

// Output: Assemble: Bookshelf, Material: Glass
$clientForBookshelf->assembleFurniture();

The poorly created code has a few disadvantages:

1. Lack of Separation of Concerns:
— The original code has the responsibility of creating and assembling furniture directly embedded in the FurnitureCreationClient class. This violates the principle of separation of concerns, where each class or module should have a distinct and focused responsibility. In a well-designed system, the creation of objects should be decoupled from the client code.

2. Limited Flexibility and Extensibility:
— The original code lacks flexibility in terms of introducing new types of furniture or modifying the creation process. Any changes or additions to the types of furniture require modifications directly in the client code. A more flexible design, such as the use of a factory pattern, allows for easier adaptation and extension of the system.

3. High Coupling between Client and Concrete Classes:
— The client code (FurnitureCreationClient) is tightly coupled to concrete implementations (e.g., Furniture class). This high level of coupling makes the code less adaptable to changes. If a new type of furniture is added or the implementation of Furniture is modified, it directly affects the client code.

4. Code Duplication:
— The code for creating and assembling furniture is duplicated in each instance of the FurnitureCreationClient class. This violates the DRY (Don’t Repeat Yourself) principle, which suggests that code duplication should be minimized. Duplicating code increases the chances of introducing errors and makes maintenance more challenging.

5. Limited Testability:
— The original code may be less testable due to the lack of abstraction and dependency injection. Testing becomes challenging when concrete implementations are directly instantiated within the client code. A more modular and testable design would involve dependency injection or using interfaces/abstractions.

6. Difficulties in Scaling:
— As the codebase grows and evolves, the lack of a modular and extensible design can hinder scalability. Adding new features or accommodating changes in requirements may become increasingly complex and error-prone.

7. Violation of SOLID Principles:
— The original code violates some of the SOLID principles, such as the Dependency Inversion Principle (DIP). The FurnitureCreationClient directly creates an instance of the Furniture class using the new Furniture(...) syntax. This creates a tight coupling between the high-level module (FurnitureCreationClient) and the low-level module (the concrete Furniture class). In a well-designed system, high-level modules should not depend on low-level modules; both should depend on abstractions.

Addressing these disadvantages through the adoption of design patterns like the Factory Pattern can lead to a more maintainable, scalable, and adaptable codebase.

Here’s how you might refactor the code using a Factory Pattern:

<?php

interface FurnitureInterface
{
public function assemble(): string;
}

class Desk implements FurnitureInterface
{
public function __construct(private string $material) {}

public function assemble(): string
{
return sprintf("Assemble Desk, Material: %s", $this->material);
}
}

class Chair implements FurnitureInterface
{
public function __construct(private string $material) {}

public function assemble(): string
{
return sprintf("Assemble Chair, Material: %s", $this->material);
}
}

class Bookshelf implements FurnitureInterface
{
public function __construct(private string $material) {}

public function assemble(): string
{
return sprintf("Assemble Bookshelf, Material: %s", $this->material);
}
}

interface FurnitureFactoryInterface
{
public function createFurniture(string $material): FurnitureInterface;
}

class DeskFactory implements FurnitureFactoryInterface
{
public function createFurniture(string $material): FurnitureInterface
{
return new Desk($material);
}
}

class ChairFactory implements FurnitureFactoryInterface
{
public function createFurniture(string $material): FurnitureInterface
{
return new Chair($material);
}
}

class BookshelfFactory implements FurnitureFactoryInterface
{
public function createFurniture(string $material): FurnitureInterface
{
return new Bookshelf($material);
}
}

class FurnitureCreationClient
{
public function __construct(private FurnitureInterface $furniture) {}

public function assembleFurniture(): void
{
echo $this->furniture->assemble() . PHP_EOL;
}
}

// Client code using the Factory Pattern
$deskFactory = new DeskFactory();
$chairFactory = new ChairFactory();
$bookshelfFactory = new BookshelfFactory();

// Create and assemble Desk
$desk = $deskFactory->createFurniture('Wood');
$clientForDesk = new FurnitureCreationClient($desk);
$clientForDesk->assembleFurniture();

// Create and assemble Chair
$chair = $chairFactory->createFurniture('Metal');
$clientForChair = new FurnitureCreationClient($chair);
$clientForChair->assembleFurniture();

// Create and assemble Bookshelf
$bookshelf = $bookshelfFactory->createFurniture('Glass');
$clientForBookshelf = new FurnitureCreationClient($bookshelf);
$clientForBookshelf->assembleFurniture();

The refactored code offers several advantages:

1. Enhanced Modularity:
— The code is more modular with dedicated factories (DeskFactory, ChairFactory, and BookshelfFactory) for each type of furniture. This modular structure improves code organization and makes it easier to manage and extend.

2. Improved Readability:
— The code is more readable as each factory is responsible for creating a specific type of furniture. This separation of concerns makes it clear which factory corresponds to which product, enhancing overall code comprehension.

3. Easy to Extend:
— Adding a new type of furniture is straightforward. You can create a new class for the furniture, implement the FurnitureInterface , and create a corresponding factory. This makes the system highly extensible without modifying existing code.

4. Better Adherence to SOLID Principles:
— The refactored code aligns more closely with SOLID principles, particularly the Single Responsibility Principle (SRP) and the Open/Closed Principle (OCP). Each class has a specific responsibility, and the system is open for extension but closed for modification.

5. Reduced Code Duplication:
— The creation and assembly logic is centralized within each factory, reducing code duplication. If there are changes to the creation process, modifications are localized to the relevant factory, adhering to the DRY (Don’t Repeat Yourself) principle.

6. Flexibility in Material Handling:
— The factories accept the material as a parameter during object creation. This flexibility allows for variations in material without modifying the client code, making the system adaptable to different scenarios.

7. Easier Testing:
— With the modular structure and dependency injection, testing becomes more straightforward. It’s easier to mock or substitute dependencies during testing, ensuring better testability of individual components.

8. Clear Separation of Concerns:
— The client code (FurnitureCreationClient) remains focused on its responsibility of creating and assembling furniture without being concerned with the details of object creation. This adheres to the Separation of Concerns principle.

9. Scalability:
— As the system evolves, the modular design facilitates scalability. Adding new types of furniture or modifying existing ones can be done independently, reducing the impact on the overall system.

In summary, the advantages lie in improved maintainability, readability, and flexibility, with a design that aligns with best practices and software design principles.

For further exploration, you can also delve into my other articles, such as “Mastering the Builder Design Pattern with Practical Examples”.

Feel free to follow me ➡️ or Subscribe 🔔 for more insightful content.
Clap 👏🏻, drop a comment 💬, and share this article with anyone you think would find it valuable.

Your interaction means the world to me and I’m incredibly grateful.
Your ongoing support gives me the power to write more valuable articles. ❤️ Thank you for being here and for reading!

Happy coding!

--

--

Nikolay Nikolov

Head of Software Development at CONUTI GmbH | 20 years experience | Passionate about clean code, design patterns, and software excellence.