The abstract factory pattern is a creational pattern which provides you with an interface for creating families of related objects without specifying their concrete classes. As long as the objects are created using the factory, you don't have to worry about creating the wrong combination of objects.
You can find the full example source code here.
In our example we have two kinds of objects: Orders
and Payments
.
interface Order {
id: number;
addProduct(productId: string): void;
addShippingAddres(address: string): void;
}
interface Payment {
addCreditCardNumber(ccNumber: number): void;
completePayment(order: Order): boolean;
}
For our example we have two variants: Online and Physical. So we have to create the following classes: OnlineOrder
, OnlinePayment
, PhysicalOrder
and PhysicalPayment
.
/**
* Order object + Online variant
*/
class OnlineOrder implements Order {
id: number;
constructor(id: number) {
this.id = id;
}
addProduct(productId: string): void {
console.log(`Product ${productId} added to the online order`);
}
addShippingAddres(address: string): void {
console.log(`Shipping address ${address} added to the online order`);
}
}
/**
* Order object + Physical variant
*/
class PhysicalOrder implements Order {
id: number;
constructor(id: number) {
this.id = id;
}
addProduct(productId: string): void {
console.log(`Product ${productId} added to the physical order`);
}
addShippingAddres(address: string): void {
console.log(`Shipping address ${address} added to the physical order`);
}
}
/**
* Payment object + Online variant
*/
class OnlinePayment implements Payment {
addCreditCardNumber(ccNumber: number): void {
console.log(`Credit card number ${ccNumber} added to the online payment`);
}
completePayment(order: OnlineOrder): boolean {
console.log(`Payment completed for the online order ${order.id}`);
return true;
}
}
/**
* Payment object + Physical variant
*/
class PhysicalPayment implements Payment {
addCreditCardNumber(ccNumber: number): void {
console.log(`Credit card number ${ccNumber} added to the physical payment`);
}
completePayment(order: PhysicalOrder): boolean {
console.log(
`Physical payment completed for the physical order ${order.id}`
);
return true;
}
}
Abstract Factory
which is an interface with a list of methods that will create the objects. These methods should have as a return type the interfaces of the products that we defined in the first step.Our abstract factory has to define two methods: one to create orders and another one to create payments.
interface CommerceFactory {
createOrder(id: number): Order;
createPayment(): Payment;
}
Abstract Factory
has to be created for each variant of the family of objects.In our example we have to create one factory for the online variant and another one for the physical variant.
/**
* Factory for the Online variant
*/
class OnlineCommerceFactory implements CommerceFactory {
createOrder(id: number): Order {
return new OnlineOrder(id);
}
createPayment(): Payment {
return new OnlinePayment();
}
}
/**
* Factory for the Physical variant
*/
class PhysicalCommerceFactory implements CommerceFactory {
createOrder(id: number): Order {
return new PhysicalOrder(id);
}
createPayment(): Payment {
return new PhysicalPayment();
}
}
An example of how client code would create objects for the online variant:
const onlineCommerceFactory = new OnlineCommerceFactory();
const onlineOrder = onlineCommerceFactory.createOrder(1);
const onlinePayment = onlineCommerceFactory.createPayment();
onlineOrder.addProduct("123");
onlineOrder.addShippingAddres("123 Main St");
onlinePayment.addCreditCardNumber(123456789);
onlinePayment.completePayment(onlineOrder);
An this is how the client would use the physical variant:
const physicalCommerceFactory = new PhysicalCommerceFactory();
const physicalOrder = physicalCommerceFactory.createOrder(2);
const physicalPayment = physicalCommerceFactory.createPayment();
physicalOrder.addProduct("456");
physicalOrder.addShippingAddres("456 Main St");
physicalPayment.addCreditCardNumber(987654321);
physicalPayment.completePayment(physicalOrder);
It's important that the client code uses the abstract interfaces and not the concrete classes. This lets us change the family of objects that are returned dynamically without modifying or breaking client code and it isolates the client from concrete implementations. The client can work with any family of objects as longs as it uses the abstract interfaces.
As always, make sure it makes sense to use this pattern in your application. Otherwise you could be introducing unnecessary complexity.