Skip to main content

Real World Implementation Of Abstract Factory Pattern

Suppose you're designing a framework that should work across multiple platforms (e.g., Windows and MacOS), and each platform has a different set of UI elements (like buttons, checkboxes, etc.). Each platform's UI elements behave differently and have different appearances, but the actual functionality of these elements (clicking a button, checking a checkbox, etc.) is essentially the same.

Real World Implementation

The elements in the user interface can include buttons, menus, control sticks, etc. In this scenario, you could use the Abstract Factory pattern to ensure that for each platform, the right type of UI elements are created without tying your code to the specific classes of each platform's UI elements.

Here's an implementation of this in TypeScript:

interface Button {
render(): void;

onClick(f: Function): void;
}

interface Checkbox {
render(): void;

toggle(): void;
}

interface GUIFactory {
createButton(): Button;

createCheckbox(button: Button): Checkbox;
}

class WindowsButton implements Button {
render() {
console.log("Render a button in Windows style");
}

onClick(f: Function) {
console.log("Bind a Windows style button click event");
f();
}
}

class WindowsCheckbox implements Checkbox {
private button: Button;

constructor(button: Button) {
this.button = button;
}

render() {
console.log("Render a checkbox in Windows style");
}

toggle() {
this.button.onClick(() => console.log("Checkbox state toggled!"));
}
}

class MacOSButton implements Button {
render() {
console.log("Render a button in MacOS style");
}

onClick(f: Function) {
console.log("Bind a MacOS style button click event");
f();
}
}

class MacOSCheckbox implements Checkbox {
private button: Button;

constructor(button: Button) {
this.button = button;
}

render() {
console.log("Render a checkbox in MacOS style");
}

toggle() {
this.button.onClick(() => console.log("Checkbox state toggled!"));
}
}

class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}

createCheckbox(button: Button): Checkbox {
return new WindowsCheckbox(button);
}
}

class MacOSFactory implements GUIFactory {
createButton(): Button {
return new MacOSButton();
}

createCheckbox(button: Button): Checkbox {
return new MacOSCheckbox(button);
}
}

function renderUI(factory: GUIFactory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox(button);

button.render();
checkbox.render();

button.onClick(() => console.log("Button clicked!"));
checkbox.toggle();
}

console.log("App: Launched with the Windows factory.");
renderUI(new WindowsFactory());

console.log("App: Launched with the MacOS factory.");
renderUI(new MacOSFactory());

The Checkbox depends on a Button, and the GUIFactory's createCheckbox method now requires a Button parameter. The Checkbox's toggle method calls the Button's onClick method to change its state. The MacOSButton and MacOSCheckbox have the same behavior as the WindowsButton and WindowsCheckbox, just with different render outputs to simulate a different platform. This demonstrates the Abstract Factory pattern in a scenario where the products have interdependencies.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

Advantages Of Abstract Factory

The Abstract Factory pattern provides several advantages:

Consistency among products

Abstract factory ensures that the products being created are all compatible with each other and belong to the same family. This is helpful in situations where you need to enforce some constraints or business rules regarding which products can be used together.

In the provided example, the WindowsFactory and MacOSFactory always create products (Button and Checkbox) that are consistent with each other (either both Windows or both MacOS).

class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}

createCheckbox(button: Button): Checkbox {
return new WindowsCheckbox(button);
}
}

class MacOSFactory implements GUIFactory {
createButton(): Button {
return new MacOSButton();
}
createCheckbox(button: Button): Checkbox {
return new MacOSCheckbox(button);
}
}

Avoiding concrete product classes

This pattern allows a program to use the interfaces of products instead of the concrete classes. This can help isolate the client code from the concrete product classes, and can reduce the coupling between the different parts of the program.

In the client code, we only depend on the abstract Button and Checkbox interfaces, not their concrete implementations.

function renderUI(factory: GUIFactory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox(button);

button.render();
checkbox.render();

button.onClick(() => console.log("Button clicked!"));
checkbox.toggle();
}

Code reusability and swapping product families

The Abstract Factory pattern allows for easy swapping of product families, enhancing code reusability. For instance, we can easily switch from WindowsFactory to MacOSFactory in the client code, without having to change the client code itself.

console.log("App: Launched with the Windows factory.");
renderUI(new WindowsFactory());

console.log("App: Launched with the MacOS factory.");
renderUI(new MacOSFactory());

Single Responsibility Principle

Each concrete factory is only responsible for creating products of a single variant and hence it complies with the Single Responsibility Principle. This can make the code cleaner, easier to understand, and less likely to introduce bugs.

class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(button: Button): Checkbox {
return new WindowsCheckbox(button);
}
}

Open/Closed Principle

It's easy to introduce new factories and product families without changing the existing client code, which complies with the Open/Closed Principle. You can introduce a new GUI Factory, e.g., LinuxFactory, which will create Linux-style buttons and checkboxes without modifying the existing client code.

class LinuxFactory implements GUIFactory {
createButton(): Button {
return new LinuxButton();
}
createCheckbox(button: Button): Checkbox {
return new LinuxCheckbox(button);
}
}

In summary, the Abstract Factory pattern can offer a lot of flexibility and organization to your code, especially in complex systems with families of related products.

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.

YouTube @cloudaffle