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.
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 DETAILSAdvantages 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.