设计模式-抽象工厂模式

写软件的时候,经常会遇到这样一种需求:同一类东西有多种"系列",而且这些系列往往要成套地一起使用。比如界面皮肤的一整套控件(按钮、输入框、列表),要么全是"亮色风格",要么全是"暗色风格",不能按钮是亮色、输入框却是暗色,否则风格就乱套了。再比如程序要支持多种数据库:MySQL、Oracle、SQLServer......每种数据库都有一整套自己的"配套对象",你需要保证同一时刻使用的是同一数据库系列的对象。

如果你刚开始写这类代码,很可能会写出类似下面这样的东西:在代码里各种 if / else 或 switch,根据一个类型字段来 new 不同的具体类:

java 复制代码
public class ButtonFactory {

    public static Button createButton(String type) {
        if ("light".equals(type)) {
            return new LightButton();
        } else if ("dark".equals(type)) {
            return new DarkButton();
        } else {
            return null;
        }
    }
}

看起来似乎没什么问题?需要亮色按钮就传 "light",需要暗色按钮就传 "dark",按钮对象就搞定了。然后你很自然地又写一个 InputFactory 来负责输入框:

java 复制代码
public class InputFactory {

    public static Input createInput(String type) {
        if ("light".equals(type)) {
            return new LightInput();
        } else if ("dark".equals(type)) {
            return new DarkInput();
        } else {
            return null;
        }
    }
}

一开始用着也挺顺手:UI 想切换成暗色主题,就到代码里把 "light" 改成 "dark",按钮和输入框都能换成暗色的实现类。

可是没过多久,你的 leader 找你聊天了。他说:"你这个写法勉强能用,但是存在很多问题。第一,if / else 满天飞,对扩展极其不友好;第二,你只能保证'按钮'同一时刻是同一个系列,却没法保证'按钮 + 输入框 + 列表'这一整套都是同一个风格系列,很容易有人调用的时候漏改一个地方。"

你仔细一想,确实是这样。现在要再加一个 List 控件,你又得写一个 ListFactory,里面同样一堆 if / else 判断系列类型。更麻烦的是,在业务代码里,你得自己小心翼翼地保证:

按钮用 type = "light"

输入框用 type = "light"

列表也要用 type = "light"

只要有人写错一个地方,比如把按钮类型写成 "dark",整个界面就会出现"混搭风"。leader 让你想办法,把"成套产品"的概念表达出来,让调用者只能使用同一系列的一组产品,彻底避免混用。

你琢磨半天,leader 看你皱着眉头,就提醒你:"这个场景其实就是典型的抽象工厂模式。你可以试试用抽象工厂来重构一下。"

你头一次听说抽象工厂这个名字,于是先在心里给它一个简单定义:抽象工厂,就是用来生产"同一产品族的一整套对象"的工厂,它对外只暴露一个统一的创建接口,让你在不指定具体类的前提下,成套地获取同一系列的具体产品。

于是你动手开始重构。

一、先抽象出"产品族"的接口

既然不想在外部到处 if / else new 具体类,那就先把"产品"抽象出来。比如现在有三个控件:按钮、输入框、列表,你先给它们定义统一接口:

java 复制代码
public interface Button {
    void click();
}

public interface Input {
    void input(String text);
}

public interface ListView {
    void show();
}

接着,为"亮色风格"和"暗色风格"分别实现这些产品:

java 复制代码
// 亮色系列
public class LightButton implements Button {
    @Override
    public void click() {
        System.out.println("亮色按钮被点击");
    }
}

public class LightInput implements Input {
    @Override
    public void input(String text) {
        System.out.println("亮色输入框输入:" + text);
    }
}

public class LightListView implements ListView {
    @Override
    public void show() {
        System.out.println("展示亮色列表");
    }
}

// 暗色系列
public class DarkButton implements Button {
    @Override
    public void click() {
        System.out.println("暗色按钮被点击");
    }
}

public class DarkInput implements Input {
    @Override
    public void input(String text) {
        System.out.println("暗色输入框输入:" + text);
    }
}

public class DarkListView implements ListView {
    @Override
    public void show() {
        System.out.println("展示暗色列表");
    }
}

到这里,产品本身已经有了清晰的层次:接口定义"产品类型",实现类区分"产品系列"。

二、再抽象出"工厂族"的接口

接下来是抽象工厂的核心:不是给每个产品单独写一个工厂,而是给"产品族"写一个工厂接口,这个接口负责创建这一整套产品。

java 复制代码
public interface SkinFactory {

    Button createButton();

    Input createInput();

    ListView createListView();
}

你可以把 SkinFactory 理解为"皮肤系列"的抽象工厂,它定义了这一族产品:按钮、输入框、列表,至于具体创建哪个实现类,由它的具体子类决定。

于是你实现两个具体的工厂类,对应两个皮肤系列:

java 复制代码
// 亮色皮肤工厂
public class LightSkinFactory implements SkinFactory {

    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Input createInput() {
        return new LightInput();
    }

    @Override
    public ListView createListView() {
        return new LightListView();
    }
}

// 暗色皮肤工厂
public class DarkSkinFactory implements SkinFactory {

    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Input createInput() {
        return new DarkInput();
    }

    @Override
    public ListView createListView() {
        return new DarkListView();
    }
}

这样一来,你就有了两个"工厂族"的具体实现:亮色皮肤工厂和暗色皮肤工厂,它们都遵循同一个 SkinFactory 接口,但各自生产的都是自己系列的产品。

三、客户端代码怎么使用?

现在到了见证抽象工厂威力的时候了。在客户端使用的时候,你只需要先选定一个"皮肤系列工厂",后面所有控件都从这个工厂里拿,就天然保证了整套 UI 是同一个风格系列:

java 复制代码
public class Client {

    public static void main(String[] args) {
        // 假设从配置文件或者用户设置里读出当前风格
        String theme = "dark"; // "light" 或 "dark"

        SkinFactory factory;

        if ("dark".equals(theme)) {
            factory = new DarkSkinFactory();
        } else {
            factory = new LightSkinFactory();
        }

        // 后面所有 UI 控件都从同一个工厂拿
        Button button = factory.createButton();
        Input input = factory.createInput();
        ListView listView = factory.createListView();

        button.click();
        input.input("Hello World");
        listView.show();
    }
}

注意看,现在客户端只跟 SkinFactory、Button、Input、ListView 这些抽象接口打交道,完全不需要关心 LightButton、DarkButton 这类具体类。你只要在一个地方确定用哪个"皮肤工厂",剩下的所有 UI 对象都会自然保持风格一致。

你的 leader 看了以后很满意:"这就是抽象工厂的典型用法,让一整套相关或相互依赖的对象保持同一个产品族,外部代码不需要也不应该知道具体类叫什么。"

四、抽象工厂模式的正式定义

在跟着 leader 一顿实战之后,你终于能给抽象工厂下一个比较严谨的定义了:

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

这里有几个关键词你要特别注意:

"一系列":不是只创建一个对象,而是一个"产品族",比如一整套 UI 控件、一整套数据库访问对象、一整套日志组件等等;

"相关或相互依赖":这些对象是成套使用的,它们之间往往有某种内在联系,比如同一个风格、同一个平台、同一个品牌;

"无需指定具体类":客户端拿到的永远是抽象接口或抽象类,具体类被"藏"在工厂实现里,方便你以后替换或扩展。

五、抽象工厂模式的优点和缺点

你用抽象工厂重构完 UI 皮肤之后,leader 顺便让你总结一下它的优缺点,这样以后遇到类似场景你就知道什么时候该用它了。

优点

  1. 保证产品族的一致性:通过同一个具体工厂创建出来的对象,天然属于同一个产品系列,避免了"混搭风"的风险。

  2. 对客户端屏蔽具体类:客户端只关心抽象接口,具体类的创建逻辑被封装在工厂里,这样一旦需要替换实现,只要换一个工厂实现即可。

  3. 更容易切换产品族:比如从"亮色皮肤"切换到"暗色皮肤",只需要在配置或初始化的时候切一个工厂,业务逻辑代码完全不动。

  4. 符合开闭原则(对扩展开放,对修改关闭):新增一个产品族(比如"高对比度皮肤")时,只需要新增一组具体产品类和一个新的具体工厂,不需要去改原来的工厂接口和客户端调用逻辑。

缺点

  1. 不利于扩展单个产品等级结构:如果你在产品族里又加了一个新产品(比如再加一种控件 Toggle),那就必须修改所有工厂接口和所有具体工厂类,这就违反了"对修改关闭"。

  2. 结构相对复杂:相比简单工厂或工厂方法模式,抽象工厂的类和接口数量明显更多,对于小项目来说可能显得有点"重"。

  3. 易被误用:如果你的业务实际上只有一个产品,或者根本不存在"成套的产品族"概念,那硬套抽象工厂就会显得非常臃肿。

leader 总结了一句你印象很深的话:"抽象工厂最适合的是:'系列化产品 + 需要统一切换'的场景。没有产品族、没有系列化,就别硬上。"

六、抽象工厂和其他工厂模式的对比

你顺势问 leader:"那简单工厂、工厂方法和抽象工厂之间到底是啥关系?" leader 给你画了一个简单的"进化链":

简单工厂:一个工厂类,根据参数决定创建哪种具体产品。关注点是:创建某一个产品的不同实现,不关心成套或系列。

工厂方法:为每一种产品创建一个对应的工厂接口,每个具体工厂只负责创建一种产品。关注点是:延迟到子类去决定创建哪种产品,更强调"单一职责"的工厂。

抽象工厂:在工厂接口里定义一系列产品的创建方法,一个具体工厂负责创建一整套相关产品。关注点是:生产一个"产品族",保证成套使用时的一致性。

leader 打了一个比方:

简单工厂像一个"杂货铺老板",你跟他要啥他就给你啥;

工厂方法像是"专门的按钮生产厂"、"专门的输入框生产厂";

抽象工厂则是"整套 UI 套装供应商",你要某个风格的整套组件,它一套一套地给你。

七、抽象工厂的典型应用场景

最后,leader 让你回去多找几个例子体会一下抽象工厂的使用场景,你稍微查了下资料,再结合自己的体会,总结了几个典型的应用:

  1. 跨平台 UI 库:比如同一套业务逻辑,需要适配 Windows、macOS、Linux,每个平台有自己的一套按钮、窗口、菜单......这就是多个"平台系列"的 UI 产品族。

  2. 多数据库支持:同样的 DAO 接口,可以有 MySQL 实现、Oracle 实现、PostgreSQL 实现,每个实现内部又有连接、语句、事务等一整套配套对象。

  3. 游戏中的皮肤 / 阵营系统:比如红方、蓝方、绿方,每个阵营都有自己的一整套兵种、建筑、UI 资源,用抽象工厂可以很方便切换整套阵营配置。

  4. 日志 / 存储策略成套替换:比如本地日志、远程日志、混合日志模式,每种模式内部有自己的一整套"日志格式化器、输出器、持久化策略"。

你感叹道:原来抽象工厂并不只是一个"高大上的名词",而是很多日常场景里早就隐约用过的思想,只是以前没有这么系统地整理。

leader 听完后笑着说:"记住一句话:当你开始在系统里维护'多套成体系的实现',并需要在它们之间统一切换时,就该想想是不是可以用抽象工厂了。"

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

备注:上面内容是基于AI生成的。

相关推荐
寰宇的行者3 小时前
设计模式:单例模式
单例模式·设计模式
zjjsctcdl4 小时前
Spring Boot 经典九设计模式全览
java·spring boot·设计模式
君主黑暗4 小时前
设计模式-建造者模式
设计模式·建造者模式
bmseven4 小时前
23种设计模式 - 原型模式(Prototype)
设计模式·原型模式
皙然4 小时前
深度解析 “池化思想”:从设计模式到 Java 技术栈的落地与实践
java·开发语言·设计模式
技术人生黄勇4 小时前
Google 开源实战指南:21种AI智能体设计模式,覆盖从基础到安全的完整体系
人工智能·设计模式
Yu_Lijing5 小时前
基于C++的《Head First设计模式》笔记——解释器模式
c++·设计模式·解释器模式
bmseven5 小时前
23种设计模式 - 建造者模式(Builder)
设计模式·建造者模式
君主黑暗5 小时前
设计模式-适配器模式
设计模式·适配器模式