Abstract Factory Design Pattern in TypeScript (Part 2): Participants, Consequences & Implementation

Hooman Momtaheni
6 min readMay 11, 2024

--

In the first part, I discussed the intent, motivation, and applicability of the abstract factory design pattern, and in this (last) section, I describe its participants, consequences, and implementation. As with previous postings in this series, the major reference is the book “Design Patterns: Elements of Reusable Object-Oriented Software”.

Structure

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

Participants

· AbstractFactory (WidgetFactory) declares an interface for operations that create abstract product objects.

· ConcreteFactory (MotifWidgetFactory, PMWidgetFactory) implements the operations to create concrete product objects.

· AbstractProduct (Window, ScrollBar) declares an interface for a type of product object.

· ConcreteProduct (MotifWindow, MotifScrollBar) defines a product object to be created by the corresponding concrete factory and implements the AbstractProduct interface.

· Client uses only interfaces declared by AbstractFactory and AbstractProduct classes.

Collaborations

· Normally a single instance of a ConcreteFactory class is created at run-time. This concrete factory creates product objects having a particular implementation. To create different product objects, clients should use a different concrete factory.

const motifFactory = new MotifWidgetFactory();
const PMFactory = new PMWidgetFactory();

const appWithMotif = new Application(motifFactory);
const appWithPM = new Application(PMFactory);

· AbstractFactory defers creation of product objects to its ConcreteFactory subclass.

abstract class WidgetFactory {
abstract createWindow(): Window;
abstract createScrollBar(): ScrollBar;
}

class PMWidgetFactory extends WidgetFactory {
createWindow(): Window {
return new PMWindow();
}

createScrollBar(): ScrollBar {
return new PMScrollBar();
}
}

Consequences

The Abstract Factory pattern has the following benefits and liabilities:

1.It isolates concrete classes. The Abstract Factory pattern helps you control the classes of objects that an application creates. Because a factory encapsulates the responsibility and the process of creating product objects, it isolates clients from implementation classes. Clients manipulate instances through their abstract interfaces. Product class names (In our example MotifWindow, MotifScrollBar, PMWindow, PMScrollBar) are isolated in the implementation of the concrete factory; they do not appear in client code.

2. It makes exchanging product families easy. The class of a concrete factory appears only once in an application — that is, where it’s instantiated. This makes it easy to change the concrete factory an application uses. It can use different product configurations simply by changing the concrete factory. Because an abstract factory creates a complete family of products, the whole product family changes at once. Example:

const motifFactory = new MotifWidgetFactory();
const PMFactory = new PMWidgetFactory();

3. It promotes consistency among products. When product objects in a family are designed to work together, it’s important that an application use objects from only one family at a time. AbstractFactory makes this easy to enforce.

4. Supporting new kinds of products is difficult. Extending abstract factories to produce new kinds of Products isn’t easy. That’s because the AbstractFactory interface fixes the set of products that can be created. Supporting new kinds of products requires extending the factory interface, which involves changing the AbstractFactory class and all of its subclasses. We discuss one solution to this problem in the Implementation section. In our example, if we want to add ‘dialog box’ to UI, we should change abstract WidgetFactory and all its subclasses (MotifWidgetFactory, PMWidgetFactory) and clearly it violates Open/Close principle

Implementation

Here are some useful techniques for implementing the Abstract Factory pattern.

1.Factories as singletons. An application typically needs only one instance of a ConcreteFactory per product family. So, it’s usually best implemented as a Singleton.

class MotifWidgetFactory extends WidgetFactory {
private static instance: MotifWidgetFactory | null = null;

// Private constructor to prevent instantiation from outside
private constructor() {
super();
}

// Static method to get the singleton instance
public static getInstance(): MotifWidgetFactory {
if (!MotifWidgetFactory.instance) {
MotifWidgetFactory.instance = new MotifWidgetFactory();
}
return MotifWidgetFactory.instance;
}

createWindow(): Window {
return new MotifWindow();
}

createScrollBar(): ScrollBar {
return new MotifScrollBar();
}
}

2. Creating the products. AbstractFactory only declares an interface for creating products. It’s up to ConcreteProduct subclasses to actually create them. The most common way to do this is to define Factory Method for each product. A concrete factory will specify its products by overriding the factory method for each. While this implementation is simple, it requires a new concrete factory subclass for each family, even if the families differ only slightly.

If many product families are possible, the concrete factory can be implemented using the Prototype pattern. The concrete factory is initialized with a prototypical instance of each product in the family, and it creates a new product by cloning its prototype.

3. Defining extensible factories. AbstractFactory usually defines a different operation for each kind of product it can produce. The kinds of products are encoded in the operation signatures (Operation signatures typically refer to the definitions of methods within a class or an abstract class or interface. These signatures include the method’s name, parameters, return type, and potentially any exceptions or errors it might throw i.e., accelerate(speed: number): void;). Adding a new kind of product requires changing the AbstractFactory interface and all the classes that depend on it.

A more flexible but less safe design is to add a parameter to operations that create objects. This parameter specifies the kind of object to be created. It could be a class identifier, an integer, a string, or anything else that identifies the kind of product. In fact, with this approach, AbstractFactory only needs a single “make” operation with a parameter indicating the kind of object to create. This is the technique used in the Prototype and the class-based abstract factories discussed earlier.

abstract class Widget{
abstract render(): void;
}

// Abstract widget classes
abstract class Window extends Widget{
abstract render(): void;
}

abstract class ScrollBar extends Widget{
abstract render(): void;
}

// Abstract factory class
abstract class WidgetFactory {
abstract make(type: string): Widget;
}

// Concrete implementations for Motif look-and-feel
class MotifWindow extends Window {
render() {
console.log("Rendering Motif window");
// Render Motif window implementation
}
}

class MotifScrollBar extends ScrollBar {
render() {
console.log("Rendering Motif scrollbar");
// Render Motif scrollbar implementation
}
}

class MotifWidgetFactory extends WidgetFactory {
make(type: string): Widget {
if (type === "Window") {
return new MotifWindow();
} else if (type === "ScrollBar") {
return new MotifScrollBar();
} else {
throw new Error("Invalid type for MotifWidgetFactory");
}
}
}

// Client code
class Application {
private widgetFactory: WidgetFactory;

constructor(widgetFactory: WidgetFactory) {
this.widgetFactory = widgetFactory;
}

createUI(type: string) {
const widget = this.widgetFactory.make(type);
widget.render();
}
}

// Example usage
const motifFactory = new MotifWidgetFactory();

const appWithMotif = new Application(motifFactory);
appWithMotif.createUI("Window");

This variation is easier to use in a dynamically typed language like JavaScript than in a statically typed language like TypeScript. You can use it in TypeScript only when all objects have the same abstract base class (in our example Widget class) or when the product objects can be safely coerced (converting a value from one type to another implicitly or explicitly) to the correct type by the client that requested them.

But even when no coercion is needed, an inherent problem remains: All products are returned to the client with the same abstract interface as given by the return type. The client will not be able to differentiate or make safe assumptions about the class of a product (Window or ScrollBar). If clients need to perform subclass-specific operations, they won’t be accessible through the abstract interface. Although the client could perform a downcast* that’s not always feasible or safe, because the downcast can fail. This is the classic trade-off for a highly flexible and extensible interface.

Downcast: Downcasting refers to the process of converting a reference of a base class to one of its derived classes.

Related Patterns

AbstractFactory classes are often implemented with Factory Method, but they can also be implemented using Prototype.

A concrete factory is often a Singleton.

--

--

Hooman Momtaheni

Full Stack Web Application Developer | Admire foundations and structures | Helping companies have reliable web apps and motivated software development team