Introduction to the Abstract Factory Pattern

Welcome back! You’ve already explored the power of the Factory Method Pattern and how it promotes flexibility in your code design. Today, we are moving a step further by diving into the Abstract Factory Pattern. This pattern will help you create families of related objects without specifying their concrete classes.

Understanding the Abstract Factory Pattern

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. This pattern is particularly useful when you need to ensure that a set of related objects is created together, maintaining consistency across your application.

To understand this better, let's consider a scenario in which you are developing a graphical user interface (GUI) toolkit. Your toolkit should support multiple operating systems, such as Windows and Mac. Each OS has its own set of UI components, like buttons and checkboxes. Using the Abstract Factory Pattern, you can define interfaces for these components and create their concrete implementations for each OS.

Factory Method vs. Abstract Factory: What’s the Difference?

You might be wondering how the Abstract Factory Pattern differs from the Factory Method Pattern you learned earlier. Here’s a quick comparison to clarify:

  • Factory Method Pattern: Defines an interface for creating a single product, but lets subclasses decide which concrete class to instantiate. It focuses on creating one type of object.

    • Example: A Dialog class that creates a Button using a factory method. Subclasses like WindowsDialog or MacDialog override the method to return a specific Button implementation.
  • Abstract Factory Pattern: Provides an interface for creating families of related or dependent products (not just one), without specifying their concrete classes. It ensures that products created together are compatible.

    • Example: A GUIFactory that can create both Button and Checkbox objects, ensuring that both are from the same family (e.g., both Windows-style or both Mac-style).

In summary:

  • Use Factory Method when you need to create one product and let subclasses decide which one.
  • Use Abstract Factory when you need to create families of related products and ensure their compatibility.
Why Use Interfaces (and Not Abstract Classes) Here?

TypeScript provides both interfaces and abstract classes to define contracts for your code. In this lesson, we use interfaces for product and factory definitions because:

  • Interfaces are ideal for describing the shape of objects and are purely about defining contracts—what methods and properties a class must implement—without any implementation details.
  • Abstract classes can provide both a contract (abstract methods) and some shared implementation, but they cannot be implemented by plain objects and are less flexible for multiple inheritance.

For design patterns like Abstract Factory, interfaces are preferred when you only need to specify what methods must exist, and you don’t need to share any code between implementations. This keeps your code flexible and focused on the contract, which is especially useful when defining families of related products and factories.

If you need to provide some shared logic or default behavior, you could use an abstract class instead. However, in most cases for creational patterns in TypeScript, interfaces are sufficient and more idiomatic.

Defining Abstract Product Interfaces

TypeScript provides built-in support for interfaces and abstract classes, allowing you to define clear contracts for your components. Here’s how you can define abstract product interfaces for UI components:

// Abstract Product A
interface Button {
  paint(): void;
}

// Abstract Product B
interface Checkbox {
  paint(): void;
}

In this setup, the Button and Checkbox interfaces declare the paint method, which must be implemented by all concrete classes.

Creating Concrete Product Implementations

Now, let's create concrete implementations of these products for Windows and Mac. In TypeScript, you use the implements keyword to ensure that classes adhere to the interface contracts:

// Concrete Product A1
class WinButton implements Button {
  paint(): void {
    console.log("Rendering a button in a Windows style.");
  }
}

// Concrete Product A2
class MacButton implements Button {
  paint(): void {
    console.log("Rendering a button in a Mac style.");
  }
}

// Concrete Product B1
class WinCheckbox implements Checkbox {
  paint(): void {
    console.log("Rendering a checkbox in a Windows style.");
  }
}

// Concrete Product B2
class MacCheckbox implements Checkbox {
  paint(): void {
    console.log("Rendering a checkbox in a Mac style.");
  }
}

Each concrete class implements the appropriate interface and provides its own version of the paint method.

Defining the Abstract Factory Interface

TypeScript allows you to define abstract factories using interfaces or abstract classes. Here, we use an interface to specify the factory contract:

// Abstract Factory
interface GUIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

The GUIFactory interface declares methods for creating buttons and checkboxes. Concrete factories will implement these methods to produce specific products.

Creating Concrete Factory Implementations

Concrete factories implement the GUIFactory interface and instantiate the corresponding concrete products. TypeScript’s type system ensures that the correct types are returned:

// Concrete Factory 1
class WinFactory implements GUIFactory {
  createButton(): Button {
    return new WinButton();
  }

  createCheckbox(): Checkbox {
    return new WinCheckbox();
  }
}

// Concrete Factory 2
class MacFactory implements GUIFactory {
  createButton(): Button {
    return new MacButton();
  }

  createCheckbox(): Checkbox {
    return new MacCheckbox();
  }
}

WinFactory creates Windows-specific products, while MacFactory creates Mac-specific components. This results in the consistent creation of related objects without referring to their concrete classes.

Implementing the Client Code

The client code interacts with the abstract factory to create and use the products. TypeScript’s type annotations help ensure type safety throughout the process:

// Client code
class Application {
  private button: Button;
  private checkbox: Checkbox;

  constructor(private factory: GUIFactory) {
    this.button = this.factory.createButton();
    this.checkbox = this.factory.createCheckbox();
  }

  paint(): void {
    this.button.paint();
    this.checkbox.paint();
  }
}

let osType: string = "Windows";  // Change to "Mac" to see Mac UI
let factory: GUIFactory | null = null;

if (osType === "Windows") {
  factory = new WinFactory();
} else if (osType === "Mac") {
  factory = new MacFactory();
}

if (factory) {
  const app = new Application(factory);
  app.paint();
  // Output:
  // Rendering a button in a Windows style.
  // Rendering a checkbox in a Windows style.
} else {
  console.log("Unknown OS type.");
}

The Application class demonstrates client operations with abstract interfaces, promoting flexibility and scalability.

Code Overview

Here is the complete code in TypeScript:

// Abstract Product A
interface Button {
  paint(): void;
}

// Concrete Product A1
class WinButton implements Button {
  paint(): void {
    console.log("Rendering a button in a Windows style.");
  }
}

// Concrete Product A2
class MacButton implements Button {
  paint(): void {
    console.log("Rendering a button in a Mac style.");
  }
}

// Abstract Product B
interface Checkbox {
  paint(): void;
}

// Concrete Product B1
class WinCheckbox implements Checkbox {
  paint(): void {
    console.log("Rendering a checkbox in a Windows style.");
  }
}

// Concrete Product B2
class MacCheckbox implements Checkbox {
  paint(): void {
    console.log("Rendering a checkbox in a Mac style.");
  }
}

// Abstract Factory
interface GUIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

// Concrete Factory 1
class WinFactory implements GUIFactory {
  createButton(): Button {
    return new WinButton();
  }

  createCheckbox(): Checkbox {
    return new WinCheckbox();
  }
}

// Concrete Factory 2
class MacFactory implements GUIFactory {
  createButton(): Button {
    return new MacButton();
  }

  createCheckbox(): Checkbox {
    return new MacCheckbox();
  }
}

// Client code
class Application {
  private button: Button;
  private checkbox: Checkbox;

  constructor(private factory: GUIFactory) {
    this.button = this.factory.createButton();
    this.checkbox = this.factory.createCheckbox();
  }

  paint(): void {
    this.button.paint();
    this.checkbox.paint();
  }
}

let osType: string = "Windows";  // Change to "Mac" to see Mac UI
let factory: GUIFactory | null = null;

if (osType === "Windows") {
  factory = new WinFactory();
} else if (osType === "Mac") {
  factory = new MacFactory();
}

if (factory) {
  const app = new Application(factory);
  app.paint();
  // Output:
  // Rendering a button in a Windows style.
  // Rendering a checkbox in a Windows style.
} else {
  console.log("Unknown OS type.");
}

This example illustrates the Abstract Factory Pattern in a TypeScript environment, allowing for the dynamic creation and management of families of related objects with strong type safety and clear contracts.

Summary

The Abstract Factory Pattern is a powerful creational design pattern that enables you to create families of related objects without specifying their concrete classes. In TypeScript, interfaces are typically used to define the contracts for products and factories, keeping your code flexible and focused on what needs to be implemented, rather than how. This approach ensures that your application can easily support multiple product families (such as Windows and Mac UI components) while maintaining consistency and type safety. By separating the creation logic from the client code, the Abstract Factory Pattern promotes scalability, maintainability, and clear code organization.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal