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
1 from abc import ABC, abstractmethod
2 import copy
3
4
5 # ── Widget interface ──────────────────────────────────────────────
6
7 class Widget(ABC):
8 """Base interface for all UI components."""
9
10 @abstractmethod
11 def render(self) -> str: ...
12
13 def clone(self) -> "Widget":
14 return copy.deepcopy(self)
15
16
17 # ── Abstract product types ────────────────────────────────────────
18
19 class Button(Widget, ABC):
20 def __init__(self, label: str = "Button"):
21 self.label = label
22
23 class TextField(Widget, ABC):
24 def __init__(self, placeholder: str = "Enter text..."):
25 self.placeholder = placeholder
26
27 class Checkbox(Widget, ABC):
28 def __init__(self, label: str = "Option", checked: bool = False):
29 self.label = label
30 self.checked = checked
31
32
33 # ── Windows widgets ───────────────────────────────────────────────
34
35 class WindowsButton(Button):
36 def render(self) -> str:
37 return f"[Windows Button: '{self.label}']"
38
39 class WindowsTextField(TextField):
40 def render(self) -> str:
41 return f"[Windows TextField: '{self.placeholder}']"
42
43 class WindowsCheckbox(Checkbox):
44 def render(self) -> str:
45 state = "x" if self.checked else " "
46 return f"[Windows Checkbox: [{state}] {self.label}]"
47
48
49 # ── Mac widgets ───────────────────────────────────────────────────
50
51 class MacButton(Button):
52 def render(self) -> str:
53 return f"(Mac Button: '{self.label}')"
54
55 class MacTextField(TextField):
56 def render(self) -> str:
57 return f"(Mac TextField: '{self.placeholder}')"
58
59 class MacCheckbox(Checkbox):
60 def render(self) -> str:
61 state = "x" if self.checked else " "
62 return f"(Mac Checkbox: [{state}] {self.label})"
63
64
65 # ── Web widgets ───────────────────────────────────────────────────
66
67 class WebButton(Button):
68 def render(self) -> str:
69 return f"<button>{self.label}</button>"
70
71 class WebTextField(TextField):
72 def render(self) -> str:
73 return f'<input placeholder="{self.placeholder}" />'
74
75 class WebCheckbox(Checkbox):
76 def render(self) -> str:
77 checked_attr = " checked" if self.checked else ""
78 return f"<input type='checkbox'{checked_attr} /> {self.label}"
79
80
81 # ── Abstract Factory ──────────────────────────────────────────────
82
83 class UIFactory(ABC):
84 """One factory per platform. Each produces a full family of widgets."""
85
86 @abstractmethod
87 def create_button(self, label: str = "Button") -> Button: ...
88
89 @abstractmethod
90 def create_text_field(self, placeholder: str = "Enter text...") -> TextField: ...
91
92 @abstractmethod
93 def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox: ...
94
95
96 class WindowsFactory(UIFactory):
97 def create_button(self, label: str = "Button") -> Button:
98 return WindowsButton(label)
99
100 def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
101 return WindowsTextField(placeholder)
102
103 def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
104 return WindowsCheckbox(label, checked)
105
106
107 class MacFactory(UIFactory):
108 def create_button(self, label: str = "Button") -> Button:
109 return MacButton(label)
110
111 def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
112 return MacTextField(placeholder)
113
114 def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
115 return MacCheckbox(label, checked)
116
117
118 class WebFactory(UIFactory):
119 def create_button(self, label: str = "Button") -> Button:
120 return WebButton(label)
121
122 def create_text_field(self, placeholder: str = "Enter text...") -> TextField:
123 return WebTextField(placeholder)
124
125 def create_checkbox(self, label: str = "Option", checked: bool = False) -> Checkbox:
126 return WebCheckbox(label, checked)
127
128
129 # ── Prototype Registry ────────────────────────────────────────────
130
131 class WidgetRegistry:
132 """Stores pre-configured widget templates. Clone instead of rebuild."""
133
134 def __init__(self):
135 self._templates: dict[str, Widget] = {}
136
137 def register(self, name: str, widget: Widget) -> None:
138 self._templates[name] = widget
139
140 def create(self, name: str) -> Widget:
141 template = self._templates.get(name)
142 if template is None:
143 raise KeyError(f"No template registered under '{name}'")
144 return template.clone()
145
146
147 # ── Demo ──────────────────────────────────────────────────────────
148
149 def build_form(factory: UIFactory) -> list[Widget]:
150 """Client code that works with any platform factory."""
151 return [
152 factory.create_button("Submit"),
153 factory.create_text_field("Your name"),
154 factory.create_checkbox("Agree to terms"),
155 ]
156
157
158 if __name__ == "__main__":
159 # 1. Build a Windows form
160 print("=== Windows Form ===")
161 for widget in build_form(WindowsFactory()):
162 print(" " + widget.render())
163
164 # 2. Same form, Mac platform
165 print("\n=== Mac Form ===")
166 for widget in build_form(MacFactory()):
167 print(" " + widget.render())
168
169 # 3. Same form, Web platform
170 print("\n=== Web Form ===")
171 for widget in build_form(WebFactory()):
172 print(" " + widget.render())
173
174 # 4. Prototype registry: register a template, clone it twice
175 print("\n=== Prototype Registry ===")
176 registry = WidgetRegistry()
177 primary_btn = WindowsButton("Primary Action")
178 registry.register("primary-button", primary_btn)
179
180 clone_a = registry.create("primary-button")
181 clone_b = registry.create("primary-button")
182
183 # Customize clones independently
184 clone_a.label = "Save"
185 clone_b.label = "Cancel"
186
187 print(f" Clone A: {clone_a.render()}")
188 print(f" Clone B: {clone_b.render()}")
189 print(f" Original template: {primary_btn.render()}")
190
191 # 5. Prove clones are independent from the template
192 assert primary_btn.label == "Primary Action", "Template must not change"
193 assert clone_a.label == "Save"
194 assert clone_b.label == "Cancel"
195 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.