In the previous article, we explored the Factory Method pattern and saw how it delegates object creation to subclasses.
But what if your application doesn't need to create just one object?
What if it needs an entire family of related objects that are designed to work together?
That's exactly the problem the Abstract Factory pattern solves.
The Problem
Imagine you're building an e-commerce application that supports multiple payment providers.
Your application currently integrates with:
- Stripe
- PayPal
For each payment provider, you need several related objects:
- A Payment processor
- A Refund processor
- A Customer service
For Stripe, those objects might be:
StripePayment
StripeRefund
StripeCustomer
For PayPal:
PayPalPayment
PayPalRefund
PayPalCustomer
If you instantiate each object manually, your code quickly becomes filled with conditionals:
if provider == :stripe
payment = StripePayment.new
refund = StripeRefund.new
customer = StripeCustomer.new
else
payment = PayPalPayment.new
refund = PayPalRefund.new
customer = PayPalCustomer.new
end
The problem isn't creating a single object anymore.
It's ensuring that all related objects come from the same payment provider.
The client shouldn't accidentally create a StripePayment with a PayPalRefund.
Abstract Factory
The Gang of Four defines Abstract Factory as:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Instead of asking for individual objects, the client asks a factory to provide an entire family.
StripeFactory
├── Payment
├── Refund
└── Customer
PayPalFactory
├── Payment
├── Refund
└── Customer
The client never knows which concrete classes it's using.
It only communicates with the factory interface.
Structure
The pattern consists of five participants.
Abstract Factory
Defines the interface for creating every product in the family.
create_payment
create_refund
create_customer
Concrete Factory
Implements the Abstract Factory.
Each concrete factory creates one complete product family.
Examples:
- StripeFactory
- PayPalFactory
Abstract Product
Defines the common interface shared by each product.
Examples:
- Payment
- Refund
- Customer
Concrete Products
The concrete implementations belonging to a specific family.
StripePayment
StripeRefund
StripeCustomer
PayPalPayment
PayPalRefund
PayPalCustomer
Client
The client depends only on the Abstract Factory.
At runtime, it receives whichever concrete factory is needed.
This completely decouples the client from concrete implementations.
Ruby Example
Products
class StripePayment
def process(amount)
puts "Charging $#{amount} with Stripe"
end
end
class StripeRefund
def process(transaction_id)
puts "Refunding #{transaction_id} through Stripe"
end
end
class StripeCustomer
def create(name)
puts "Creating Stripe customer: #{name}"
end
end
class PayPalPayment
def process(amount)
puts "Charging $#{amount} with PayPal"
end
end
class PayPalRefund
def process(transaction_id)
puts "Refunding #{transaction_id} through PayPal"
end
end
class PayPalCustomer
def create(name)
puts "Creating PayPal customer: #{name}"
end
end
Abstract Factory
class PaymentProviderFactory
def create_payment
raise NotImplementedError
end
def create_refund
raise NotImplementedError
end
def create_customer
raise NotImplementedError
end
end
Concrete Factories
class StripeFactory < PaymentProviderFactory
def create_payment
StripePayment.new
end
def create_refund
StripeRefund.new
end
def create_customer
StripeCustomer.new
end
end
class PayPalFactory < PaymentProviderFactory
def create_payment
PayPalPayment.new
end
def create_refund
PayPalRefund.new
end
def create_customer
PayPalCustomer.new
end
end
Client
class CheckoutService
def initialize(factory)
@factory = factory
end
def checkout(customer_name, amount)
customer = @factory.create_customer
payment = @factory.create_payment
customer.create(customer_name)
payment.process(amount)
end
def refund(transaction_id)
refund = @factory.create_refund
refund.process(transaction_id)
end
end
Using the client:
factory = StripeFactory.new
checkout = CheckoutService.new(factory)
checkout.checkout("Alice", 100)
checkout.refund("txn_123")
Switching to PayPal requires changing only one line:
factory = PayPalFactory.new
checkout = CheckoutService.new(factory)
The rest of the application remains unchanged.
Notice that CheckoutService never references StripePayment, PayPalPayment, or any other concrete implementation.
It only communicates with the Abstract Factory.
Factory Method vs Abstract Factory
Although both patterns deal with object creation, they solve different problems.
| Factory Method | Abstract Factory |
|---|---|
| Creates one product | Creates a family of related products |
| Uses inheritance | Uses object composition |
| Subclasses decide what to create | A factory object decides which family to create |
| Best when object creation varies | Best when entire product families vary |
Another way to think about it:
Factory Method asks:
Which object should I create?
Abstract Factory asks:
Which family of objects should I create?
Adding New Products vs New Families
Suppose our payment providers now support webhooks.
We need another product:
create_webhook
With Abstract Factory, the abstract factory interface must change.
class PaymentProviderFactory
def create_payment; end
def create_refund; end
def create_customer; end
def create_webhook; end
end
Every concrete factory (StripeFactory, PayPalFactory, etc.) must also implement create_webhook.
On the other hand, if you add a completely new payment provider—say Adyen—you simply introduce a new concrete factory:
AdyenFactory
├── AdyenPayment
├── AdyenRefund
├── AdyenCustomer
The existing factories remain unchanged.
That's why people often say:
- Factory Method is open to new creators.
- Abstract Factory is open to new product families.
Where You'll See It in Rails
Although Rails doesn't explicitly label it as the Abstract Factory pattern, the idea appears in several places.
Active Storage
Depending on your configuration, Rails creates a family of storage-related objects for:
- Local Disk
- Amazon S3
- Google Cloud Storage
- Azure Blob Storage
Your application interacts with a common interface while Rails supplies the correct implementation behind the scenes.
When Should You Use It?
Use Abstract Factory when:
- Your application works with multiple product families.
- Objects within a family should always be used together.
- You want to switch implementations through configuration.
- You want to isolate your application from concrete classes.
Avoid it when you only need to create a single object. In that case, Factory Method or even a Simple Factory is usually a better fit.
Key Takeaways
The Factory Method pattern creates one object and relies on inheritance to determine which concrete product to instantiate.
The Abstract Factory pattern creates families of related objects and relies on composition to swap entire implementations at runtime.
A simple rule to remember is:
Factory Method answers: Which object should I create?
Abstract Factory answers: Which family of objects should I create?
for a full example in ruby check this https://github.com/ShroukAbozeid/design-pattern/tree/main/factory/abstract_factory
Top comments (0)