写软件的时候,经常会遇到这样一种需求:同一类东西有多种"系列",而且这些系列往往要成套地一起使用。比如界面皮肤的一整套控件(按钮、输入框、列表),要么全是"亮色风格",要么全是"暗色风格",不能按钮是亮色、输入框却是暗色,否则风格就乱套了。再比如程序要支持多种数据库: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 顺便让你总结一下它的优缺点,这样以后遇到类似场景你就知道什么时候该用它了。
优点
-
保证产品族的一致性:通过同一个具体工厂创建出来的对象,天然属于同一个产品系列,避免了"混搭风"的风险。
-
对客户端屏蔽具体类:客户端只关心抽象接口,具体类的创建逻辑被封装在工厂里,这样一旦需要替换实现,只要换一个工厂实现即可。
-
更容易切换产品族:比如从"亮色皮肤"切换到"暗色皮肤",只需要在配置或初始化的时候切一个工厂,业务逻辑代码完全不动。
-
符合开闭原则(对扩展开放,对修改关闭):新增一个产品族(比如"高对比度皮肤")时,只需要新增一组具体产品类和一个新的具体工厂,不需要去改原来的工厂接口和客户端调用逻辑。
缺点
-
不利于扩展单个产品等级结构:如果你在产品族里又加了一个新产品(比如再加一种控件 Toggle),那就必须修改所有工厂接口和所有具体工厂类,这就违反了"对修改关闭"。
-
结构相对复杂:相比简单工厂或工厂方法模式,抽象工厂的类和接口数量明显更多,对于小项目来说可能显得有点"重"。
-
易被误用:如果你的业务实际上只有一个产品,或者根本不存在"成套的产品族"概念,那硬套抽象工厂就会显得非常臃肿。
leader 总结了一句你印象很深的话:"抽象工厂最适合的是:'系列化产品 + 需要统一切换'的场景。没有产品族、没有系列化,就别硬上。"
六、抽象工厂和其他工厂模式的对比
你顺势问 leader:"那简单工厂、工厂方法和抽象工厂之间到底是啥关系?" leader 给你画了一个简单的"进化链":
简单工厂:一个工厂类,根据参数决定创建哪种具体产品。关注点是:创建某一个产品的不同实现,不关心成套或系列。
工厂方法:为每一种产品创建一个对应的工厂接口,每个具体工厂只负责创建一种产品。关注点是:延迟到子类去决定创建哪种产品,更强调"单一职责"的工厂。
抽象工厂:在工厂接口里定义一系列产品的创建方法,一个具体工厂负责创建一整套相关产品。关注点是:生产一个"产品族",保证成套使用时的一致性。
leader 打了一个比方:
简单工厂像一个"杂货铺老板",你跟他要啥他就给你啥;
工厂方法像是"专门的按钮生产厂"、"专门的输入框生产厂";
抽象工厂则是"整套 UI 套装供应商",你要某个风格的整套组件,它一套一套地给你。
七、抽象工厂的典型应用场景
最后,leader 让你回去多找几个例子体会一下抽象工厂的使用场景,你稍微查了下资料,再结合自己的体会,总结了几个典型的应用:
-
跨平台 UI 库:比如同一套业务逻辑,需要适配 Windows、macOS、Linux,每个平台有自己的一套按钮、窗口、菜单......这就是多个"平台系列"的 UI 产品族。
-
多数据库支持:同样的 DAO 接口,可以有 MySQL 实现、Oracle 实现、PostgreSQL 实现,每个实现内部又有连接、语句、事务等一整套配套对象。
-
游戏中的皮肤 / 阵营系统:比如红方、蓝方、绿方,每个阵营都有自己的一整套兵种、建筑、UI 资源,用抽象工厂可以很方便切换整套阵营配置。
-
日志 / 存储策略成套替换:比如本地日志、远程日志、混合日志模式,每种模式内部有自己的一整套"日志格式化器、输出器、持久化策略"。
你感叹道:原来抽象工厂并不只是一个"高大上的名词",而是很多日常场景里早就隐约用过的思想,只是以前没有这么系统地整理。
leader 听完后笑着说:"记住一句话:当你开始在系统里维护'多套成体系的实现',并需要在它们之间统一切换时,就该想想是不是可以用抽象工厂了。"
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
备注:上面内容是基于AI生成的。