The Factory Pattern

·

3 min read

The Factory Pattern is a versatile design pattern that simplifies object creation, by providing a method that acts as a “factory” for creating objects. It abstracts the instantiation process, allowing the type of object designed to be determined at runtime.

Image creating a user authentication system where users have different roles like Admin, Members or Guest. Each role might have a distinct authentication mechanism and permissions.

Before using the Factory Pattern:

type UserRole = 'Admin' | 'Member' | 'Guest';
type User = { role: UserRole; authenticate: () => string; };

const createAdminUser = (): User => ({ role: 'Admin', authenticate: () => 'Admin authenticated' });
const createMemberUser = (): User => ({ role: 'Member', authenticate: () => 'Member authenticated' });
const createGuestUser = (): User => ({ role: 'Guest', authenticate: () => 'Guest authenticated' });

let user: User;
const role: UserRole = 'Admin';

if(role === 'Admin') {
    user = createAdminUser();
} else if(role === 'Member') {
    user = createMemberUser();
} else if(role === 'Guest') {
    user = createGuestUser();
} else {
    throw new Error('Invalid user role');
}

After using the Factory Pattern:

type UserRole = 'Admin' | 'Member' | 'Guest';
type User = { role: UserRole; authenticate: () => string; };

const createAdminUser = (): User => ({ role: 'Admin', authenticate: () => 'Admin authenticated' });
const createMemberUser = (): User => ({ role: 'Member', authenticate: () => 'Member authenticated' });
const createGuestUser = (): User => ({ role: 'Guest', authenticate: () => 'Guest authenticated' });

const createUser = (role: UserRole): User => {
    switch(role) {
        case 'Admin': return createAdminUser();
        case 'Member': return createMemberUser();
        case 'Guest': return createGuestUser();
        default: throw new Error('Invalid user role');
    }
};

const user = createUser('Admin');

Key Points

  • Decoupling: Client code is decoupled from the object creation code, promoting modularity.

  • Flexibility: New types/functions can be added seamlessly without altering existing code.

  • Abstraction: The client code does not need to know about the concrete types or creation logic.

We can see that the Factory Pattern abstracts away the complexity and provides a cleaner, more maintainable and scalable solution. However, as with everything, its crucial to apply it only where the benefits outweigh the introduced abstraction layer.

When to use:

The Factory Pattern is highly beneficial in certain scenarios where object creation complexity needs to be managed and controlled effectively.

  1. Diverse Object Creation: When your system needs to create objects from several classes or types, and the exact type isn’t known until runtime.

  2. Complex Instantiation Logic: If the instantiation of an object involves multiple steps, conditions, or configurations that you want to encapsulate from the client code.

  3. Decoupling Object Creation: When you want to decouple the object creation code from the code that uses the object, promoting modular design and enhancing code maintainability.

  4. Flexibility in Object Creation: If your system is expected to be extended or modified in the future, a factory can provide a flexible and centralised way to manage these changes.

  5. Managing Dependencies: When you need to manage, control, or track objects being created, such as maintaining a registry of created instances.

  6. Switching Implementations: You may need to switch between different implementations or configurations of an object dynamically at runtime.

Examples:

  • Payment Gateway Integration: In e-commerce applications, where different payment gateways (like PayPal, Stripe, or a bank transfer) might be used, and the choice of gateway might depend on user preferences or regional settings.

  • UI Component Rendering: In UI libraries or frameworks, different components need to be rendered based on user interactions or data. A factory method could create and return the appropriate component dynamically.

  • Database Connections: In scenarios where an application might connect to different types of databases (SQL, NoSQL, local, remote) based on configurations or user data.

  • User Role Management: When various user roles need different authentication and authorization mechanisms, a user factory can create instances of different user classes (Admin, Member, Guest) based on the role data.