Abstract Factory Pattern
Overview
A light-themed button and a dark-themed checkbox make an inconsistent dialog. The choice concerns a set of parts that belong together, not just how to construct one button.
Abstract Factory groups the operations that create those related products. A screen asks its factory for the parts instead of choosing their concrete classes.
UML
Dialog requests a button and a checkbox from IWidgetFactory. LightFactory creates the light-themed pair, and the screen uses each product through Draw().
History
GoF cataloged Abstract Factory as a creational pattern in Design Patterns, published in 1994. Supporting different user-interface families is one of its motivating situations.1
Why use it?
Separate theme checks in every screen spread the same selection rule. Choose a factory when assembling the screen, then request the parts the screen needs.
Adding a new product kind changes the factory contract and each family implementation. This fits changing families better than frequently changing the set of product kinds.
Example
Build one dialog with the light factory. The products return names instead of drawing real GUI controls.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(new Dialog(new LightFactory()).Render());
interface IButton { string Draw(); }
interface ICheckbox { string Draw(); }
// Keep theme selection out of the dialog and behind a product-family contract.
interface IWidgetFactory
{
IButton Button();
ICheckbox Checkbox();
}
sealed class LightButton : IButton { public string Draw() => "light button"; }
sealed class LightCheckbox : ICheckbox { public string Draw() => "light checkbox"; }
sealed class DarkButton : IButton { public string Draw() => "dark button"; }
sealed class DarkCheckbox : ICheckbox { public string Draw() => "dark checkbox"; }
sealed class LightFactory : IWidgetFactory
{
public IButton Button() => new LightButton();
public ICheckbox Checkbox() => new LightCheckbox();
}
sealed class DarkFactory : IWidgetFactory
{
public IButton Button() => new DarkButton();
public ICheckbox Checkbox() => new DarkCheckbox();
}
// The dialog uses selected products without constructing concrete theme classes.
sealed class Dialog(IWidgetFactory factory)
{
public string Render() => factory.Button().Draw() + " + " + factory.Checkbox().Draw();
}
light button + light checkboxDialog never constructs LightButton directly. The factory chooses the concrete button and checkbox.
An extended example
Pass two factories to the same Dialog in turn. The screen code stays the same while the related products change together.
using System;
using System.Collections.Generic;
using System.Linq;
foreach (IWidgetFactory factory in new IWidgetFactory[] { new LightFactory(), new DarkFactory() })
Console.WriteLine(new Dialog(factory).Render());
interface IButton { string Draw(); }
interface ICheckbox { string Draw(); }
// Keep theme selection out of the dialog and behind a product-family contract.
interface IWidgetFactory
{
IButton Button();
ICheckbox Checkbox();
}
sealed class LightButton : IButton { public string Draw() => "light button"; }
sealed class LightCheckbox : ICheckbox { public string Draw() => "light checkbox"; }
sealed class DarkButton : IButton { public string Draw() => "dark button"; }
sealed class DarkCheckbox : ICheckbox { public string Draw() => "dark checkbox"; }
sealed class LightFactory : IWidgetFactory
{
public IButton Button() => new LightButton();
public ICheckbox Checkbox() => new LightCheckbox();
}
sealed class DarkFactory : IWidgetFactory
{
public IButton Button() => new DarkButton();
public ICheckbox Checkbox() => new DarkCheckbox();
}
// The dialog uses selected products without constructing concrete theme classes.
sealed class Dialog(IWidgetFactory factory)
{
public string Render() => factory.Button().Draw() + " + " + factory.Checkbox().Draw();
}
light button + light checkbox
dark button + dark checkboxDarkFactory implements the same creation contract. Theme consistency remains a responsibility of each factory implementation; the interface alone does not detect a factory that accidentally mixes themes.
TypeScript example
If Dialog chooses themes and draws every product, another theme also changes the dialog. IWidgetFactory selects a related set of products; Dialog only uses the products it receives.
interface IButton { draw(): string; }
interface ICheckbox { draw(): string; }
// Keep theme selection out of the dialog and behind a product-family contract.
interface IWidgetFactory {
button(): IButton;
checkbox(): ICheckbox;
}
class LightButton implements IButton { draw(): string { return "light button"; } }
class LightCheckbox implements ICheckbox { draw(): string { return "light checkbox"; } }
class DarkButton implements IButton { draw(): string { return "dark button"; } }
class DarkCheckbox implements ICheckbox { draw(): string { return "dark checkbox"; } }
class LightFactory implements IWidgetFactory {
button(): IButton { return new LightButton(); }
checkbox(): ICheckbox { return new LightCheckbox(); }
}
class DarkFactory implements IWidgetFactory {
button(): IButton { return new DarkButton(); }
checkbox(): ICheckbox { return new DarkCheckbox(); }
}
// The dialog uses selected products without constructing concrete theme classes.
class Dialog {
constructor(private readonly factory: IWidgetFactory) {}
render(): string {
return this.factory.button().draw() + " + " + this.factory.checkbox().draw();
}
}
for (const factory of [new LightFactory(), new DarkFactory()]) {
console.log(new Dialog(factory).render());
}light button + light checkbox
dark button + dark checkboxThe TypeScript interfaces express the same roles as the C# interfaces. Each factory implementation remains responsible for returning a matching product family.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Creational patterns, Abstract Factory. β©