Abstract Factory Design Pattern
Deep Dive: The Abstract Factory Pattern
Creating Families of Related Objects.
What is the Abstract Factory Pattern?
The Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes. It's essentially a "factory of factories." You define an interface for creating a set of related products (e.g., a chair and a sofa), and then create concrete factory classes that implement this interface to produce products of a specific variant (e.g., a `ModernFurnitureFactory` creates modern chairs and sofas, while a `VictorianFurnitureFactory` creates Victorian ones). This ensures that the products created by a single factory are always compatible with each other.
Pros
- Ensures products from a factory are compatible with each other.
- Avoids tight coupling between concrete products and client code.
- Single Responsibility Principle: You extract product creation code into one place.
- Open/Closed Principle: You can introduce new variants of products without breaking existing client code.
Cons
- The codebase can become complex due to the many new interfaces and classes.
- Adding a new product type (e.g., a "table") requires changing the abstract factory and all of its subclasses.
Use Case: Supporting Multiple Payment Service Providers
The Problem
An application needs to support multiple payment gateways, like Stripe and PayPal. Each gateway has its own set of related services. For example, Stripe has one service for processing payments and another for handling refunds. PayPal has its own, different services for payments and refunds. We need to ensure that if we decide to process a payment with Stripe, we also use Stripe's refund service for any subsequent refunds on that transaction. We must avoid accidentally mixing a Stripe payment with a PayPal refund.
The Solution
The Abstract Factory pattern is perfect for this. We can define an `IPaymentGatewayFactory` interface that knows how to create both a payment service (`IPaymentService`) and a refund service (`IRefundService`). We then implement concrete factories: `StripeFactory` creates `StripePaymentService` and `StripeRefundService`, while `PayPalFactory` creates `PayPalPaymentService` and `PayPalRefundService`. The client code can now choose a factory and be certain that all the services it gets will be from the same compatible family.
C# Implementation
Here is a C# implementation of the payment gateway scenario. Notice how the client interacts with the abstract interfaces, completely unaware of the concrete Stripe or PayPal classes.
// --- 1. Abstract Product Interfaces ---
public interface IPaymentService { void ProcessPayment(decimal amount); }
public interface IRefundService { void ProcessRefund(decimal amount); }
// --- 2. Concrete Products for Stripe ---
public class StripePaymentService : IPaymentService { /* ... */ }
public class StripeRefundService : IRefundService { /* ... */ }
// --- 3. Concrete Products for PayPal ---
public class PayPalPaymentService : IPaymentService { /* ... */ }
public class PayPalRefundService : IRefundService { /* ... */ }
// --- 4. The Abstract Factory Interface ---
public interface IPaymentGatewayFactory
{
IPaymentService CreatePaymentService();
IRefundService CreateRefundService();
}
// --- 5. Concrete Factories ---
public class StripeFactory : IPaymentGatewayFactory
{
public IPaymentService CreatePaymentService() => new StripePaymentService();
public IRefundService CreateRefundService() => new StripeRefundService();
}
public class PayPalFactory : IPaymentGatewayFactory
{
public IPaymentService CreatePaymentService() => new PayPalPaymentService();
public IRefundService CreateRefundService() => new PayPalRefundService();
}
// --- 6. Client Code ---
public class CheckoutService
{
private readonly IPaymentService _paymentService;
private readonly IRefundService _refundService;
// The client gets a factory and uses it to create the whole family of products.
public CheckoutService(IPaymentGatewayFactory factory)
{
_paymentService = factory.CreatePaymentService();
_refundService = factory.CreateRefundService();
}
public void ProcessCheckout(decimal amount)
{
// The client uses the products without knowing their concrete types.
_paymentService.ProcessPayment(amount);
}
}
How to Decide: Factory Method vs. Abstract Factory
This is a common point of confusion. Both patterns create objects and promote loose coupling, but they solve problems at different scales. Here’s how to decide.
The Core Difference: One Object vs. a Family of Objects
- Factory Method is concerned with creating a single object. The subclasses decide which specific class to instantiate.
- Abstract Factory is concerned with creating a family of related objects. It ensures that the objects created together are from the same variant and are compatible.
Ask Yourself These Questions:
- 1. Does my client need to create different types of a single product?
- If yes, and the creation logic is complex enough to warrant subclasses, **Factory Method** is a good fit.
Example: A logistics app needs to create different `Transport` objects (`Truck`, `Ship`, `Plane`). A `RoadLogistics` class will have a factory method to create a `Truck`. - 2. Does my client need a set of related products that must work together?
- If yes, and you have multiple variants of these product families, **Abstract Factory** is the correct choice.
Example: A UI toolkit needs to create a `Button`, a `Checkbox`, and a `TextBox`. You need to ensure you don't mix a `WindowsButton` with a `MacCheckbox`. A `WindowsUIFactory` will create a consistent family of Windows-style controls. - 3. Is the main goal to let subclasses decide on the object type?
- This points to **Factory Method**. The pattern's power comes from using inheritance to change the created object type.
- 4. Is the main goal to ensure consistency among multiple created objects?
- This points to **Abstract Factory**. Its primary goal is to guarantee that the products from a single factory belong to the same variant.
Prerequisite C# Topics to Understand
To fully grasp this pattern's C# implementation, you should be comfortable with these concepts:
- `interface`
- This pattern relies heavily on interfaces to define contracts for both the products and the factories. The client code interacts exclusively with these interfaces, which is the key to achieving loose coupling.
- Polymorphism
- The client can work with any object that implements the `IPaymentGatewayFactory` interface. It doesn't care if it's a `StripeFactory` or a `PayPalFactory`. This ability to treat different objects in a uniform way through a common interface is polymorphism.
- Composition over Inheritance
- Unlike Factory Method, which uses inheritance, Abstract Factory uses composition. The client holds a reference to a factory object rather than inheriting from a factory class. This is often more flexible.
Comments
Post a Comment