抽象工厂模式:产品族与数据库访问层切换

抽象工厂模式,说白了就是解决「一系列相关对象」的创建问题。你想想看,如果只是单个对象的创建,用简单工厂或者工厂方法就够了。但当你需要创建一组互相搭配的产品时,抽象工厂就派上用场了。

我记得刚入行那会儿,接手过一个老项目,数据库从 SQL Server 要迁移到 Oracle。那代码改得叫一个痛苦——每个 new 关键字都得找出来替换。后来我学乖了,这种跨平台、跨数据库的场景,抽象工厂就是标准答案。

产品族与产品等级结构

先理清两个核心概念:产品族产品等级结构

  • 产品等级结构:同一类产品的继承体系。比如「按钮」这个抽象类,下面有 Windows 按钮、Mac 按钮。
  • 产品族:由同一个工厂生产的一组产品。比如 Windows 工厂生产 Windows 按钮 + Windows 文本框 + Windows 菜单栏。

抽象工厂模式要解决的,就是当你有多个产品等级结构,且它们之间存在「配套使用」的关系时,如何保证客户端始终使用同一族的产品。

核心要点:抽象工厂模式封装了产品族的创建逻辑。客户端不需要知道具体产品类名,只需要知道工厂接口。

抽象工厂接口与具体工厂

抽象工厂接口定义了创建每个产品等级结构的方法。具体工厂则实现这些方法,返回特定产品族的实例。

我在项目中遇到过一种情况:一开始只用了工厂方法模式,后来产品族扩展了,不得不把工厂方法改造成抽象工厂。嗯,这个教训告诉我——如果预见到会有产品族出现,直接上抽象工厂反而省事。

// 抽象产品:按钮
interface Button {
    void render();
    void onClick();
}

// 抽象产品:文本框
interface TextField {
    void display();
    void input(String text);
}

// 抽象工厂接口
interface UIFactory {
    Button createButton();
    TextField createTextField();
}

// 具体产品族:Windows
class WindowsButton implements Button {
    public void render() { System.out.println("渲染 Windows 风格按钮"); }
    public void onClick() { System.out.println("Windows 按钮点击"); }
}

class WindowsTextField implements TextField {
    public void display() { System.out.println("显示 Windows 文本框"); }
    public void input(String text) { System.out.println("Windows 输入: " + text); }
}

// 具体工厂:Windows 工厂
class WindowsFactory implements UIFactory {
    public Button createButton() { return new WindowsButton(); }
    public TextField createTextField() { return new WindowsTextField(); }
}

// 具体产品族:Mac
class MacButton implements Button {
    public void render() { System.out.println("渲染 Mac 风格按钮"); }
    public void onClick() { System.out.println("Mac 按钮点击"); }
}

class MacTextField implements TextField {
    public void display() { System.out.println("显示 Mac 文本框"); }
    public void input(String text) { System.out.println("Mac 输入: " + text); }
}

class MacFactory implements UIFactory {
    public Button createButton() { return new MacButton(); }
    public TextField createTextField() { return new MacTextField(); }
}

数据库访问层切换案例

这是抽象工厂最经典的应用场景。假设你的系统需要支持 MySQL、PostgreSQL、Oracle 三种数据库。每种数据库都有连接、命令、事务等操作,这些操作之间是配套的——你不能用 MySQL 的连接去执行 Oracle 的命令。

// 抽象产品
interface DBConnection {
    void connect();
    void disconnect();
}

interface DBCommand {
    void execute(String sql);
    void setConnection(DBConnection conn);
}

// 抽象工厂
interface DatabaseFactory {
    DBConnection createConnection();
    DBCommand createCommand();
}

// MySQL 产品族
class MySQLConnection implements DBConnection {
    public void connect() { System.out.println("连接 MySQL"); }
    public void disconnect() { System.out.println("断开 MySQL"); }
}

class MySQLCommand implements DBCommand {
    private DBConnection conn;
    public void setConnection(DBConnection conn) { this.conn = conn; }
    public void execute(String sql) { System.out.println("MySQL 执行: " + sql); }
}

class MySQLFactory implements DatabaseFactory {
    public DBConnection createConnection() { return new MySQLConnection(); }
    public DBCommand createCommand() { return new MySQLCommand(); }
}

// PostgreSQL 产品族
class PostgreSQLConnection implements DBConnection {
    public void connect() { System.out.println("连接 PostgreSQL"); }
    public void disconnect() { System.out.println("断开 PostgreSQL"); }
}

class PostgreSQLCommand implements DBCommand {
    private DBConnection conn;
    public void setConnection(DBConnection conn) { this.conn = conn; }
    public void execute(String sql) { System.out.println("PostgreSQL 执行: " + sql); }
}

class PostgreSQLFactory implements DatabaseFactory {
    public DBConnection createConnection() { return new PostgreSQLConnection(); }
    public DBCommand createCommand() { return new PostgreSQLCommand(); }
}

// 客户端代码
class DatabaseClient {
    private DatabaseFactory factory;
    
    public DatabaseClient(DatabaseFactory factory) {
        this.factory = factory;
    }
    
    public void runQuery(String sql) {
        DBConnection conn = factory.createConnection();
        DBCommand cmd = factory.createCommand();
        cmd.setConnection(conn);
        conn.connect();
        cmd.execute(sql);
        conn.disconnect();
    }
}

我的经验:切换数据库时,只需要换一行代码——new DatabaseClient(new PostgreSQLFactory())。整个系统的数据库访问逻辑完全不用改。这就是抽象工厂的魅力。

抽象工厂模式的结构图

下面这张图展示了抽象工厂模式的核心结构。你看,客户端只依赖抽象工厂和抽象产品,具体工厂和具体产品都被隔离在另一侧。

«interface» DatabaseFactory «interface» DBConnection «interface» DBCommand MySQLFactory PostgreSQLFactory MySQLConnection MySQLCommand PostgreSQLConnection PostgreSQLCommand DatabaseClient 抽象层 具体工厂 具体产品

使用抽象工厂的注意事项

我曾经踩过的坑:抽象工厂模式有个明显的缺点——扩展产品等级结构很困难。比如你要新增一个「数据库连接池」产品,就得修改抽象工厂接口,所有具体工厂都得跟着改。这就是所谓的「开闭原则」的倾斜性:对产品族扩展友好,对产品等级结构扩展不友好。

所以我的建议是:

  • 如果产品等级结构比较稳定,不太会新增,用抽象工厂很合适
  • 如果经常要新增产品等级结构,考虑用工厂方法模式或者结合 IoC 容器
  • 不要为了用模式而用模式——我见过有人把只有两个产品的场景硬套抽象工厂,反而增加了复杂度

抽象工厂 vs 工厂方法

对比维度 工厂方法模式 抽象工厂模式
创建对象数量 单个产品 多个相关产品(产品族)
产品等级结构 一个 多个
扩展方向 新增产品等级结构容易 新增产品族容易
典型应用 日志记录器、文件解析器 数据库访问层、UI 组件库

说白了,工厂方法是「一条生产线生产一种产品」,抽象工厂是「一个工厂生产一整套产品」。你想想看,如果你的系统需要切换数据库,是不是每次都要换一整套——连接、命令、事务、连接池?这就是抽象工厂的用武之地。

个人习惯:我在设计框架时,通常会先定义抽象工厂接口,然后提供默认的具体工厂实现。这样既保留了扩展性,又不会让初学者觉得太复杂。等需要切换时,写一个新的具体工厂类就行了。

抽象工厂模式是设计模式中「创建型」的集大成者。它把产品族的创建逻辑集中管理,让客户端代码与具体实现彻底解耦。虽然它有一定的学习曲线,但一旦用对地方,带来的维护便利性是非常可观的。


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