Abstract Factory | Deep Dive In Design Patterns
The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It allows a client to create objects of a particular family without knowing the exact classes that will be instantiated.
Advantages
- Encapsulation: It hides the concrete classes and the instantiation process, making it easier to introduce new families of products without altering the client code.
- Consistency: Ensures that objects from the same family are used together, as the factory creates related objects.
- Scalability: Adding new product families or variants can be done by extending the existing abstract factory without modifying existing code.
Disadvantages
- Complexity: It can introduce additional layers of abstraction, which may make the codebase more complex and harder to understand.
- Overhead: If the system does not need to create products from multiple families, the pattern might introduce unnecessary abstraction and overhead.
Code Sample in TypeScript
Let’s create a simple example with an Abstract Factory pattern. Suppose we have a system that produces two types of UI components: Buttons and Checkboxes. We want to create different families of UI components for different operating systems: Windows and MacOS.
- Define Abstract Products
interface Button {
render(): void;
}
interface Checkbox {
render(): void;
}
2. Concrete Products for Windows
class WindowsButton implements Button {
render() {
console.log("Rendering a Windows button");
}
}
class WindowsCheckbox implements Checkbox {
render() {
console.log("Rendering a Windows checkbox");
}
}
3. Concrete Products for MacOS
class MacOSButton implements Button {
render() {
console.log("Rendering a MacOS button");
}
}
class MacOSCheckbox implements Checkbox {
render() {
console.log("Rendering a MacOS checkbox");
}
}
4. Define Abstract Factory
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
5. Concrete Factories
class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
class MacOSFactory implements GUIFactory {
createButton(): Button {
return new MacOSButton();
}
createCheckbox(): Checkbox {
return new MacOSCheckbox();
}
}
6. Client Code
class Application {
private button: Button;
private checkbox: Checkbox;
constructor(factory: GUIFactory) {
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
renderUI() {
this.button.render();
this.checkbox.render();
}
}
// Usage
const windowsFactory = new WindowsFactory();
const macOSFactory = new MacOSFactory();
const appForWindows = new Application(windowsFactory);
appForWindows.renderUI();
const appForMacOS = new Application(macOSFactory);
appForMacOS.renderUI();
How It Helps Improve Code
- Flexibility: You can easily swap out
WindowsFactory
forMacOSFactory
without modifying theApplication
class. This allows for easier maintenance and scalability. - Consistency: By using the factory, you ensure that
Button
andCheckbox
objects are compatible and work well together, as they are created by the same factory. - Separation of Concerns: The
Application
class focuses on its core functionality and delegates object creation to the factory, adhering to the Single Responsibility Principle.
How it is different from factory pattern
The Abstract Factory pattern and the Factory Method pattern are both creational design patterns, but they serve different purposes and are used in different scenarios. Here’s a breakdown of their differences:
Factory Method Pattern
Purpose:
- Provides a method for creating objects without specifying the exact class of the object that will be created. It allows a class to delegate the instantiation of objects to its subclasses.
Key Characteristics:
- Defines an interface for creating an object but allows subclasses to alter the type of objects that will be created.
- Typically, there is a single factory method for creating one type of product.
Usage:
- Used when a class can’t anticipate the class of objects it must create.
- Useful when subclasses are responsible for instantiating specific types of objects.
Example:
// Product interface
interface Product {
operation(): string;
}
// Concrete Products
class ConcreteProductA implements Product {
operation() {
return "Operation A";
}
}
class ConcreteProductB implements Product {
operation() {
return "Operation B";
}
}
// Creator
abstract class Creator {
abstract factoryMethod(): Product;
someOperation(): string {
const product = this.factoryMethod();
return `Creator: The same creator's code just worked with ${product.operation()}`;
}
}
// Concrete Creators
class ConcreteCreatorA extends Creator {
factoryMethod(): Product {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
factoryMethod(): Product {
return new ConcreteProductB();
}
}
// Usage
const creatorA = new ConcreteCreatorA();
console.log(creatorA.someOperation()); // "Creator: The same creator's code just worked with Operation A"
const creatorB = new ConcreteCreatorB();
console.log(creatorB.someOperation()); // "Creator: The same creator's code just worked with Operation B"
Abstract Factory Pattern
Purpose:
- Provides an interface for creating families of related or dependent objects without specifying their concrete classes. It allows a client to create objects from a particular family of products.
Key Characteristics:
- Defines an abstract factory interface with multiple factory methods to create different types of products.
- Often used when there are multiple families of products and you want to ensure that products from one family are used together.
Usage:
- Used when a system needs to work with multiple families of related objects and you want to ensure that objects from one family are used together.
- Helps manage dependencies between related objects and enforces consistency.
Thanks for reading