Cross-Platform UI Toolkit
One factory per platform, each producing a consistent family of widgets. Abstract Factory guarantees you never mix a Windows button with a Mac checkbox.
Key Abstractions
Abstract factory defining create methods for each widget type in the family
Common interface for all UI components with render() and clone() methods
Concrete factories producing platform-consistent widget families
Prototype registry storing pre-configured widget templates for cloning
Class Diagram
The Key Insight
A cross-platform UI toolkit has a consistency problem. You need buttons, text fields, and checkboxes, but each platform renders them differently. Windows draws beveled edges. Mac uses rounded Aqua controls. The web emits HTML tags. If you scatter platform checks across your codebase, every time you add a widget or a platform, you are editing dozens of files.
Abstract Factory solves this by grouping creation behind a single interface. You pick the factory once at application startup. From that point forward, every widget produced through it belongs to the same platform family. A WindowsFactory will never hand you a MacCheckbox. The type system enforces that guarantee.
But there is a second problem. Teams build the same styled button over and over: primary blue, border radius 4, font size 14. Prototype pattern handles this. You configure a widget once, store it as a template, and clone it whenever you need another. No repeated construction logic. And because the clone is a deep copy, modifying it leaves the original template untouched.
Requirements
Functional
- Create buttons, text fields, and checkboxes for Windows, Mac, and Web platforms
- A factory produces a full family of widgets for a single platform
- Client code works with abstract Widget and UIFactory types only
- Support a prototype registry that stores pre-configured widget templates
- Cloning a template returns an independent copy that can be customized without affecting the original
Non-Functional
- Adding a new platform requires only one new factory class, no changes to existing code
- Adding a new widget type means updating the factory interface and all concrete factories
- Widget rendering is stateless and safe for concurrent use
- Clone operations produce deep copies so mutations never leak between instances
Design Decisions
Why Abstract Factory over scattered if-else checks?
If you check if platform == "windows" every time you create a button, a text field, or a checkbox, that platform check is scattered across your codebase. Thirty creation points means thirty conditionals. Abstract Factory centralizes the decision. You pick the factory once at startup, and everything created through it is consistent for that platform. When someone asks for Linux support, you write one new factory class. You do not hunt down every conditional in the application.
Why Prototype for widget templates instead of a builder?
Builder constructs objects step by step. That works well when the construction process varies between instances. But for UI templates, the configuration is already decided upfront: font size 14, color blue, border radius 4. The widget is fully formed. Cloning a finished object is faster and simpler than replaying a build sequence. You store the result once and copy it whenever you need a variant.
Why a separate Widget interface instead of tying client code to concrete classes?
The form-building code calls factory.create_button() and gets back a Button. It calls button.render() without knowing whether that is a WindowsButton or a MacButton. This decoupling is the whole point. The same form code runs on any platform just by swapping the factory at the top level. No conditionals, no platform-specific imports in the form logic.
Why does the factory return abstract types, not concrete ones?
If create_button() returned WindowsButton, calling code would import and depend on Windows-specific classes. That defeats the purpose. Returning Button (the abstract product) keeps client code platform-agnostic. The concrete type still exists at runtime, but the caller never sees it. This is what makes platform swapping a one-line change rather than a codebase-wide refactor.
Interview Follow-ups
- "How would you add a new platform like Linux?" Write a
LinuxFactorythat implementsUIFactory, plusLinuxButton,LinuxTextField, andLinuxCheckbox. No existing factory or client code changes. PassLinuxFactoryat startup and everything works. - "What happens when you need to add a new widget type, say a Dropdown?" You add
create_dropdown()to theUIFactoryinterface. Every concrete factory must implement it. This is the trade-off of Abstract Factory: adding a new product to the family touches all factories. In practice this is manageable because factories are small, focused classes. - "How would you handle theming on top of platform rendering?" Decorator pattern. Wrap any widget in a
ThemedWidgetthat adjusts colors or fonts before delegating to the inner widget'srender(). The factory keeps producing base widgets; theming is layered on top without modifying the factory hierarchy. - "Could you lazy-load platform factories to avoid bundling all platform code?" Yes. Use a factory map keyed by platform string, and load the concrete factory module on demand. The client code still programs against
UIFactory. The only difference is that the factory instance arrives through dynamic import rather than static construction.
Code Implementation
from abc import ABC, abstractmethod
import copy
# ── Widget interface ──────────────────────────────────────────────
class Widget(ABC):
"""Base interface for all UI components."""
@abstractmethod
def render(self) -> str: ...
def clone(self) -> "Widget":
return copy.deepcopy(self)
# ── Abstract product types ────────────────────────────────────────
class Button(Widget, ABC):
def __init__(self, label: str = "Button"):
self.label = label
class TextField(Widget, ABC):
def __init__(self, placeholder: str = "Enter text..."):
self.placeholder = placeholder
class Checkbox(Widget, ABC):
def __init__(self, label: str = "Option", checked: bool = False):
self.label = label
self.checked = checked
# ── Windows widgets ───────────────────────────────────────────────
class WindowsButton(Button):
def render(self) -> str:
return f"[Windows Button: '{self.label}']"
class WindowsTextField(TextField):
def render(self) -> str:
return f"[Windows TextField: '{self.placeholder}']"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
state = "x" if self.checked else " "
return f"[Windows Checkbox: [{state}] {self.label}]"
# ── Mac widgets ───────────────────────────────────────────────────
class MacButton(Button):
def render(self) -> str:
return f"(Mac Button: '{self.label}')"
class MacTextField(TextField):
def render(self) -> str:
return f"(Mac TextField: '{self.placeholder}')"
class MacCheckbox(Checkbox):
def render(self) -> str:
state = "x" if self.checked else " "
return f"(Mac Checkbox: [{state}] {self.label})"
# ── Web widgets ───────────────────────────────────────────────────
class WebButton(Button):
def render(self) -> str:
return f"<button>{self.label}</button>"
class WebTextField(TextField):
def render(self) -> str:
return f'<input placeholder="{self.placeholder}" />'
class WebCheckbox(Checkbox):
def render(self) -> str:
checked_attr = " checked" if self.checked else ""
return f"<input type='checkbox'{checked_attr} /> {self.label}"
# ── Abstract Factory ──────────────────────────────────────────────
class UIFactory(ABC):
"""One factory per platform. Each produces a full family of widgets."""
@abstractmethod
def create_button(self, label: str = "Button") -> Button: ...
@abstractmethod
def create_text_field(self, placeholder: str = "Enter text...") -> TextField: ...
@abstractmethod
def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox: ...
class WindowsFactory(UIFactory):
def create_button(self, label: str = "Button") -> Button:
return WindowsButton(label)
def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
return WindowsTextField(placeholder)
def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
return WindowsCheckbox(label, checked)
class MacFactory(UIFactory):
def create_button(self, label: str = "Button") -> Button:
return MacButton(label)
def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
return MacTextField(placeholder)
def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
return MacCheckbox(label, checked)
class WebFactory(UIFactory):
def create_button(self, label: str = "Button") -> Button:
return WebButton(label)
def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
return WebTextField(placeholder)
def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
return WebCheckbox(label, checked)
# ── Prototype Registry ────────────────────────────────────────────
class WidgetRegistry:
"""Stores pre-configured widget templates. Clone instead of rebuild."""
def __init__(self):
self._templates: dict[str, Widget] = {}
def register(self, name: str, widget: Widget) -> None:
self._templates[name] = widget
def create(self, name: str) -> Widget:
template = self._templates.get(name)
if template is None:
raise KeyError(f"No template registered under '{name}'")
return template.clone()
# ── Demo ──────────────────────────────────────────────────────────
def build_form(factory: UIFactory) -> list[Widget]:
"""Client code that works with any platform factory."""
return [
factory.create_button("Submit"),
factory.create_text_field("Your name"),
factory.create_checkbox("Agree to terms"),
]
if __name__ == "__main__":
# 1. Build a Windows form
print("=== Windows Form ===")
for widget in build_form(WindowsFactory()):
print(" " + widget.render())
# 2. Same form, Mac platform
print("\n=== Mac Form ===")
for widget in build_form(MacFactory()):
print(" " + widget.render())
# 3. Same form, Web platform
print("\n=== Web Form ===")
for widget in build_form(WebFactory()):
print(" " + widget.render())
# 4. Prototype registry: register a template, clone it twice
print("\n=== Prototype Registry ===")
registry = WidgetRegistry()
primary_btn = WindowsButton("Primary Action")
registry.register("primary-button", primary_btn)
clone_a = registry.create("primary-button")
clone_b = registry.create("primary-button")
# Customize clones independently
clone_a.label = "Save"
clone_b.label = "Cancel"
print(f" Clone A: {clone_a.render()}")
print(f" Clone B: {clone_b.render()}")
print(f" Original template: {primary_btn.render()}")
# 5. Prove clones are independent from the template
assert primary_btn.label == "Primary Action", "Template must not change"
assert clone_a.label == "Save"
assert clone_b.label == "Cancel"
print("\nAll assertions passed. Clones are independent of the template.")Common Mistakes
- ✗Using if-else on a platform string to create widgets. That spreads platform logic across every creation point.
- ✗Adding a new widget type to the factory interface without updating all concrete factories. All factories must implement the full product family.
- ✗Making prototypes mutable after cloning. The registry's template should not change when a clone is modified.
- ✗Confusing Abstract Factory with Factory Method. Abstract Factory creates families of related objects. Factory Method creates one object and lets subclasses decide the type.
Key Points
- ✓Abstract Factory ensures platform consistency. One factory, one platform, no mixing.
- ✓Adding a new platform means one new factory class. Existing code stays untouched.
- ✓Prototype avoids repeating complex widget configuration. Clone a template and customize.
- ✓Client code depends on UIFactory and Widget interfaces, never on platform-specific classes.