1. 场景先行:当代码里只剩 if/else 时,抽象工厂要解决什么问题
先说个真实经历。前两年我接手一套内部数据报表系统,代码写的时间不短了,但每次要加一个新功能我都想骂人。不是业务逻辑复杂,而是大量判断逻辑散落在各个创建对象的地方。比如系统需要支持两套主题、两套导出格式,于是代码里到处是:
def create_button(theme: str): if theme == "dark": return DarkButton() elif theme == "light": return LightButton() else: raise ValueError(f"unknown theme: {theme}")乍一看没问题,但这种函数在项目里有七八个,每个都要自己写一遍主题判断。后续加了blue主题,你至少要改八个文件,漏掉任何一个,运行时就会出现“主题一半是 dark、一半是 light”的诡异界面。后来我把这一大坨重构时,特意用 Python 的 Abstract Factory Pattern 重写了整套创建逻辑,代码量减少了一大半,新增主题改一处就能生效。
这篇文章不打算讲教科书定义,而是把我当时的思考、踩坑和最终落地方案完整记录下来。适合已经熟悉 Python 基础语法、想进阶设计模式,或者正在为“代码里全是 if/else 不知道怎么收拾”发愁的人。
1.1 痛点的本质:创建逻辑和调用逻辑混在一起
很多人没意识到,if theme == "dark"这种判断,本质上是把“选哪套实现”和“用哪个产品”两件事耦合在同一个函数里。调用方本来只关心“给我一个按钮,我要用它渲染”,现在却被迫知道“有哪些主题、每个主题对应什么类”,这就是典型的违反依赖倒置。
真正的麻烦在于“一组产品”。按钮、输入框、复选框,这三样东西在 UI 体系里必须成套出现。你会用暗色主题的按钮,搭配一个亮色主题的输入框吗?一般不会,因为视觉上不协调。更关键的是,如果某个具体产品实现依赖同主题下的另一个产品,那拆开创建就会出问题。比如暗色主题的输入框内部用暗色绘制 API,亮色主题的输入框用亮色绘制 API,你没法保证“随机搭配”还不出错。
抽象工厂模式解决的就是这个问题:把“成套产品的创建”封装进一个工厂,调用方拿到的永远是同一个主题下的完整一套。
1.2 抽象工厂的目标:让调用方向“依赖抽象而非依赖具体”
用一句大白话总结这个模式的使命:调用方只认“产品长什么样”,不关心“产品从哪里来”。你告诉工厂“我要一套按钮、输入框、复选框”,工厂根据配置给你一套风格统一的实现,你拿到的对象保证能配合工作。
这听起来像个挺虚的理念,但落地到代码里有一个可量化的收益:创建产品时的分支判断从 N 个地方收敛到了 1 个地方。原来新增主题要改 8 个文件,现在只需新增一个工厂类和若干产品类,原来的调用代码一行都不用动。
2. 角色拆解:抽象工厂模式里到底有哪几类对象
抽象工厂模式一共涉及四个角色:抽象工厂、具体工厂、抽象产品、具体产品。听起来抽象,实际上很好理解。
2.1 四个角色的职责划分
- 抽象产品:定义一类产品的接口。比如
Button抽象类声明了render()和on_click()方法,它是客户端唯一依赖的东西。 - 具体产品:实现抽象产品的某个具体版本。
DarkButton、LightButton就是两个具体产品。 - 抽象工厂:定义创建一组产品的方法。比如
UIFactory声明了创建按钮、输入框、复选框的接口,它不管具体实现。 - 具体工厂:实现抽象工厂,为每个抽象产品提供一个具体实现。
DarkThemeFactory返回DarkButton、DarkTextInput、DarkCheckbox,LightThemeFactory则返回另一套。
客户端代码里永远看不到DarkButton(这种直接实例化语句,它只依赖Button和UIFactory这两个抽象层。这就是依赖倒置的落地。
2.2 用家具工厂来理解这套结构
拿家具打比方。假设你要布置客厅,需要沙发、茶几、书架三件套。市面上有北欧风格、中式风格、工业风格三种系列,每套都有沙发、茶几、书架,但你不能把北欧沙发配中式的茶几,风格不搭。
这里的“沙发、茶几、书架”就是三个抽象产品;北欧沙发、中式茶几是具体产品;“家具商城”是抽象工厂,它提供“买沙发、买茶几、买书架”三个入口;“北欧家具工厂”“中式家具工厂”是具体工厂,分别生产各自风格的三件套。你作为买家,只需要说“我要北欧风格”,然后按照茶几、沙发、书架的入口点一遍,拿到的必然是一整套北欧货。
Python 世界里最有名的例子就是标准库和 GUI 框架的关系。你写代码时调用的是tkinter的组件,但如果哪天想换成PyQt,只需要换掉底层创建组件的那一层,业务代码不用跟着改——这就是抽象工厂在真实项目里的价值。
2.3 和“工厂方法”的区别
很多人把工厂方法模式和抽象工厂模式搞混,这两者最核心的差异是:
- 工厂方法解决的是“一个产品如何被创建”的问题,它只关心一个对象的实例化。
- 抽象工厂解决的是“一组产品如何被成套创建”的问题,它关注的是产品族的完整性和兼容性。
打个比方,工厂方法是“单点餐厅”,你点一道菜,后厨做一道;抽象工厂是“套餐工厂”,你选了一个套餐,自动给你搭配前菜、主菜、甜品,保证口味风格一致。如果你的场景只需要创建一个对象,用抽象工厂就是杀鸡用牛刀;如果你的场景里存在“必须成套出现”的对象组,工厂方法则管不住这层约束。
3. 代码落地:一步步写出一个可运行的抽象工厂示例
理论讲再多不如看一段能跑的代码。下面我用最经典的 UI 主题系统做演示,完整代码可以直接复制去跑。为了让每一步都看得懂,我按照“先定义产品、再实现工厂、最后让客户端依赖抽象层”的顺序来写。
3.1 先定义抽象产品:让客户端只依赖接口
产品抽象层通常用标准库abc模块实现。这里的核心做法是:客户端永远不会直接跟具体产品类打交道,它只知道“这是个按钮”“这是个输入框”。
from abc import ABC, abstractmethod class Button(ABC): """按钮的抽象接口""" @abstractmethod def render(self) -> str: """渲染按钮""" @abstractmethod def on_click(self, callback) -> None: """绑定点击事件""" class TextInput(ABC): """输入框的抽象接口""" @abstractmethod def render(self) -> str: """渲染输入框""" @abstractmethod def get_value(self) -> str: """获取当前值""" class Checkbox(ABC): """复选框的抽象接口""" @abstractmethod def render(self) -> str: """渲染复选框""" @abstractmethod def is_checked(self) -> bool: """是否被选中"""这里我坚持用@abstractmethod而不是普通空方法,原因只有一个:强制约束。后续写具体产品时如果漏掉某个方法,类在实例化阶段就会直接报错,而不是等到运行时才发现“调了个不存在的方法”。这种“提前失败”的行为在设计模式落地时非常重要,能帮你把大量 bug 消灭在编译代码之前。
3.2 实现具体产品:Dark 与 Light 两套组件
接下来写两套具体产品。这里面的逻辑本身不复杂,但这是最容易写“薄”的部分,也是后续扩展的切入点。
class DarkButton(Button): def render(self) -> str: return "[Dark] 按钮已渲染" def on_click(self, callback) -> None: print("[Dark] 绑定点击事件:", callback.__name__) class DarkTextInput(TextInput): def render(self) -> str: return "[Dark] 输入框已渲染" def get_value(self) -> str: return "dark-text-value" class DarkCheckbox(Checkbox): def render(self) -> str: return "[Dark] 复选框已渲染" def is_checked(self) -> bool: return True class LightButton(Button): def render(self) -> str: return "[Light] 按钮已渲染" def on_click(self, callback) -> None: print("[Light] 绑定点击事件:", callback.__name__) class LightTextInput(TextInput): def render(self) -> str: return "[Light] 输入框已渲染" def get_value(self) -> str: return "light-text-value" class LightCheckbox(Checkbox): def render(self) -> str: return "[Light] 复选框已渲染" def is_checked(self) -> bool: return False每个类的实现很短,但请注意它们都完整实现了抽象产品里声明的所有方法。真正常见的问题不是方法写不完,而是有人为了省事,在某个具体产品里把方法写成了pass,结果运行时才炸。抽象基类能拦住一部分,但拦不住“实现了但是是空实现”的情况,这是 Python 的灵活性带来的代价,只能靠自觉和代码审查。
3.3 定义抽象工厂和两个具体工厂
工厂层是抽象工厂模式的核心。抽象工厂声明创建一组产品的方法,具体工厂则决定这一组产品到底落到哪套实现上。
class UIFactory(ABC): """抽象工厂:定义创建一组 UI 组件的方法""" @abstractmethod def create_button(self) -> Button: pass @abstractmethod def create_text_input(self) -> TextInput: pass @abstractmethod def create_checkbox(self) -> Checkbox: pass class DarkThemeFactory(UIFactory): def create_button(self) -> Button: return DarkButton() def create_text_input(self) -> TextInput: return DarkTextInput() def create_checkbox(self) -> Checkbox: return DarkCheckbox() class LightThemeFactory(UIFactory): def create_button(self) -> Button: return LightButton() def create_text_input(self) -> TextInput: return LightTextInput() def create_checkbox(self) -> Checkbox: return LightCheckbox()看到没有,具体工厂里没有丝毫业务判断逻辑。它的全部职责就是“组装返回一套风格统一的组件”。这个模块化带来的好处是:以后想要新增BlueThemeFactory,你不需要碰任何已存在的工厂代码,也不需要对客户端做任何改动,新增一个文件就完事了。
3.4 客户端调用方式:依赖倒置怎么体现
客户端代码是检验一个设计模式是否真的好用的最终标准。你看下面这段函数,完全不知道具体工厂是 dark 还是 light,它只接收一个UIFactory对象:
def build_ui(factory: UIFactory) -> None: button = factory.create_button() text_input = factory.create_text_input() checkbox = factory.create_checkbox() print(button.render()) print(text_input.render()) print(checkbox.render()) callback = lambda: print("clicked") button.on_click(callback) def main(theme: str) -> None: if theme == "dark": factory = DarkThemeFactory() elif theme == "light": factory = LightThemeFactory() else: raise ValueError(f"unknown theme: {theme}") build_ui(factory) if __name__ == "__main__": main("dark")注意main函数里还是有一个if/elif,但你仔细想一下:这个判断已经从“产品创建”的层面上升到了“工厂选择”的层面。全项目只有这一处选择,而不是原来那样每个组件创建都要判断一次。将来加一个blue主题,只需要在这个入口加一个分支,业务层代码一行不动。
3.5 两个容易踩的细节:抽象类的实例化和 mypy 配合
第一个细节:很多人第一次写抽象基类时,会试图直接实例化抽象类,然后发现报了TypeError。
>>> UIFactory() TypeError: Can't instantiate abstract class UIFactory with abstract method create_button这个报错是好事,它是抽象基类在主动保护你。如果连抽象类都能实例化,那“强制实现接口”就成了一句空话。
第二个细节:Python 是动态类型语言,但现在的项目基本都会配上 mypy 之类的类型检查。抽象工厂模式跟类型检查配合得非常好,因为所有工厂都返回标注好的抽象产品类型,mypy 能精确推断出你拿到的是Button而不是什么Any。这样调用端可以放心调用接口里声明的方法,不会因为拼错方法名而苦恼。
4. 选型边界:抽象工厂和工厂方法、普通工厂的取舍
写代码最怕的不是不会用某个模式,而是拿错了模式。抽象工厂确实能拯救混乱的 if/else,但你不能见一个分支就上抽象工厂,否则代码会被类文件淹没。我见过不少项目,模块总共不到 3000 行,却硬生生拆出十几个抽象类,维护成本比刚开始还高。
4.1 三种工厂类设计模式的能力对比
把它们放在一张表里看最直观:
| 对比项 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 创建对象数量 | 单个 | 单个 | 一组/产品族 |
| 产品间是否需要兼容 | 不需要 | 不需要 | 必须保证整套兼容 |
| 扩展新产品的改动量 | 改工厂函数,加分支 | 新增工厂子类 | 新增具体工厂,外加一组具体产品 |
| 客户端依赖对象 | 具体工厂类 | 抽象工厂父类 | 抽象工厂接口 |
| 典型应用 | 按文件后缀创建解析器 | 数据库单表单后端 | UI 主题、多套接口协议、组件库 |
简单工厂其实不是一个严格的设计模式,通常就是一个函数或类,内部写if/elif按参数返回不同对象。它最轻量,适合“今天可能只有一个对象要创建,以后不确定加不加”的情况。工厂方法在 Java 里见得最多,负责延迟创建对象,把“什么时候创建”的控制权交给子类。抽象工厂最重,但也是唯一能保证“产品族一致性”的方案。
4.2 真正适合抽象工厂的业务场景
判断标准很简单:你的系统里是否存在“不拆开使用的产品组”。满足这个条件,抽象工厂就值得用;反之,别硬上。我平时会按三种情况思考:
- UI 主题系统:按钮、输入框、弹窗必须成套出现,上文示例就是这类的简化版。
- 多平台 API 适配:比如客户端需要同时对接两家不同的支付接口,每家提供的请求签名、加密方式、回调校验都不一样,这些细节必须封装成完整的组合,漏配一个就会出现“下单成功但回调验签失败”的鬼问题。
- 同业务多套存储后端:有些项目需要同时支持 MySQL 和 PostgreSQL,且各自的事务处理、关联查询写法差异较大,整套数据访问层必须成套替换。
第三类我做过的项目里最典型。当时业务系统支持两种部署模式,一种是标准模式,另一种是轻量模式,两种模式里涉及报表、权限、工单三块核心逻辑。不抽抽象工厂的话,业务层代码里全是if mode == "standard"这种判断,看得人头大。
4.3 反面案例:过度设计如何毁掉维护性
我也踩过反面教材。有个小工具项目,总共只有一个“通知发送器”需要按渠道类型创建——短信、邮件、站内信,每种渠道只有一个类。当初为了“遵循设计模式最佳实践”,硬套了一层抽象工厂,结果为了三个渠道写了三层类结构,抽象产品、具体产品、抽象工厂、三个具体工厂,一共七八个文件。
后来新需求要在通知里加一个附件对象,我被迫为每个渠道新增一个附件产品类,同时修改所有工厂接口,白白改了一个下午。最后我删掉了抽象工厂,改成一个普通工厂函数加注册表,代码量直接砍半。这件事给我的教训是:设计模式的成本来自约束,收益也来自约束。当你的场景根本不需要“产品族一致性”约束时,抽象工厂的每个约束都是纯负担。
5. 生态化改造:注册表、依赖注入让抽象工厂更 Pythonic
抽象工厂模式本身是个经典模式,但原封不动地照搬往往不够“Pythonic”。Python 的装饰器、动态类型和dict字典能把这个模式变得更灵活,这也是很多 Python 项目落地抽象工厂时的通用做法。
5.1 用注册表工厂让新增家族零改动
传统抽象工厂的问题在哪里?新增一套产品时,你必须修改客户端入口的工厂创建函数,加一个elif。虽然只有一处,但总归动了已有代码。如果把“工厂注册”交给装饰器,你就能做到“新增一个文件就完成扩展”。
class UIFactoryRegistry: _factories: dict[str, type[UIFactory]] = {} @classmethod def register(cls, name: str): def decorator(factory_cls: type[UIFactory]): cls._factories[name] = factory_cls return factory_cls return decorator @classmethod def get(cls, name: str) -> UIFactory: try: factory_cls = cls._factories[name] except KeyError: raise ValueError(f"unknown UI factory: {name}") from None return factory_cls()然后给具体工厂加个装饰器,就能完成注册:
@UIFactoryRegistry.register("dark") class DarkThemeFactory(UIFactory): pass @UIFactoryRegistry.register("light") class LightThemeFactory(UIFactory): pass调用方不需要任何if/elif,一行代码就能拿到对应工厂:
factory = UIFactoryRegistry.get(theme) build_ui(factory)新增一个BlueThemeFactory时,只需要在菜单接口的外部新建一个文件,写上具体产品类、具体工厂类,然后打上@UIFactoryRegistry.register("blue")装饰器就行。客户端、注册表、已有工厂统统不用动。这个模式我愿称之为“插件式工厂”,在需要高频扩展的主题类、协议类项目里非常受用。
不过注册表也有代价:项目小的时侯,dict里到底注册了哪些工厂并不直观,IDE 跳转时也会绕一层。老项目用一个简单的模块级字典FACTORIES = {"dark": DarkThemeFactory}就足够了,不要迷信装饰器方案,够用才是真标准。
5.2 结合依赖注入的玩法
抽象工厂模式本身就已经实现了某种程度的依赖注入:调用方不创建具体对象,而是由工厂创建。但如果你的项目里已经用了依赖注入框架,比如dependency-injector或 FastAPI 的Depends,那抽象工厂还有更进阶的玩法——把工厂本身也交给容器管理。
这样设计的好处是:测试时可以往容器里替换一个假的工厂,返回一堆 mock 产品,完全不碰真实代码。你可以直接对build_ui写单元测试,而不需要真的去触发某个具体主题的分支。
def build_ui(factory: UIFactory) -> str: # 这段代码天然适合单测: # 传一个 fake_factory,断言返回值和调用顺序 return factory.create_button().render()我特别喜欢这个视角:抽象工厂让业务代码和具体实现解耦,依赖注入让工厂和组装方式解耦,两层解耦叠加之后,几乎每个模块都能独立测试。这套组合拳在大型项目里价值极大。
5.3 在动态扩展时保持类型安全
最后提醒一个注册表方案容易踩的坑。装饰器注册给动态扩展带来了方便,但也把类型信息藏进了字典,mypy 很难帮你检查出“某个工厂没实现create_button”这类问题。
我的实践是两条路并行:第一,注册表里的类型标注写清楚,_factories: dict[str, type[UIFactory]],这样 mypy 至少能保证“注册进字典的类必须继承自UIFactory”,漏实现方法的情况下该类无法实例化;第二,仍然坚持给具体工厂写完整的方法实现,用抽象类做约束,不要因为有了注册表就放松对接口完整性的要求。有人图省事,让具体工厂直接从object继承,抽象工厂的约束就全部失效了,运行时必踩漏方法的大坑。
我实际用下来还有一个更适合中小项目的替代方案:不需要@abstractmethod,改用typing.Protocol,配合@runtime_checkable做运行时校验。这样产品类甚至不需要继承抽象的Button,只要方法签名对上就行,同时通过isinstance(obj, ButtonProtocol)来校验。Python 生态的优势就在于,同一个模式可以按项目节奏选择不同的实现强度,完全不必拘泥于 GoF 里的原始结构。
说到底,设计模式的价值从来不是“背出四个角色”,而是帮你想清楚“这段关系里谁该依赖谁”。抽象工厂把“创建”和“使用”隔开,把“产品族”的约束固定在一个边界里。等你遇到十几个主题切换、多套协议适配、多后端存储切换的时候,就会明白这套结构的价值;在你只有一个产品要创建的时候,也别拿它折磨自己。先看需求,再谈模式,这比任何设计模式本身都重要。