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流程图:抽象工厂模式结构

下面这张图,帮你理清各个角色之间的关系:

UIFactory 抽象工厂 WindowsUIFactory 具体工厂 MacUIFactory 具体工厂 Button 抽象产品 TextBox 抽象产品 WindowsButton MacButton WindowsTextBox MacTextBox

避坑指南

我曾经在一个跨平台项目中,把工厂模式用「过」了。什么意思呢?就是每个组件都单独建了一个工厂,结果工厂数量比组件还多,管理起来反而更乱。

后来我总结出几条经验:

常见问题 我的建议
工厂粒度太细 按「产品族」分组,不要每个组件一个工厂
新增操作系统困难 确保抽象工厂的方法签名稳定,新增系统只需加一个具体工厂
产品之间有关联 比如按钮和文本框需要共享某种样式,可以在工厂内部统一处理
性能问题 工厂创建开销大时,可以结合单例模式,每个工厂只实例化一次

注意: 抽象工厂模式不适合「产品等级结构」经常变化的场景。如果你今天加按钮,明天加文本框,后天加下拉框,抽象工厂的接口就要频繁修改,那就得不偿失了。

扩展思考:不只是操作系统

UI组件工厂的应用场景其实很广。除了操作系统,你还可以用它来:

  • 支持不同主题(暗黑模式、浅色模式)
  • 支持不同UI框架(React、Vue、原生)
  • 支持不同设备类型(手机、平板、桌面)

嗯,这里要注意:不要为了用模式而用模式。如果你的项目只有一套UI风格,那直接new就完事了,没必要上工厂。

但如果你确定未来会有多套风格,或者已经在维护多套风格了,那抽象工厂模式绝对值得你花时间引入。

我的一个小技巧: 在项目初期,先用简单工厂顶着。等真正出现第二套风格需求时,再重构为抽象工厂。这样既不会过度设计,也不会错过最佳时机。

好了,UI组件工厂就聊到这儿。记住一句话:工厂模式的核心,是把「变」与「不变」分离。不变的是抽象接口,变的是具体实现。把握好这个度,你的代码会越来越干净。


公众号:蓝海资料掘金营,微信deep3321