Design Patterns — Factory Method

Umur Alpay
CodeResult
Published in
6 min readSep 3, 2023

We’re launching a detailed exploration into the world of design patterns. This series promises an in-depth look at each pattern, from the basic building blocks to the more intricate ones. By the journey’s end, you’ll grasp not just the how-to but also the underlying principles that drive these patterns. Today we will cover the Factory Method design pattern.

First Some History

The Early Days

The roots of the Factory Method Pattern can be traced back to the early days of object-oriented programming (OOP). As software systems grew in complexity during the late 1970s and early 1980s, there was a pressing need for structures that could promote both reusability and flexibility. The Factory Method emerged as a solution to a recurring problem: How can an object be created so that its specific implementation is decoupled from the client that uses it?

The Gang of Four Influence

The Factory Method Pattern’s prominence was significantly boosted in 1994 with the publication of the seminal book “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, commonly referred to as the “Gang of Four” (GoF). In their book, the GoF presented the Factory Method as one of the 23 classic design patterns, solidifying its place in the software design lexicon.

Benefits of Using Factory Method Design Pattern

Decoupling Code

One of the most significant advantages of the Factory Method Pattern is the decoupling it introduces. By separating the creation of objects from the classes that use them, we ensure that our system remains modular. This decoupling means that changes in one part of the system don’t ripple through and cause unexpected issues in other parts.

Scalability

The Factory Method Pattern shines when it comes to scalability. As our software grows and evolves, introducing new object types or changing existing ones becomes a breeze. Since the creation logic is centralized in the factory, we can easily modify or extend it without affecting the client code that uses the objects.

Consistency in Object Creation

By centralizing the object creation process, we ensure a consistent approach across our application. Whether it’s setting default values, ensuring certain configurations, or applying specific constraints, having a single point of creation ensures that every object is instantiated just the way we want it.

Simplified Client Code

Clients don’t need to know the intricate details of object creation. They simply request an object, and the factory handles the rest. This abstraction simplifies client code, making it cleaner and easier to maintain.

Flexibility in Product Configurations

Factories can be designed to accept parameters that dictate the type or configuration of the object to be created. This flexibility allows for dynamic object creation based on runtime conditions, making the system more adaptable to varying requirements.

Easier Testing and Mocking

For those who practice test-driven development (TDD), the Factory Method Pattern is a boon. By decoupling object creation, it becomes much easier to substitute real implementations with mock objects, facilitating isolated unit testing.

Code Reusability

The Factory Method Pattern promotes reusability. Once a factory is set up for a particular object type, it can be reused across different parts of the application or even in different projects, reducing redundancy and ensuring consistent object creation.

Drawbacks of Using Factory Method Design Pattern

Complexity Overhead

For smaller applications or situations where the object creation process is straightforward, introducing the Factory Method Pattern can add unnecessary complexity. The additional classes and interfaces might seem like overkill, leading to increased development time and potential confusion for developers unfamiliar with the pattern.

Difficulties in Refactoring

Once the Factory Method Pattern is deeply ingrained in a system, refactoring can become challenging. If you ever need to change the way objects are created or introduce new dependencies, you might find yourself navigating a maze of factory methods and their implementations.

Potential for Code Duplication

If not implemented carefully, different factories might end up having similar or duplicated code. This redundancy can make the codebase harder to maintain and introduce subtle bugs if changes are made in one place but overlooked in another.

Overhead in Object Creation

While factories provide a centralized place for object creation, they can sometimes introduce performance overhead, especially if the creation process is resource-intensive or if the factory performs additional checks and configurations.

Rigidity in Factory Hierarchies

While the Factory Method Pattern promotes flexibility in product creation, the factory hierarchies themselves can become rigid. Introducing a new factory or altering the hierarchy might require significant changes to the existing factories and the client code that uses them.

Obscured Object Initialization

Factories abstract away the object creation process. While this abstraction is beneficial in many scenarios, it can sometimes obscure the specifics of object initialization, making it harder for developers to understand the lifecycle and configuration of the created objects.

Ideal Use Cases

Decoupling Object Creation

Scenario: When you want to decouple the creation of an object from its use, ensuring that the system remains modular and adaptable.

Example: Consider a UI library where you need to create buttons. The appearance and behavior of these buttons might differ based on the operating system (Windows, macOS, Linux). Using the Factory Method, you can abstract the creation process, allowing the system to provide the appropriate button without tying the client code to a specific button implementation.

Dynamic Instantiation

Scenario: When the exact type of the object to be created isn’t known until runtime.

Example: In a gaming application, based on the player’s choices or game progress, different enemy types might need to be instantiated. A factory method can determine which enemy to create based on the current game state.

Centralizing & Standardizing Creation

Scenario: When you want to centralize and standardize the object creation process, ensuring consistency across the application.

Example: In a cloud-based application, you might need to create different types of storage solutions (like AWS S3, Azure Blob, or Google Cloud Storage). A factory can ensure that each storage solution is initialized with consistent logging, error handling, and configuration settings.

Simplifying Object Creation

Scenario: When creating an object requires a complex setup or involves multiple steps.

Example: In a database ORM (Object-Relational Mapping) system, creating a database connection might involve setting up pooling, logging, and handling retries. A factory method can encapsulate this complexity, offering a simplified interface to the client.

Promoting Extensibility

Scenario: When you anticipate introducing new types or variations of objects in the future and want to ensure the system remains open for extension.

Example: In an e-commerce platform, as the business grows, you might introduce new types of discount strategies (like seasonal discounts, loyalty discounts, or flash sales). Using the Factory Method, you can easily add new discount types without altering the existing checkout logic.

Facilitating Unit Testing

Scenario: When you want to facilitate unit testing by making it easier to replace real implementations with mock objects.

Example: In a service that fetches data from an external API, using a factory method allows you to swap out the real data-fetching mechanism with a mock version, ensuring that tests run quickly and aren’t dependent on external systems.

Implementation Example for Factory Method Design Pattern (Javascript)

// Step 1: Define the Product interface
class UserAccount {
getPermissions() {
throw new Error("The method getPermissions is not implemented.");
}
}
// Step 2: Create Concrete Products
class AdminAccount extends UserAccount {
getPermissions() {
return ["read", "write", "delete", "execute"];
}
}
class GuestAccount extends UserAccount {
getPermissions() {
return ["read"];
}
}
class ContributorAccount extends UserAccount {
getPermissions() {
return ["read", "write"];
}
}
// Step 3: Define the Creator (Factory) class
class UserAccountFactory {
createUserAccount(role) {
switch (role) {
case 'Admin':
return new AdminAccount();
case 'Guest':
return new GuestAccount();
case 'Contributor':
return new ContributorAccount();
default:
throw new Error(`Role ${role} is not supported.`);
}
}
}
// Usage:
const factory = new UserAccountFactory();
const admin = factory.createUserAccount('Admin');
console.log(admin.getPermissions()); // Output: ["read", "write", "delete", "execute"]
const guest = factory.createUserAccount('Guest');
console.log(guest.getPermissions()); // Output: ["read"]
const contributor = factory.createUserAccount('Contributor');
console.log(contributor.getPermissions()); // Output: ["read", "write"]

Follow me on Instagram, Facebook or Twitter if you like to keep posted about tutorials, tips and experiences from my side.

You can support me from Patreon, Github Sponsors, Ko-fi or Buy me a coffee

--

--