DEV Community

Xavier Carrera Gimbert
Xavier Carrera Gimbert

Posted on

Abstract Factory Pattern: Explained for Humans

The Problem

We have the classes Payment and Refund. We use these classes to process and refund transactions, as the names imply.

However, we use different payment providers. So, each of the mentioned classes will be instantiated differently depending on the provider, even though they will perform the same core job.

How do we tackle this code problem in a clean way?

By using the Abstract Factory pattern.

The Solution: The Abstract Factory Pattern

First, we create the interfaces that will define the core functionality of the classes we need.

// Product interfaces

interface Payment {
  pay(amount: number): Promise<void>;
}

interface Refund {
  refund(transactionId: string): Promise<void>;
}

ℹ️ We call these interfaces "Product interfaces" because the classes that are going to implement these interfaces are the products of (or instantiated by) factory methods.

ℹ️ A factory is simply a function (or a method or a class) that instantiates a class.

Now we define the interface that the factory class will use to instantiate these products.

// Abstract Factory interface

interface PaymentAbstractFactory {
  createPayment(): Payment;
  createRefund(): Refund;
}

We then define the specific product classes we will instantiate using the interfaces created previously:

// Concrete Products for Stripe

class StripePayment implements Payment {
  async pay(amount: number): Promise<void> {
    console.log(`Processing payment of $${amount} via Stripe...`);
    // Stripe payment logic here
  }
}

class StripeRefund implements Refund {
  async refund(transactionId: string): Promise<void> {
    console.log(
      `Processing refund for transaction ${transactionId} via Stripe...`,
    );
    // Stripe refund logic here
  }
}

And now we create the factory that will create the products:

// Concrete Factory for Stripe

class StripeFactory implements PaymentAbstractFactory {
  createPayment(): Payment {
    return new StripePayment();
  }

  createRefund(): Refund {
    return new StripeRefund();
  }
}

And finally, we use it in the client!

// Client code

const processTransaction = async (
  factory: PaymentAbstractFactory,
  amount: number,
) => {
  const payment = factory.createPayment();
  await payment.pay(amount);
};

const refundTransaction = async (
  factory: PaymentAbstractFactory,
  transactionId: string,
) => {
  const refund = factory.createRefund();
  await refund.refund(transactionId);
};

const stripeFactory = new StripeFactory();
processTransaction(stripeFactory, 100);
refundTransaction(stripeFactory, 'stripe_tx_12345');

What have we done here?

We have created an interface (the Abstract Factory) that we use to create factories. These factories, in turn, create a family of product classes that share a common relationship (e.g., all belonging to the Stripe provider).

Why is this pattern useful?

It allows us to have factories that generate classes which have a common relationship.

In this case, we are able to have a factory that generates classes that work for a common payment service.

We could easily create another factory for a different payment service and use it too:

// Concrete Factory for PayPal

class PayPalFactory implements PaymentAbstractFactory {
  createPayment(): Payment {
    return new PayPalPayment();
  }

  createRefund(): Refund {
    return new PayPalRefund();
  }
}

Where can I learn more about this pattern?

You can ask your trustworthy LLM, drop a comment, or visit one of the most famous resources to learn about design patterns: Refactoring Guru.

Here is the specific article about this design pattern: Abstract Factory

Goodbye!

I hope this article helped you understand this design pattern better! Feel free to leave a comment if you have any questions or feedback.

Thanks!

Top comments (0)