Creational Design Patterns - Part 2 - Factory Design Pattern

Deep Dive: Factory Method Pattern in C#

Deep Dive: The Factory Method Pattern

Decoupling object creation from the client.

What is the Factory Method Pattern?

The Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It defines a method for creating an object, which subclasses can then override to specify the exact class of the object that will be created. This pattern lets a class defer instantiation to its subclasses, promoting loose coupling and adhering to the Open/Closed Principle.

Pros

  • Avoids tight coupling between the creator and the concrete products.
  • Single Responsibility Principle: You can move product creation code into one place.
  • Open/Closed Principle: You can introduce new product types without changing existing client code.

Cons

  • Can lead to a large hierarchy of creator classes that parallels the product class hierarchy.
  • Can be an overkill for simple scenarios.

Use Case: Handling Multiple Payment Providers

The Problem

Imagine an e-commerce application that needs to process payments. The application supports various payment methods: Credit Card, PayPal, and Bank Transfer. Each method requires a different process, communicates with a different API, and needs a distinct processing object. A naive approach would be to use a large `switch` statement in your main payment processing logic:

if (method == "CreditCard") { /*...*/ }
else if (method == "PayPal") { /*...*/ }

This approach is brittle. Adding a new payment method (like "Crypto") requires modifying this core logic, violating the Open/Closed Principle and making the code harder to maintain.

The Solution

The Factory Method pattern allows us to delegate the responsibility of creating the specific payment provider object to subclasses. We define an abstract `PaymentProcessor` with a `CreatePaymentProvider()` factory method. Then, we create concrete subclasses like `CreditCardProcessor` and `PayPalProcessor`. Each subclass overrides the factory method to return the correct provider object. The client code can now work with any `PaymentProcessor` without knowing the concrete details, making it easy to add new payment methods in the future.

C# Implementation

Let's model the payment provider scenario. We'll have an interface for payment providers (the Product), concrete providers, an abstract creator with the factory method, and concrete creators for each payment type.

// --- 1. The Product Interface ---
public interface IPaymentProvider
{
    void ProcessPayment(decimal amount);
}

// --- 2. Concrete Products ---
public class CreditCardProvider : IPaymentProvider { /* ... */ }
public class PayPalProvider : IPaymentProvider { /* ... */ }

// --- 3. The Abstract Creator ---
public abstract class PaymentProcessor
{
    // The Factory Method - subclasses MUST implement this.
    public abstract IPaymentProvider CreatePaymentProvider();

    // A core business logic method that uses the factory method.
    public void ProcessOrderPayment(decimal amount)
    {
        IPaymentProvider provider = CreatePaymentProvider(); // The magic happens here!
        provider.ProcessPayment(amount);
        Console.WriteLine("Payment processing complete.");
    }
}

// --- 4. Concrete Creators ---
public class CreditCardPaymentProcessor : PaymentProcessor
{
    public override IPaymentProvider CreatePaymentProvider()
    {
        return new CreditCardProvider();
    }
}

public class PayPalPaymentProcessor : PaymentProcessor
{
    public override IPaymentProvider CreatePaymentProvider()
    {
        return new PayPalProvider();
    }
}

// --- 5. Client Code ---
public class ECommerceSite
{
    // This client is still coupled to the concrete creators.
    // See the "Variations" section for a better approach.
    public void Checkout(string paymentMethod, decimal orderTotal)
    {
        PaymentProcessor processor = null;
        if (paymentMethod == "CreditCard") { processor = new CreditCardPaymentProcessor(); }
        else if (paymentMethod == "PayPal") { processor = new PayPalPaymentProcessor(); }
        processor?.ProcessOrderPayment(orderTotal);
    }
}

Variations & Improvements

Decoupling the Client with Dependency Injection

The client code above still has an `if/else` block, which means it knows about the concrete creator classes. A better system would use Dependency Injection to provide the correct factory at runtime, so the client only knows about the abstract `PaymentProcessor`.

public class ECommerceSite
{
    private readonly PaymentProcessor _processor;

    // The concrete factory is "injected" via the constructor.
    public ECommerceSite(PaymentProcessor processor)
    {
        _processor = processor;
    }

    public void Checkout(decimal orderTotal)
    {
        // The client is now completely decoupled from concrete types!
        _processor.ProcessOrderPayment(orderTotal);
    }
}

Parameterized Factory Method

A common variation is to have a single concrete factory with a method that takes a parameter to determine which product to create. This can reduce the number of creator classes if the creation logic is simple. This is often just called a "Factory" and is a step simpler than the full Factory Method pattern.

public enum PaymentType { CreditCard, PayPal }

public class PaymentProviderFactory
{
    public IPaymentProvider CreateProvider(PaymentType type)
    {
        switch (type)
        {
            case PaymentType.CreditCard: return new CreditCardProvider();
            case PaymentType.PayPal: return new PayPalProvider();
            default: throw new NotSupportedException();
        }
    }
}

Common Mistakes to Avoid

  1. Returning Concrete Types: The factory method should always return the abstract product type (e.g., `IPaymentProvider`), not a concrete type. Returning a concrete type defeats the purpose of decoupling the client from the implementation.
  2. Overcomplicating the Creator: The primary job of the Concrete Creator is to create the product. If it starts accumulating lots of other business logic, it might be violating the Single Responsibility Principle.
  3. Breaking the Product Interface Contract: All concrete products must fully adhere to the product interface. If one concrete product behaves wildly differently from another, it violates the Liskov Substitution Principle and can lead to unexpected client-side bugs.

Factory Method vs. Abstract Factory

This is a common point of confusion. Both patterns create objects, but they operate at different scales and have different intents.

  • Factory Method is about creating a single object. It uses inheritance to delegate the creation of that single object to subclasses. Think of it as a single production line that can be reconfigured by swapping out the station (subclass) that makes the core component.
  • Abstract Factory is about creating families of related objects. It uses composition to delegate the creation of a whole set of objects to a separate factory object. Think of it as choosing a whole factory (e.g., a "Modern Furniture" factory vs. a "Victorian Furniture" factory) to get a consistent set of products (chair, table, sofa).

Prerequisite C# Topics to Understand

To fully grasp this pattern's C# implementation, you should be comfortable with these concepts:

`abstract class`
An abstract class cannot be instantiated on its own and is meant to be inherited by other classes. It can contain abstract methods (with no body) and concrete methods. Our `PaymentProcessor` is abstract because it defines the *idea* of processing a payment but relies on subclasses to provide the specific creator logic.
`interface`
An interface defines a contract—a set of public methods and properties. Any class that implements the interface must provide an implementation for all its members. `IPaymentProvider` ensures that all our provider classes have a `ProcessPayment` method, allowing the client to treat them uniformly.
`virtual` and `override`
The `abstract` keyword forces a subclass to implement a method. The `virtual` keyword allows a subclass to *optionally* override a method that already has a base implementation. The `override` keyword is used in the subclass to provide the new implementation for an abstract or virtual method. In our case, `override` is used to implement the abstract `CreatePaymentProvider` method.
Polymorphism
Literally "many shapes," this is the ability to treat objects of different classes that share a common interface or base class in the same way. Our `ProcessOrderPayment` method works with an `IPaymentProvider` object. It doesn't care if it's a `CreditCardProvider` or a `PayPalProvider`; it just knows it can call `ProcessPayment` on it. This is polymorphism in action.

Comments

Popular posts from this blog

Master Asp.Net Core Middleware concepts.

ASP.net Interview P1

Book Store Project