13、工厂模式实战(二):UI组件工厂(支持不同操作系统的按钮、文本框)
上一章我们聊了日志工厂,这次来点更贴近前端的实战——UI组件工厂。
说实话,跨平台UI组件这块,我踩过的坑真不少。早年做桌面端软件,Windows和Mac的按钮长得完全不一样,你总不能给用户一个「Windows风格」的按钮跑在Mac上吧?那体验太割裂了。
工厂模式在这里,就是我们的救星。
核心问题:操作系统差异
不同操作系统,UI组件的视觉风格、交互逻辑、甚至API命名都不同。比如:
- Windows的按钮有经典的直角边框,Mac的按钮偏圆润
- 文本框在Linux下的默认字体和间距,跟Windows完全两码事
- 有些系统支持阴影,有些系统不支持
你想想看,如果每个组件都写if-else判断操作系统,代码会变成什么样?
我曾经在一个老项目里看到过这样的代码:
if (os === 'windows') {
button = new WindowsButton();
} else if (os === 'mac') {
button = new MacButton();
} else if (os === 'linux') {
button = new LinuxButton();
} else {
button = new DefaultButton();
}
这还只是一个按钮。文本框呢?下拉框呢?对话框呢?每个组件都来一套if-else,代码量直接爆炸,维护起来更是噩梦。
工厂模式怎么解决?
说白了,就是把「创建组件」这件事抽象出来。我们定义一个抽象工厂,然后每个操作系统实现自己的具体工厂。
这样,客户端代码只需要跟抽象工厂打交道,根本不用关心底层是哪个操作系统。
核心思想: 将「产品族」的创建逻辑集中管理。一个工厂负责生产一套完整风格的组件。
代码实现:UI组件抽象工厂
我们先定义抽象产品——按钮和文本框:
// 抽象按钮
class Button {
render() {
throw new Error('子类必须实现 render 方法');
}
}
// 抽象文本框
class TextBox {
render() {
throw new Error('子类必须实现 render 方法');
}
}
然后,每个操作系统实现自己的具体产品:
// Windows 风格按钮
class WindowsButton extends Button {
render() {
console.log('渲染 Windows 风格按钮:直角、蓝色、带阴影');
// 实际项目中这里会操作 DOM 或 Canvas
}
}
// Mac 风格按钮
class MacButton extends Button {
render() {
console.log('渲染 Mac 风格按钮:圆角、灰色、无阴影');
}
}
// Windows 风格文本框
class WindowsTextBox extends TextBox {
render() {
console.log('渲染 Windows 风格文本框:带边框、白色背景');
}
}
// Mac 风格文本框
class MacTextBox extends TextBox {
render() {
console.log('渲染 Mac 风格文本框:无边框、浅灰色背景');
}
}
接下来,定义抽象工厂:
class UIFactory {
createButton() {
throw new Error('子类必须实现 createButton 方法');
}
createTextBox() {
throw new Error('子类必须实现 createTextBox 方法');
}
}
具体工厂实现:
class WindowsUIFactory extends UIFactory {
createButton() {
return new WindowsButton();
}
createTextBox() {
return new WindowsTextBox();
}
}
class MacUIFactory extends UIFactory {
createButton() {
return new MacButton();
}
createTextBox() {
return new MacTextBox();
}
}
客户端使用:
function renderUI(factory) {
const button = factory.createButton();
const textBox = factory.createTextBox();
button.render();
textBox.render();
}
// 根据操作系统选择工厂
const os = detectOS(); // 返回 'windows' 或 'mac'
let factory;
if (os === 'windows') {
factory = new WindowsUIFactory();
} else {
factory = new MacUIFactory();
}
renderUI(factory);
个人习惯: 我一般会把工厂选择逻辑封装成一个「工厂提供者」函数,这样客户端代码连if-else都不用写了。直接调用 getUIFactory() 就行。
SVG流程图:抽象工厂模式结构
下面这张图,帮你理清各个角色之间的关系:
避坑指南
我曾经在一个跨平台项目中,把工厂模式用「过」了。什么意思呢?就是每个组件都单独建了一个工厂,结果工厂数量比组件还多,管理起来反而更乱。
后来我总结出几条经验:
| 常见问题 | 我的建议 |
|---|---|
| 工厂粒度太细 | 按「产品族」分组,不要每个组件一个工厂 |
| 新增操作系统困难 | 确保抽象工厂的方法签名稳定,新增系统只需加一个具体工厂 |
| 产品之间有关联 | 比如按钮和文本框需要共享某种样式,可以在工厂内部统一处理 |
| 性能问题 | 工厂创建开销大时,可以结合单例模式,每个工厂只实例化一次 |
注意: 抽象工厂模式不适合「产品等级结构」经常变化的场景。如果你今天加按钮,明天加文本框,后天加下拉框,抽象工厂的接口就要频繁修改,那就得不偿失了。
扩展思考:不只是操作系统
UI组件工厂的应用场景其实很广。除了操作系统,你还可以用它来:
- 支持不同主题(暗黑模式、浅色模式)
- 支持不同UI框架(React、Vue、原生)
- 支持不同设备类型(手机、平板、桌面)
嗯,这里要注意:不要为了用模式而用模式。如果你的项目只有一套UI风格,那直接new就完事了,没必要上工厂。
但如果你确定未来会有多套风格,或者已经在维护多套风格了,那抽象工厂模式绝对值得你花时间引入。
我的一个小技巧: 在项目初期,先用简单工厂顶着。等真正出现第二套风格需求时,再重构为抽象工厂。这样既不会过度设计,也不会错过最佳时机。
好了,UI组件工厂就聊到这儿。记住一句话:工厂模式的核心,是把「变」与「不变」分离。不变的是抽象接口,变的是具体实现。把握好这个度,你的代码会越来越干净。
公众号:蓝海资料掘金营,微信deep3321