抽象工厂模式完全指南:让一族产品保持配套的设计利器

工厂方法解决"一类产品的创建",抽象工厂解决"一族相关产品的创建"。听起来抽象,但落地很具体:你做跨平台 GUI 时,Mac 上要 Mac 风格的 Button、TextField、Checkbox,Windows 上要 Windows 风格的全部对应控件 —— 这些控件必须"配对",混用就是灾难。抽象工厂就是为这种"产品族要保持一致"的场景设计的。这篇文章讲透它的结构、典型用法和与工厂方法的边界。

从一个具体问题切入

假设你要做一个跨平台的对话框框架,支持 Mac / Windows / Linux 三种风格。每种风格都有自己的按钮、文本框、复选框。最朴素的做法:

public class Dialog {
    public void render(String os) {
        if (os.equals("mac")) {
            new MacButton().render();
            new MacTextField().render();
            new MacCheckbox().render();
        } else if (os.equals("windows")) {
            new WindowsButton().render();
            new WindowsTextField().render();
            new WindowsCheckbox().render();
        } else {
            new LinuxButton().render();
            new LinuxTextField().render();
            new LinuxCheckbox().render();
        }
    }
}

问题清单:

  • 每加一个平台,要改 render 全部分支;每加一种控件,要在所有分支里加一行。
  • 同平台的控件可能写错配对(把 MacButton 和 WindowsTextField 放一起,样式就乱了)。
  • 这堆 if/else 散布在十几个不同地方,改起来九死一生。

抽象工厂解决思路:把"一个平台的所有控件"打包到一个工厂里,保证它们配套出厂

标准结构

// 1. 一组抽象产品
interface Button { void render(); }
interface TextField { void render(); }
interface Checkbox { void render(); }

// 2. 具体产品(每个平台一组)
class MacButton implements Button { public void render() { System.out.println("Mac 按钮"); } }
class MacTextField implements TextField { public void render() { System.out.println("Mac 文本框"); } }
class MacCheckbox implements Checkbox { public void render() { System.out.println("Mac 复选框"); } }

class WindowsButton implements Button { public void render() { System.out.println("Win 按钮"); } }
class WindowsTextField implements TextField { public void render() { System.out.println("Win 文本框"); } }
class WindowsCheckbox implements Checkbox { public void render() { System.out.println("Win 复选框"); } }

// 3. 抽象工厂:列出所有创建方法
interface UIFactory {
    Button createButton();
    TextField createTextField();
    Checkbox createCheckbox();
}

// 4. 具体工厂:每个平台一个
class MacFactory implements UIFactory {
    public Button createButton()       { return new MacButton(); }
    public TextField createTextField() { return new MacTextField(); }
    public Checkbox createCheckbox()   { return new MacCheckbox(); }
}
class WindowsFactory implements UIFactory {
    public Button createButton()       { return new WindowsButton(); }
    public TextField createTextField() { return new WindowsTextField(); }
    public Checkbox createCheckbox()   { return new WindowsCheckbox(); }
}

// 5. 使用方依赖抽象工厂,不知道具体平台
class Dialog {
    private final UIFactory factory;
    public Dialog(UIFactory factory) { this.factory = factory; }
    public void render() {
        factory.createButton().render();
        factory.createTextField().render();
        factory.createCheckbox().render();
    }
}

// 6. 启动时挑一个具体工厂
UIFactory factory = isMac() ? new MacFactory() : new WindowsFactory();
new Dialog(factory).render();

结果:Dialog 类对"操作系统"零感知 —— 它只知道有一个 UIFactory 能造控件。换平台只需启动期换一个工厂,Dialog 一行不用改。

工厂方法 vs 抽象工厂

关键区别看下表:

            工厂方法              抽象工厂
解决问题    单个产品的创建变化    一族相关产品要保持一致
扩展维度    一个                  两个(产品类型 × 平台)
继承结构    一个抽象创建者        一个抽象工厂,多个抽象产品
新加产品    新增一个工厂子类      所有具体工厂都要新增一个方法
新加平台    -                     新增一个具体工厂(实现所有方法)

记忆口诀:"工厂方法是一维扩展,抽象工厂是二维矩阵"。当你画出来发现产品是一张 N×M 的表(N 个产品 × M 个平台/主题/方言),那就是抽象工厂的舞台。

实战:跨数据库 ORM 方言

抽象工厂在 ORM 框架里非常常见。Hibernate 的 Dialect、MyBatis 的 DatabaseIdProvider、Django 的 database backend 都是抽象工厂的变体。

// 抽象产品:每种数据库需要的不同 SQL 生成器
interface SqlGenerator {
    String generateLimit(int offset, int limit);
    String generateBoolType();
    String quote(String identifier);
}

// 抽象工厂
interface DialectFactory {
    SqlGenerator createSqlGenerator();
    TypeConverter createTypeConverter();
    Reserve createReserve();    // 关键字
}

// 具体:MySQL
class MySqlDialect implements DialectFactory {
    public SqlGenerator createSqlGenerator() {
        return new SqlGenerator() {
            public String generateLimit(int o, int l) { return "LIMIT " + o + ", " + l; }
            public String generateBoolType()           { return "TINYINT(1)"; }
            public String quote(String i)              { return "`" + i + "`"; }
        };
    }
    // ... 其他
}

// 具体:PostgreSQL
class PostgresDialect implements DialectFactory {
    public SqlGenerator createSqlGenerator() {
        return new SqlGenerator() {
            public String generateLimit(int o, int l) { return "LIMIT " + l + " OFFSET " + o; }
            public String generateBoolType()           { return "BOOLEAN"; }
            public String quote(String i)              { return "\"" + i + "\""; }
        };
    }
}

// 具体:Oracle
class OracleDialect implements DialectFactory {
    public SqlGenerator createSqlGenerator() {
        return new SqlGenerator() {
            // Oracle 没有 LIMIT,要用 ROWNUM 或 ROW_NUMBER()
            public String generateLimit(int o, int l) {
                return "WHERE ROWNUM > " + o + " AND ROWNUM <= " + (o + l);
            }
            public String generateBoolType()           { return "NUMBER(1)"; }
            public String quote(String i)              { return "\"" + i + "\""; }
        };
    }
}

用法上,框架启动时根据连接字符串选一次方言,后续所有 SQL 生成、类型转换都通过这一个工厂,保证"一个查询里的所有 SQL 片段都是同一种数据库方言"。

实战:不同主题的图表组件

前端常见场景:同一套图表组件,要支持 light / dark / 高对比度三套主题。每个主题里 colors、fonts、axisStyles、tooltipStyles 都不同,且必须配对。

// 用 TypeScript
interface ColorPalette { primary: string; bg: string; text: string; grid: string; }
interface FontSet { family: string; sizeBase: number; }
interface AxisStyle { tick: string; line: string; }

interface ThemeFactory {
    createColors(): ColorPalette;
    createFonts(): FontSet;
    createAxis(): AxisStyle;
}

const lightTheme: ThemeFactory = {
    createColors: () => ({ primary: '#0ea5e9', bg: '#fff', text: '#111', grid: '#eee' }),
    createFonts:  () => ({ family: 'Inter', sizeBase: 14 }),
    createAxis:   () => ({ tick: '#999', line: '#ccc' }),
};

const darkTheme: ThemeFactory = {
    createColors: () => ({ primary: '#38bdf8', bg: '#0f172a', text: '#f1f5f9', grid: '#334155' }),
    createFonts:  () => ({ family: 'Inter', sizeBase: 14 }),
    createAxis:   () => ({ tick: '#94a3b8', line: '#475569' }),
};

class Chart {
    constructor(private theme: ThemeFactory) {}
    render() {
        const colors = this.theme.createColors();
        const fonts = this.theme.createFonts();
        const axis = this.theme.createAxis();
        // 用这些"配套"参数渲染图表 —— 永远不会出现"light 的字 + dark 的背景"
    }
}

new Chart(darkTheme).render();

函数式表达:Map of Factories

在动态语言里,抽象工厂常常简化为"一个映射"。Python 例:

themes = {
    'light': {
        'colors': lambda: {'primary': '#0ea5e9', 'bg': '#fff'},
        'fonts':  lambda: {'family': 'Inter'},
    },
    'dark': {
        'colors': lambda: {'primary': '#38bdf8', 'bg': '#0f172a'},
        'fonts':  lambda: {'family': 'Inter'},
    },
}

def render_chart(theme_name):
    theme = themes[theme_name]
    colors = theme['colors']()
    fonts = theme['fonts']()
    # ...

这种"用 dict 代替接口 + 类"的写法在 Python / JS 里更地道。本质上还是抽象工厂,但视觉上轻量得多。

抽象工厂的代价

抽象工厂解决"一族产品一致性",代价也大:

1. 加新产品很贵

原本只支持 Button / TextField / Checkbox 三个控件,现在要加 Slider。所有具体工厂都要加一个 createSlider 方法。如果你有 Mac / Windows / Linux / iOS / Android 五个工厂,这是五处改动 —— 而且少改一个就编译错。

这也是抽象工厂被批评的"产品扩展困难"。所以选用前一定要评估:"产品数量是不是相对稳定?" 是的话上抽象工厂;不是的话考虑其他方案(比如组件注册表)。

2. 容易变成"上帝接口"

当具体工厂要提供 20 个 create 方法时,接口本身就很重。再加上每个平台一份实现,类数量爆炸。可以用"组合多个小工厂"来减负:

// 把 UIFactory 拆成三个小工厂,Dialog 注入这三个
interface InputFactory  { Button createButton(); Checkbox createCheckbox(); }
interface TextFactory   { TextField createTextField(); Label createLabel(); }
interface ChromeFactory { Window createWindow(); MenuBar createMenuBar(); }

class Dialog {
    public Dialog(InputFactory inputs, TextFactory texts, ChromeFactory chrome) { ... }
}

抽象工厂与依赖注入

Spring 等 DI 容器经常被用来"自动选具体工厂":

// 配置文件决定用哪个工厂
@Profile("mac")
@Component
public class MacFactory implements UIFactory { ... }

@Profile("windows")
@Component
public class WindowsFactory implements UIFactory { ... }

// 用方
@Component
public class Dialog {
    private final UIFactory factory;
    public Dialog(UIFactory factory) { this.factory = factory; }
}

// 启动时
java -Dspring.profiles.active=mac -jar app.jar

容器接管了"挑工厂"这件事,代码里看不到任何 if/else,极其干净。

识别抽象工厂场景的关键问题

遇到一个设计抉择,问下面三个问题判断是不是抽象工厂:

  1. 有没有"一族"对象需要一起创建?(不只是一个)
  2. 这些对象之间有"配对约束"吗?(Mac 控件不能和 Win 控件混用)
  3. "平台/主题/方言"的数量是有限且稳定的吗?(2-5 个,不是开放扩展)

三个 yes 就上抽象工厂。某个 no,看具体哪个:

  • "族"只有一个产品?改用工厂方法。
  • 没有配对约束?直接 new 或简单工厂就够。
  • 平台数量会无限扩展?改用"插件 + 注册表"模式。

插件 + 注册表:抽象工厂的现代替代

当"平台/方言"数量可能无限增长时,抽象工厂的"每加一个就要写一个完整工厂类"成本太高。这时常见的替代是"插件 + 注册表":

class DialectRegistry {
    private static final Map<String, Supplier<Dialect>> map = new HashMap<>();

    public static void register(String name, Supplier<Dialect> factory) {
        map.put(name, factory);
    }

    public static Dialect get(String name) {
        return map.get(name).get();
    }
}

// 各方言"自己"在启动期注册,核心代码不需要改
public class CustomNoSQLDialectModule {
    static {
        DialectRegistry.register("mongo", MongoDialect::new);
    }
}

这种"反向控制"让框架核心永远不需要改 —— 加新方言就是新加一个 jar 包或模块,在自己的初始化里注册自己。Java 的 ServiceLoader、Spring 的 SPI 都是这套思路。

写在最后

抽象工厂模式像一把锋利但重的刀:用对场景威力很大(避免产品族错配 + 平台切换零侵入),用错场景就是过度设计。判断它是不是"对"的标志,看你画出来的"产品 × 平台"矩阵是不是真的填满了 —— 矩阵稀疏(很多格子用不到)时,抽象工厂就显得笨重,改用工厂方法 + 简单工厂的组合更合适。

给一个落地建议:第一次发现"我要为 Mac / Windows 分别写一套实现"时,先用工厂方法;当第二次、第三次发现"还有一组组件要按平台区分"时,把它们提升为抽象工厂。设计模式不是预先用,而是被代码"逼"出来的 —— 重复出现的相似结构,自然会浮现到合适的抽象层级。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

工厂方法模式完全指南:从硬编码 new 到开闭原则的优雅实现

2026-5-15 11:35:34

技术教程

建造者模式完全指南:从构造函数地狱到流式 API 的工程演化

2026-5-15 11:35:35

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索