如何用抽象工厂实现跨平台 UI 组件?
简化版
跨平台 UI 可以把 Button、Checkbox 等组件定义成抽象产品,再为 Windows、Mac 等平台定义具体工厂。客户端只依赖 UIFactory,切换具体工厂就能整体切换一套平台风格。
详细版
核心接口可以这样设计:
interface Button {
void render();
}
interface Checkbox {
void render();
}
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
Windows 平台实现:
class WindowsFactory implements UIFactory {
public Button createButton() {
return new WindowsButton();
}
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
客户端代码只拿 UIFactory:
class Dialog {
private final UIFactory factory;
Dialog(UIFactory factory) {
this.factory = factory;
}
void render() {
factory.createButton().render();
factory.createCheckbox().render();
}
}
这样应用启动时根据操作系统选择 WindowsFactory 或 MacFactory,业务层不用知道具体组件类。
完整版教学
一、先抽象产品等级
跨平台 UI 里,Button、Checkbox、Dialog 都可以是产品等级。每个产品等级定义自己的抽象接口,客户端只依赖这些接口。
interface Button {
void onClick();
}
具体产品再实现平台差异,比如 WindowsButton 使用 Windows 风格渲染,MacButton 使用 Mac 风格渲染。
二、再抽象产品族工厂
UIFactory 负责创建同一平台下的一组组件。WindowsFactory 创建 Windows 族组件,MacFactory 创建 Mac 族组件。
这个结构的关键是:客户端不分别选择 ButtonFactory 和 CheckboxFactory,而是选择一个平台工厂,避免组件风格被拆散。
三、客户端如何切换平台
平台选择通常放在应用启动配置中:
UIFactory factory = os.isMac() ? new MacFactory() : new WindowsFactory();
Dialog dialog = new Dialog(factory);
后续所有组件都从同一个工厂创建,风格自然保持一致。
四、新增平台和新增组件的代价不同
新增 Linux 平台时,新增 LinuxButton、LinuxCheckbox、LinuxFactory 即可,原有客户端和抽象接口不需要改。
但如果新增 Menu 组件,就要修改 UIFactory,并让 WindowsFactory、MacFactory、LinuxFactory 都实现 createMenu()。这是抽象工厂的典型代价。
五、客户端只选择一次平台而不是逐组件选择
应用启动时选择 WindowsFactory 后,Dialog 后续创建 Button、Checkbox 都不再读取操作系统分支。这样平台决策只出现一次,新增界面组件时也能检查每个平台是否提供同族实现。
Button Checkbox
Windows WindowsButton WindowsCheckbox
Mac MacButton MacCheckbox
启动配置 os=mac -> MacFactory
Dialog.createButton() -> MacButton
Dialog.createCheckbox() -> MacCheckbox
客户端只依赖 Button/Checkbox 接口
不出现 WindowsButton + MacCheckbox
矩阵的每一行是一族兼容产品,每一列是一个产品等级。客户端选择一行对应的具体工厂,再从该工厂取得各列产品;如果允许调用方绕过工厂分别选择每个具体类,产品族一致性的结构性保证就消失了。
六、二维扩展成本与模式边界
| 评审维度 | 本题结论 |
|---|---|
| 产品族不变量 | 一个 Dialog 构建周期内的组件全部来自同一 UIFactory。 |
| 新增一行(产品族) | 新增 Linux 组件一套和 LinuxFactory,旧客户端保持抽象依赖。 |
| 新增一列(产品等级) | 新增 Menu 需修改 UIFactory 和全部平台工厂。 |
| 不应由工厂承担 | 组件渲染状态、事件业务和页面布局。 |
| 退回更轻方案的条件 | 只有 Button 一种组件时工厂方法足够;平台原生组件不需切换时直接构造更简单。 |
抽象工厂对所有变化并非都开放。它刻意固定列集合,以换取整行切换的一致性;如果系统最常发生的是新增列,工厂接口和所有行实现会频繁联动,此时应拆分工厂边界或选择组合方式。
七、落地验收清单
- 为每个具体工厂跑同一组契约测试,确认每个创建方法都返回正确产品等级。
- 给产品暴露
familyId()仅用于测试,校验同一工厂创建的所有产品 familyId 一致。 - 加入一个假产品族,记录是否只新增一行实现和装配配置,而未修改旧工厂。
- 模拟新增一个产品等级,评估抽象接口和全部具体工厂的真实修改成本。
- 检查公共产品接口是否出现 AWS、MySQL、Windows 等具体实现词汇。
- 检查具体工厂是否只做组装与创建,没有吞入完整业务流程。
- 若产品持有连接或线程池,明确由工厂、容器还是调用方负责关闭。
- 若运行期按租户切换工厂,验证路由缓存、凭证刷新和租户隔离。
记忆钩子:平台只选一次,组件从同一工厂成套拿。
八、常见误区与追问
- 误区:客户端可以分别选择每个组件平台。 这会破坏抽象工厂要保证的产品族一致性。
- 误区:产品族是 Button 的所有实现。 那是产品等级;产品族是同平台的不同组件。
- 误区:新增 Linux 平台需要修改 WindowsFactory。 现有产品等级稳定时只需新增 Linux 一行。
- 追问:操作系统判断放哪里? 放应用组合根或配置层,避免散落在每个页面。
- 追问:组件间需要共享主题怎么办? 把不可变主题配置注入具体工厂,由同族产品共享。
- 追问:新增 Menu 为什么成本高? 它新增一列,所有平台工厂都必须履行新创建契约。
九、平台选择时序与一致性测试
平台判断应只出现在应用组合根,而不是每个 Dialog 或组件内部。启动时根据 OS 或配置创建一个 UIFactory,再把它注入后续对象,业务层便不会重复出现平台分支。
启动检测 os=mac
-> 组合根选择 MacFactory
-> 注入 Dialog
-> createButton() = MacButton
-> createCheckbox() = MacCheckbox
-> 切换为 WindowsFactory 后整套替换
测试时不只验证类型,还要让同一工厂创建的 Button 和 Checkbox 运行共享主题或平台契约。再注入 FakeFactory,可以在不启动真实 UI 工具包的情况下验证 Dialog 只依赖抽象产品。
十、加强记忆
跨平台 UI 的抽象工厂答案要抓住两点:产品等级是 Button、Checkbox,产品族是 Windows、Mac。客户端选的是整套平台工厂,不是零散地选每个组件实现。