Factory Method Pattern: The Secret to Scalable and Modular Applications

Factory Method Pattern The Secret to Scalable and Modular Applications

When building complex applications, one of the most crucial aspects is how we create and manage objects. Directly instantiating objects in various parts of your codebase can lead to rigid, hard-to-maintain code that is difficult to extend. This is where the Factory Method Pattern comes into play.

The Factory Pattern is a creational design pattern that provides a flexible way to create objects, allowing subclasses to modify the type of object to be instantiated without changing the client code. This pattern is especially useful when the types of objects to be created may vary or need to be determined at runtime, promoting a more modular and maintainable design.

In this post, we’ll explore how the Factory Method Pattern can help you avoid the pitfalls of rigid object creation, making your code more modular and adaptable to change.

When to Use the Factory Method Pattern

Here are a few scenarios where the Factory Method Pattern shines:

  • Complex or Variable Object Creation: If your object creation involves complex logic or needs to vary based on different conditions, a factory method can encapsulate this complexity and make the client code cleaner and more reusable.

  • Decoupling Object Creation: By using a factory method, you can separate the responsibility of creating objects from the classes that use them. This reduces dependencies and makes it easier to modify or extend the system without breaking existing code.

  • Changing dynamically: When the creation of objects can be changed dynamically, but you still want to maintain an abstraction of how objects are instantiated.

  • Handling Product Variations: If your system needs to handle multiple versions of a product or is expected to introduce new types in the future, the Factory Method Pattern provides a flexible and scalable solution. You can define a specific factory method for each product type.

  • Customizable Object Creation: A factory method can also encapsulate configuration logic, allowing clients to customize how objects are created by passing parameters or options to the method.

Without Factory Pattern

let’s see how this code looks without using a Factory:

C#
				public async Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system)
{
    IMessageSchedulingService service;

    // Creating the service based on the type of message
    if (message.Type == EMessageType.Email)
        service = new EmailNotificationService(Params...);
    else if (message.Type == EMessageType.Sms)
        service = new SmsNotificationService(Params...);
    else if (message.Type == EMessageType.Push)
        service = new PushNotificationService(Params...);
    else
        service = new DefaultNotificationService(Params...);

    return await service.SendPreviewAsync(message, system);
}

			

The Problems with This Approach

This approach has a few common issues:

  1. Tight Coupling: The method directly depends on concrete implementations (EmailNotificationService, SmsNotificationService, etc.). This means every time we want to add a new notification type or modify an existing one, we have to modify this method.

  2. Hard to Maintain: As the application grows and we need to introduce new notification types, we will have to continually update this method. This can lead to bloated and hard-to-maintain code.

  3. Ignoring Single Responsibility Principle (SRP): Without the use of the Factory Method Pattern, the object creation logic is often mixed directly within your main business logic, violating this principle, multiple reasons for change.

  4. Ignoring Open/Closed Principle (OCP): Without using the Factory Method Pattern, it becomes harder to add new types of products (or services, in this case) without modifying the existing code. This violates OCP and makes the code harder to extend.

With Factory Method Pattern

let’s see how this code looks creating a Factory:

C#
				public class MessageSchedulingServiceFactory(Params...) : IMessageSchedulingServiceFactory
{
    public IMessageSchedulingService Get(EMessageType type)
            => type switch
            {
                EMessageType.Email => new EmailMessageSchedulingService(Params...),
                EMessageType.Sms => new SmsMessageSchedulingService(Params...),
                EMessageType.Push => new PushMessageSchedulingService(Params...),
                _ => new DefaultMessageSchedulingService(Params...)
            };
}

			

Now in the main method call the factory:

C#
				public async Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system)
{
    var factory = messageSchedulingServiceFactory.Get(message.Type);

    return await service.SendPreviewAsync(message, system);
}
			

Now, we implement the same interface in each class and consequently the method SendPreviewAsync():

C#
				public interface IMessageSchedulingService
{
    Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system);
}
			
C#
				public class EmailMessageSchedulingService(Params...) : IMessageSchedulingService
{
    public async Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system)
    {
        //Logic
        return Result<int>.Success(Param...);
    }
}

public class SmsMessageSchedulingService(Params...) : IMessageSchedulingService
{
    public async Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system)
    {
        //Logic
        return Result<int>.Success(Param...);
    }
}

public class PushMessageSchedulingService(Params...) : IMessageSchedulingService
{
    public async Task<Result<int>> SendPreviewAsync(PreviewMessageRequest message, ESystemType system)
    {
        //Logic
        return Result<int>.Success(Param...);
    }
}
			

Main advantages of factory method pattern:

  • Separates creation logic from client code, improving flexibility.
  • New product types can be added easily.
  • Simplifies unit testing by allowing mock product creation.
  • Centralizes object creation logic across the application.
  • Hides specific product classes from clients, reducing dependency.

This is what the Factory Pattern looks like in a Diagram

This diagram shows how our factory is responsible for managing instances and will manage new classes that are added in the future.

Diagram Factory Method (2)

Conclusion

The Factory Method Design Pattern can be a powerful tool that enhances flexibility, scalability, and maintainability in your code. By abstracting the object creation logic into a dedicated factory class, you can easily manage different types of services without cluttering your client code. This approach aligns well with key software design principles such as the Single Responsibility Principle and Open/Closed Principle, allowing you to introduce new service types without affecting existing code.

However, it’s important to understand when and how to implement the Factory Method. While it can simplify the code in many cases, overusing it or applying it incorrectly may add unnecessary complexity. Carefully assess your project’s needs to determine if the Factory Method is the best solution for your specific use case.

When implemented appropriately, the Factory Method ensures a modular and adaptable system, making it easier to extend, modify, and maintain over time.

Have you used the Factory Method in your projects?

Thank you for reading.

See you next time!

Share the Post:
plugins premium WordPress