03、Python - 抽象工厂模式

03、Python - 抽象工厂模式:从造车到跨平台架构,一篇讲透产品族的创建之道

本文为原创文章,如需转载,请联系作者获得授权,并注明出处。


文章目录

一、痛点场景:你是否也曾被"串味"的代码折磨?

1.1 场景一:跨平台UI的"风格混乱"

假设你正在开发一套跨平台的桌面应用,需要同时支持Windows和macOS。界面上有按钮、文本框、复选框三种组件。

新手最容易写出这样的代码:

python 复制代码
class WindowsButton:
    def render(self):
        print("渲染Windows风格按钮")

class MacButton:
    def render(self):
        print("渲染Mac风格按钮")

class WindowsTextField:
    def render(self):
        print("渲染Windows风格文本框")

class MacTextField:
    def render(self):
        print("渲染Mac风格文本框")

# 业务代码中到处都是if-else
def build_ui(platform):
    if platform == "windows":
        button = WindowsButton()
        text_field = WindowsTextField()
    elif platform == "mac":
        button = MacButton()
        text_field = MacTextField()
    button.render()
    text_field.render()

看起来没问题?等一下,当团队有5个开发者、20个页面、10种组件时,灾难就来了:

  • 开发者A在某个页面写了WindowsButton(),却配了MacTextField(),界面风格直接"串味"
  • 新增Linux平台时,需要在20个页面里逐个修改if-else,漏改一个就是线上bug
  • 组件从10种增加到15种时,每个平台的工厂都要改一遍

这就是典型的产品族一致性问题:一组相关的产品必须配套使用,但代码无法强制保证这一点。

1.2 场景二:数据库切换的"牵一发而动全身"

再看一个后端常见场景。你的系统需要支持MySQL和PostgreSQL两种数据库,每种数据库都有连接对象、命令对象、事务对象。

python 复制代码
# 糟糕的实现:业务代码直接依赖具体类
def save_user(user_data):
    conn = MySQLConnection("localhost", 3306, "mydb")
    cmd = MySQLCommand(conn)
    tx = MySQLTransaction(conn)
    tx.begin()
    try:
        cmd.execute("INSERT INTO users ...")
        tx.commit()
    except Exception:
        tx.rollback()

当客户要求切换到PostgreSQL时,你发现整个项目里有上百处这样的代码,每一处都要改。更可怕的是,有人可能混用了MySQLConnectionPostgreSQLCommand,运行时直接崩溃。

1.3 场景三:造车时的"零部件来源失控"

回到文章开头提到的造车案例。在普通工厂模式下,造一辆奔驰车时,如果不对零部件来源加以控制,可能出现:

  • 轮胎用了宝马的
  • 底盘用了奥迪的
  • 其他零件用了不知名小厂的

最终这辆"奔驰"开起来异响不断,4S店还推卸责任说"零件不是我们的"。

抽象工厂模式要解决的,正是这一类问题:当一组相关产品必须一起使用、且需要整体切换时,如何保证一致性并降低切换成本?


二、什么是抽象工厂模式?

2.1 专业解释

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供一个接口,用于创建一系列相关或相互依赖的对象(即一个"产品族"),而无需指定它们具体的类。

该模式围绕一个"超级工厂"创建其他工厂,这个超级工厂又被称为"工厂的工厂"。

核心要点:

  • 一个抽象工厂接口声明了多个创建方法,每个方法对应一种产品
  • 每个具体工厂实现该接口,负责创建一个完整产品族的所有产品
  • 客户端只依赖抽象工厂和抽象产品接口,不关心具体实现

2.2 大白话

说人话就是:抽象工厂就是一个"品牌总代理",它不直接生产产品,而是管着一堆代工厂。你告诉总代理"我要苹果全家桶",它就给你拿出iPhone、iPad、MacWatch、AirPods,全都是苹果品牌的,保证配套。你说"我要华为全家桶",它立刻换成华为的手机、平板、手表、耳机,风格统一,不会出现"苹果手机配华为充电器"这种尴尬场面。

普通工厂模式是"一个工厂造一种产品",抽象工厂模式是"一个工厂造一整套配套产品"。

2.3 生活案例

案例一:家具套装

你去宜家买卧室家具,有"北欧风格"和"中式风格"两个系列。每个系列都包含床、衣柜、床头柜、台灯。

  • 北欧工厂:生产北欧床、北欧衣柜、北欧床头柜、北欧台灯
  • 中式工厂:生产中式床、中式衣柜、中式床头柜、中式台灯

你选了北欧风格,导购员(抽象工厂)就给你配齐一整套北欧家具,不会出现"北欧床配中式衣柜"的混搭灾难。

案例二:餐饮套餐

麦当劳有"汉堡套餐"和"鸡肉卷套餐"。每个套餐都包含主食、小吃、饮料。

  • 汉堡套餐工厂:汉堡 + 薯条 + 可乐
  • 鸡肉卷套餐工厂:鸡肉卷 + 鸡块 + 橙汁

你点了汉堡套餐,收银员(抽象工厂)确保给你的是汉堡配薯条配可乐,而不是汉堡配鸡块配橙汁。

案例三:古代兵工厂

中国古代打仗,朝廷有"军械监"(抽象工厂),下设"弓弩坊"和"甲胄坊"等具体工坊。

  • 步兵装备工厂:步弓 + 皮甲 + 环首刀
  • 骑兵装备工厂:骑弓 + 铁甲 + 马槊

将军下令"装备骑兵",军械监就统一发放骑兵的一整套装备,保证弓、甲、兵器都是为骑兵设计的,不会给骑兵发一把步兵用的长戈(骑马根本挥不开)。


三、为什么要用抽象工厂模式?

3.1 保证产品族的一致性

这是抽象工厂最核心的价值。当一组产品被设计成一起使用时,抽象工厂从架构层面强制保证了"同族产品一起使用",避免了混搭导致的运行时错误或风格混乱。

比如数据库场景中,MySQLConnection只能搭配MySQLCommand,抽象工厂确保你不会拿到一个PostgreSQLCommand去操作MySQLConnection

3.2 切换产品族只需改一处

当需要从一个产品族切换到另一个时(比如从Windows风格切换到Mac风格,从MySQL切换到PostgreSQL),只需要替换具体工厂实例,业务代码完全不用改。

python 复制代码
# 切换前
factory = WindowsFactory()
# 切换后,只改这一行
factory = MacFactory()
# 下面的业务代码完全不变
button = factory.create_button()
text_field = factory.create_text_field()

3.3 客户端与具体实现解耦

客户端只依赖抽象工厂接口和抽象产品接口,完全不知道具体产品类的存在。这符合依赖倒置原则:高层模块不依赖低层模块,二者都依赖抽象。

3.4 有利于产品的一致性约束

当产品族内部有约束关系时(比如某种按钮只能搭配某种文本框),抽象工厂把这些约束封装在工厂内部,客户端无需了解这些复杂规则。


四、演进之路:从简单工厂到抽象工厂

抽象工厂不是凭空出现的,它是工厂模式不断演进的结果。让我们沿着时间线,看看它是怎么一步步进化来的。

4.1 第一阶段:直接new对象(原始时代)

最早的时候,程序员直接在业务代码里new对象:

python 复制代码
def order_pizza():
    pizza = CheesePizza()  # 直接依赖具体类
    pizza.bake()
    return pizza

问题:要换一种披萨,就得改业务代码,违反开闭原则。

4.2 第二阶段:简单工厂模式(青铜时代)

为了解决直接new的问题,人们把创建逻辑抽到一个工厂类里:

python 复制代码
class SimplePizzaFactory:
    def create_pizza(self, pizza_type):
        if pizza_type == "cheese":
            return CheesePizza()
        elif pizza_type == "pepperoni":
            return PepperoniPizza()

def order_pizza(factory, pizza_type):
    pizza = factory.create_pizza(pizza_type)
    pizza.bake()
    return pizza

优点:客户端不再直接new对象。

缺点:新增披萨类型时需要修改工厂类的if-else,违反开闭原则;一个工厂只能创建一种产品。

4.3 第三阶段:工厂方法模式(白银时代)

为了解决简单工厂违反开闭原则的问题,人们把工厂也抽象化,每个产品对应一个工厂:

python 复制代码
from abc import ABC, abstractmethod

class PizzaFactory(ABC):
    @abstractmethod
    def create_pizza(self):
        pass

class CheesePizzaFactory(PizzaFactory):
    def create_pizza(self):
        return CheesePizza()

class PepperoniPizzaFactory(PizzaFactory):
    def create_pizza(self):
        return PepperoniPizza()

优点:新增产品只需新增工厂,符合开闭原则。

缺点:一个工厂只能生产一种产品。当需要生产一组相关产品时,就需要管理大量工厂,而且无法保证产品之间的配套关系。

4.4 第四阶段:抽象工厂模式(黄金时代)

当业务发展到需要"一整套配套产品"时,抽象工厂模式应运而生:

python 复制代码
from abc import ABC, abstractmethod

class GUIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass

    @abstractmethod
    def create_text_field(self):
        pass

class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()
    def create_text_field(self):
        return WindowsTextField()

class MacFactory(GUIFactory):
    def create_button(self):
        return MacButton()
    def create_text_field(self):
        return MacTextField()

一个工厂管一整套产品,切换工厂就是切换整套风格。

4.5 三种工厂模式对比

对比维度 简单工厂 工厂方法 抽象工厂
核心思想 一个工厂根据参数创建不同产品 让子类决定实例化哪个产品 创建一系列相关产品族
产品数量 多种产品共用一个工厂 每个产品对应一个工厂 一个工厂生产一族产品
工厂接口方法数 1个(带参数) 1个(无参数) 多个(每种产品一个)
开闭原则 违反(新增产品改工厂) 符合(新增产品加工厂) 倾斜性符合(新增产品族容易,新增产品等级困难)
适用场景 产品少且稳定 单一产品类型,变体多 多产品配套使用,需整体切换
复杂度

五、核心概念:产品族与产品等级

理解抽象工厂模式,必须先搞清楚两个核心概念:产品族产品等级结构

5.1 产品等级结构

产品等级结构是指同一类产品的继承结构,即抽象产品和它的具体实现构成的层次。

比如:

  • 按钮(抽象产品)← Windows按钮 / Mac按钮 / Linux按钮(具体产品)
  • 文本框(抽象产品)← Windows文本框 / Mac文本框 / Linux文本框
  • 空调(抽象产品)← 美的空调 / 格力空调 / 海尔空调

产品等级结构是纵向的,描述的是"同一种东西的不同品牌/实现"。

5.2 产品族

产品族是指由同一个具体工厂生产的、位于不同产品等级结构中的一组产品

比如:

  • Windows产品族:Windows按钮 + Windows文本框 + Windows复选框
  • Mac产品族:Mac按钮 + Mac文本框 + Mac复选框
  • 美的产品族:美的空调 + 美的冰箱 + 美的洗衣机

产品族是横向的,描述的是"同一个品牌下的不同种类产品"。

5.3 一张图看懂

复制代码
产品等级1(按钮)    产品等级2(文本框)    产品等级3(复选框)
     │                     │                      │
     ├─ Windows按钮        ├─ Windows文本框       ├─ Windows复选框  ← Windows产品族
     ├─ Mac按钮            ├─ Mac文本框           ├─ Mac复选框      ← Mac产品族
     └─ Linux按钮          └─ Linux文本框         └─ Linux复选框    ← Linux产品族
  • 纵向看:每一列是一个产品等级结构
  • 横向看:每一行是一个产品族
  • 抽象工厂:每一行对应一个具体工厂

5.4 关键洞察

抽象工厂模式的本质是:用工厂来隔离产品族,确保同一族的产品被一起使用。

  • 新增一个产品族(比如新增Linux平台):只需新增一个具体工厂,符合开闭原则
  • 新增一个产品等级(比如新增"滚动条"组件):需要修改所有具体工厂,违反开闭原则

这就是抽象工厂模式的"倾斜性开闭原则":对产品族的扩展开放,对产品等级的扩展关闭。


六、四大角色与UML类图

6.1 四大角色

抽象工厂模式包含四个核心角色:

角色 职责 对应代码
抽象工厂(AbstractFactory) 声明创建抽象产品对象的接口,包含多个创建方法 GUIFactory
具体工厂(ConcreteFactory) 实现抽象工厂接口,生成一个产品族的所有具体产品 WindowsFactoryMacFactory
抽象产品(AbstractProduct) 为每种产品声明接口 ButtonTextField
具体产品(ConcreteProduct) 定义具体工厂生产的具体产品对象,实现抽象产品接口 WindowsButtonMacButton

6.2 客户端如何使用

客户端(Client)只使用抽象工厂和抽象产品声明的接口,完全不接触具体类:

python 复制代码
def client_code(factory: GUIFactory):
    button = factory.create_button()
    text_field = factory.create_text_field()
    button.render()
    text_field.render()

客户端拿到的是抽象工厂,调用它的创建方法得到抽象产品,然后调用抽象产品的方法。整个过程中,客户端不知道也不关心具体是Windows还是Mac。


七、怎么用:Python标准实现

7.1 使用abc模块定义抽象基类

Python中实现抽象工厂模式,标准做法是使用abc模块(Abstract Base Classes)。abc.ABC是抽象基类,@abstractmethod装饰器声明抽象方法。

7.2 完整实现:跨平台UI组件

下面以跨平台UI组件为例,给出完整的Python实现:

python 复制代码
from abc import ABC, abstractmethod


# ========== 抽象产品 ==========

class Button(ABC):
    """抽象产品:按钮"""

    @abstractmethod
    def render(self) -> None:
        pass

    @abstractmethod
    def click(self) -> None:
        pass


class TextField(ABC):
    """抽象产品:文本框"""

    @abstractmethod
    def render(self) -> None:
        pass

    @abstractmethod
    def set_text(self, text: str) -> None:
        pass


class Checkbox(ABC):
    """抽象产品:复选框"""

    @abstractmethod
    def render(self) -> None:
        pass

    @abstractmethod
    def toggle(self) -> None:
        pass


# ========== 具体产品:Windows风格 ==========

class WindowsButton(Button):
    def render(self):
        print("[Windows] 渲染方形按钮")

    def click(self):
        print("[Windows] 按钮被点击,发出'咔'的声音")


class WindowsTextField(TextField):
    def render(self):
        print("[Windows] 渲染直角文本框")

    def set_text(self, text: str):
        print(f"[Windows] 文本框内容设置为: {text}")


class WindowsCheckbox(Checkbox):
    def __init__(self):
        self.checked = False

    def render(self):
        status = "勾选" if self.checked else "未勾选"
        print(f"[Windows] 渲染方形复选框 [{status}]")

    def toggle(self):
        self.checked = not self.checked
        print(f"[Windows] 复选框切换为: {'勾选' if self.checked else '未勾选'}")


# ========== 具体产品:Mac风格 ==========

class MacButton(Button):
    def render(self):
        print("[Mac] 渲染圆角按钮")

    def click(self):
        print("[Mac] 按钮被点击,触感反馈")


class MacTextField(TextField):
    def render(self):
        print("[Mac] 渲染圆角文本框")

    def set_text(self, text: str):
        print(f"[Mac] 文本框内容设置为: {text}")


class MacCheckbox(Checkbox):
    def __init__(self):
        self.checked = False

    def render(self):
        status = "勾选" if self.checked else "未勾选"
        print(f"[Mac] 渲染圆形复选框 [{status}]")

    def toggle(self):
        self.checked = not self.checked
        print(f"[Mac] 复选框切换为: {'勾选' if self.checked else '未勾选'}")


# ========== 抽象工厂 ==========

class GUIFactory(ABC):
    """抽象工厂:GUI组件工厂"""

    @abstractmethod
    def create_button(self) -> Button:
        pass

    @abstractmethod
    def create_text_field(self) -> TextField:
        pass

    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass


# ========== 具体工厂 ==========

class WindowsFactory(GUIFactory):
    """Windows风格组件工厂"""

    def create_button(self) -> Button:
        return WindowsButton()

    def create_text_field(self) -> TextField:
        return WindowsTextField()

    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()


class MacFactory(GUIFactory):
    """Mac风格组件工厂"""

    def create_button(self) -> Button:
        return MacButton()

    def create_text_field(self) -> TextField:
        return MacTextField()

    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()


# ========== 客户端代码 ==========

def build_login_page(factory: GUIFactory):
    """使用抽象工厂构建登录页面,完全不依赖具体平台"""
    print("=== 开始构建登录页面 ===")
    username_field = factory.create_text_field()
    password_field = factory.create_text_field()
    remember_checkbox = factory.create_checkbox()
    login_button = factory.create_button()

    username_field.render()
    username_field.set_text("user@example.com")
    password_field.render()
    password_field.set_text("********")
    remember_checkbox.render()
    remember_checkbox.toggle()
    login_button.render()
    login_button.click()
    print("=== 登录页面构建完成 ===\n")


if __name__ == "__main__":
    print("---------- 使用Windows风格 ----------")
    build_login_page(WindowsFactory())

    print("---------- 使用Mac风格 ----------")
    build_login_page(MacFactory())

运行结果:

复制代码
---------- 使用Windows风格 ----------
=== 开始构建登录页面 ===
[Windows] 渲染直角文本框
[Windows] 文本框内容设置为: user@example.com
[Windows] 渲染直角文本框
[Windows] 文本框内容设置为: ********
[Windows] 渲染方形复选框 [未勾选]
[Windows] 复选框切换为: 勾选
[Windows] 渲染方形按钮
[Windows] 按钮被点击,发出'咔'的声音
=== 登录页面构建完成 ===

---------- 使用Mac风格 ----------
=== 开始构建登录页面 ===
[Mac] 渲染圆角文本框
[Mac] 文本框内容设置为: user@example.com
[Mac] 渲染圆角文本框
[Mac] 文本框内容设置为: ********
[Mac] 渲染圆形复选框 [未勾选]
[Mac] 复选框切换为: 勾选
[Mac] 渲染圆角按钮
[Mac] 按钮被点击,触感反馈
=== 登录页面构建完成 ===

7.3 代码解读

  1. 抽象产品层ButtonTextFieldCheckbox 三个抽象基类,定义了每种组件的接口
  2. 具体产品层:每个抽象产品有Windows和Mac两个具体实现
  3. 抽象工厂层GUIFactory 声明了三个创建方法,分别对应三种产品
  4. 具体工厂层WindowsFactoryMacFactory 分别生产各自风格的一整套组件
  5. 客户端build_login_page 只接收 GUIFactory 抽象类型,完全不知道具体平台

切换平台时,只需把 WindowsFactory() 换成 MacFactory(),业务代码一行都不用改。


八、常用场景教学

8.1 场景一:跨平台UI工具包

这是抽象工厂模式最经典的应用场景。Java的Swing、Python的Tkinter、Qt框架等都在底层使用了类似的思想。

适用条件

  • 同一套业务逻辑需要运行在多个操作系统上
  • 每个平台的UI组件外观和行为不同
  • 同一页面的组件必须保持同一种平台风格

实现要点

  • 抽象工厂定义create_button()create_text_field()create_menu()等方法
  • 每个平台一个具体工厂
  • 客户端通过配置或环境变量选择具体工厂

8.2 场景二:多主题/换肤系统

很多应用支持"亮色主题"和"暗色主题",甚至更多自定义主题。每个主题下,按钮颜色、背景色、文字颜色、边框样式都是配套的。

python 复制代码
class ThemeFactory(ABC):
    @abstractmethod
    def create_button_style(self):
        pass

    @abstractmethod
    def create_background_style(self):
        pass

    @abstractmethod
    def create_text_style(self):
        pass

class LightThemeFactory(ThemeFactory):
    def create_button_style(self):
        return {"bg": "#007AFF", "text": "#FFFFFF"}
    def create_background_style(self):
        return {"bg": "#FFFFFF"}
    def create_text_style(self):
        return {"color": "#000000"}

class DarkThemeFactory(ThemeFactory):
    def create_button_style(self):
        return {"bg": "#0A84FF", "text": "#FFFFFF"}
    def create_background_style(self):
        return {"bg": "#1C1C1E"}
    def create_text_style(self):
        return {"color": "#FFFFFF"}

切换主题就是切换工厂,保证所有元素的颜色风格一致,不会出现"亮色背景配暗色文字"的可读性灾难。

8.3 场景三:数据库访问层

当系统需要支持多种数据库(MySQL、PostgreSQL、Oracle、SQLite)时,每种数据库都有自己的连接对象、命令对象、事务对象、数据读取器。

为什么用抽象工厂而不是工厂方法?

因为连接、命令、事务之间有依赖关系:MySQL的命令对象只能操作MySQL的连接,PostgreSQL的命令只能操作PostgreSQL的连接。抽象工厂确保你拿到的是配套的一整套数据库对象。

python 复制代码
class DatabaseFactory(ABC):
    @abstractmethod
    def create_connection(self, host, port, db_name):
        pass

    @abstractmethod
    def create_command(self, connection):
        pass

    @abstractmethod
    def create_transaction(self, connection):
        pass

8.4 场景四:云服务SDK抽象

企业应用经常需要对接多家云服务商(AWS、Azure、阿里云、腾讯云),每家都有对象存储、云服务器、数据库、CDN等服务。

抽象工厂可以把"云服务商"作为产品族,把"对象存储、云服务器、数据库"作为产品等级:

python 复制代码
class CloudFactory(ABC):
    @abstractmethod
    def create_object_storage(self):
        pass

    @abstractmethod
    def create_vm(self):
        pass

    @abstractmethod
    def create_database(self):
        pass

class AliyunFactory(CloudFactory):
    def create_object_storage(self):
        return OSSStorage()
    def create_vm(self):
        return ECSServer()
    def create_database(self):
        return RDSDatabase()

class AWSFactory(CloudFactory):
    def create_object_storage(self):
        return S3Storage()
    def create_vm(self):
        return EC2Server()
    def create_database(self):
        return RDSEngine()

切换云服务商只需替换工厂,业务代码完全不变。

8.5 场景五:游戏开发中的种族/阵营系统

在策略游戏或RPG游戏中,不同种族(人类、兽人、精灵)有不同的兵种、建筑、科技。每个种族的兵种和建筑是配套的:人类农场只能给人类提供食物,兽人地洞只能给兽人提供人口。

python 复制代码
class RaceFactory(ABC):
    @abstractmethod
    def create_warrior(self):
        pass

    @abstractmethod
    def create_mage(self):
        pass

    @abstractmethod
    def create_building(self):
        pass

class HumanFactory(RaceFactory):
    def create_warrior(self):
        return Footman()
    def create_mage(self):
        return Sorceress()
    def create_building(self):
        return TownHall()

class OrcFactory(RaceFactory):
    def create_warrior(self):
        return Grunt()
    def create_mage(self):
        return Shaman()
    def create_building(self):
        return GreatHall()

8.6 场景六:支付网关系统

电商平台需要支持多种支付方式(支付宝、微信支付、银联、PayPal),每种支付方式都有支付接口、退款接口、对账接口、查询接口。这些接口必须来自同一个支付渠道,不能用支付宝的支付接口配微信的退款接口。

python 复制代码
class PaymentFactory(ABC):
    @abstractmethod
    def create_pay_service(self):
        pass

    @abstractmethod
    def create_refund_service(self):
        pass

    @abstractmethod
    def create_reconcile_service(self):
        pass

class AlipayFactory(PaymentFactory):
    def create_pay_service(self):
        return AlipayPayService()
    def create_refund_service(self):
        return AlipayRefundService()
    def create_reconcile_service(self):
        return AlipayReconcileService()

class WeChatPayFactory(PaymentFactory):
    def create_pay_service(self):
        return WeChatPayService()
    def create_refund_service(self):
        return WeChatRefundService()
    def create_reconcile_service(self):
        return WeChatReconcileService()

九、企业项目实战:多云存储抽象工厂

上面的例子偏教学,下面给一个真正能在企业项目中使用的完整案例:多云存储抽象工厂

9.1 业务背景

某创业公司的产品需要支持文件存储,最初只用阿里云OSS。后来为了降低成本和避免厂商锁定,需要同时支持腾讯云COS和AWS S3。

每种云存储都有三个核心操作:

  • 上传文件
  • 下载文件
  • 删除文件
  • 生成临时访问URL

而且,每种云存储的配置(密钥、区域、桶名)和客户端初始化方式都不同。

9.2 不使用抽象工厂的痛点

python 复制代码
# 糟糕的实现:业务代码里到处判断云厂商
def upload_file(cloud_type, file_path, key):
    if cloud_type == "aliyun":
        client = AliyunOSSClient(access_key, secret_key, endpoint)
        client.put_object(bucket_name, key, file_path)
    elif cloud_type == "tencent":
        client = TencentCOSClient(secret_id, secret_key, region)
        client.upload_file(bucket_name, key, file_path)
    elif cloud_type == "aws":
        client = AWSS3Client(aws_access_key_id, aws_secret_access_key, region_name)
        client.upload_file(bucket_name, key, file_path)

问题:

  • 每个操作(上传、下载、删除、生成URL)都要写一遍if-else
  • 新增云厂商时,所有操作都要修改
  • 配置管理混乱,不同云的密钥散落在各处
  • 无法统一做日志、监控、重试

9.3 抽象工厂实现

python 复制代码
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any
import datetime


# ========== 抽象产品:存储客户端 ==========

class StorageClient(ABC):
    """抽象产品:统一的存储客户端接口"""

    @abstractmethod
    def upload(self, key: str, file_path: str) -> str:
        """上传文件,返回文件的唯一标识"""
        pass

    @abstractmethod
    def download(self, key: str, save_path: str) -> bool:
        """下载文件到本地路径"""
        pass

    @abstractmethod
    def delete(self, key: str) -> bool:
        """删除文件"""
        pass

    @abstractmethod
    def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
        """生成临时访问URL"""
        pass


# ========== 具体产品:阿里云OSS ==========

class AliyunOSSStorage(StorageClient):
    def __init__(self, access_key_id: str, access_key_secret: str,
                 endpoint: str, bucket_name: str):
        self.access_key_id = access_key_id
        self.access_key_secret = access_key_secret
        self.endpoint = endpoint
        self.bucket_name = bucket_name
        # 实际项目中这里初始化 oss2.Bucket
        print(f"[AliyunOSS] 初始化客户端, bucket={bucket_name}, endpoint={endpoint}")

    def upload(self, key: str, file_path: str) -> str:
        print(f"[AliyunOSS] 上传文件: {file_path} -> {key}")
        # 实际调用: self.bucket.put_object(key, file_path)
        return f"oss://{self.bucket_name}/{key}"

    def download(self, key: str, save_path: str) -> bool:
        print(f"[AliyunOSS] 下载文件: {key} -> {save_path}")
        # 实际调用: self.bucket.get_object_to_file(key, save_path)
        return True

    def delete(self, key: str) -> bool:
        print(f"[AliyunOSS] 删除文件: {key}")
        # 实际调用: self.bucket.delete_object(key)
        return True

    def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
        print(f"[AliyunOSS] 生成临时URL: {key}, 有效期{expires_in}秒")
        # 实际调用: self.bucket.sign_url('GET', key, expires_in)
        return f"https://{self.bucket_name}.{self.endpoint}/{key}?Expires=xxx"


# ========== 具体产品:腾讯云COS ==========

class TencentCOSStorage(StorageClient):
    def __init__(self, secret_id: str, secret_key: str,
                 region: str, bucket_name: str):
        self.secret_id = secret_id
        self.secret_key = secret_key
        self.region = region
        self.bucket_name = bucket_name
        # 实际项目中这里初始化 cos_config + CosS3Client
        print(f"[TencentCOS] 初始化客户端, bucket={bucket_name}, region={region}")

    def upload(self, key: str, file_path: str) -> str:
        print(f"[TencentCOS] 上传文件: {file_path} -> {key}")
        # 实际调用: self.client.upload_file(Bucket=self.bucket_name, Key=key, LocalFilePath=file_path)
        return f"cos://{self.bucket_name}/{key}"

    def download(self, key: str, save_path: str) -> bool:
        print(f"[TencentCOS] 下载文件: {key} -> {save_path}")
        # 实际调用: self.client.download_file(Bucket=self.bucket_name, Key=key, DestFilePath=save_path)
        return True

    def delete(self, key: str) -> bool:
        print(f"[TencentCOS] 删除文件: {key}")
        # 实际调用: self.client.delete_object(Bucket=self.bucket_name, Key=key)
        return True

    def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
        print(f"[TencentCOS] 生成临时URL: {key}, 有效期{expires_in}秒")
        # 实际调用: self.client.get_presigned_url(Method='GET', Bucket=self.bucket_name, Key=key, Expired=expires_in)
        return f"https://{self.bucket_name}.cos.{self.region}.myqcloud.com/{key}?sign=xxx"


# ========== 具体产品:AWS S3 ==========

class AWSS3Storage(StorageClient):
    def __init__(self, aws_access_key_id: str, aws_secret_access_key: str,
                 region_name: str, bucket_name: str):
        self.aws_access_key_id = aws_access_key_id
        self.aws_secret_access_key = aws_secret_access_key
        self.region_name = region_name
        self.bucket_name = bucket_name
        # 实际项目中这里初始化 boto3.client('s3')
        print(f"[AWSS3] 初始化客户端, bucket={bucket_name}, region={region_name}")

    def upload(self, key: str, file_path: str) -> str:
        print(f"[AWSS3] 上传文件: {file_path} -> {key}")
        # 实际调用: self.s3.upload_file(file_path, self.bucket_name, key)
        return f"s3://{self.bucket_name}/{key}"

    def download(self, key: str, save_path: str) -> bool:
        print(f"[AWSS3] 下载文件: {key} -> {save_path}")
        # 实际调用: self.s3.download_file(self.bucket_name, key, save_path)
        return True

    def delete(self, key: str) -> bool:
        print(f"[AWSS3] 删除文件: {key}")
        # 实际调用: self.s3.delete_object(Bucket=self.bucket_name, Key=key)
        return True

    def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
        print(f"[AWSS3] 生成临时URL: {key}, 有效期{expires_in}秒")
        # 实际调用: self.s3.generate_presigned_url('get_object', Params={'Bucket': self.bucket_name, 'Key': key}, ExpiresIn=expires_in)
        return f"https://{self.bucket_name}.s3.{self.region_name}.amazonaws.com/{key}?X-Amz-Signature=xxx"


# ========== 抽象工厂 ==========

class StorageFactory(ABC):
    """抽象工厂:存储服务工厂"""

    @abstractmethod
    def create_storage(self) -> StorageClient:
        """创建存储客户端"""
        pass

    @abstractmethod
    def get_provider_name(self) -> str:
        """获取云服务商名称"""
        pass


# ========== 具体工厂 ==========

class AliyunOSSFactory(StorageFactory):
    def __init__(self, config: Dict[str, Any]):
        self.config = config

    def create_storage(self) -> StorageClient:
        return AliyunOSSStorage(
            access_key_id=self.config["access_key_id"],
            access_key_secret=self.config["access_key_secret"],
            endpoint=self.config["endpoint"],
            bucket_name=self.config["bucket_name"]
        )

    def get_provider_name(self) -> str:
        return "aliyun_oss"


class TencentCOSFactory(StorageFactory):
    def __init__(self, config: Dict[str, Any]):
        self.config = config

    def create_storage(self) -> StorageClient:
        return TencentCOSStorage(
            secret_id=self.config["secret_id"],
            secret_key=self.config["secret_key"],
            region=self.config["region"],
            bucket_name=self.config["bucket_name"]
        )

    def get_provider_name(self) -> str:
        return "tencent_cos"


class AWSS3Factory(StorageFactory):
    def __init__(self, config: Dict[str, Any]):
        self.config = config

    def create_storage(self) -> StorageClient:
        return AWSS3Storage(
            aws_access_key_id=self.config["aws_access_key_id"],
            aws_secret_access_key=self.config["aws_secret_access_key"],
            region_name=self.config["region_name"],
            bucket_name=self.config["bucket_name"]
        )

    def get_provider_name(self) -> str:
        return "aws_s3"


# ========== 工厂注册中心(企业级增强) ==========

class StorageFactoryRegistry:
    """工厂注册中心:通过配置动态选择工厂,支持运行时扩展"""

    _factories: Dict[str, type] = {}

    @classmethod
    def register(cls, provider_name: str, factory_class: type):
        """注册新的云服务商工厂"""
        cls._factories[provider_name] = factory_class
        print(f"[Registry] 注册云服务商: {provider_name}")

    @classmethod
    def create_factory(cls, provider_name: str, config: Dict[str, Any]) -> StorageFactory:
        """根据配置创建具体工厂"""
        factory_class = cls._factories.get(provider_name)
        if not factory_class:
            raise ValueError(f"不支持的云服务商: {provider_name}, 已注册: {list(cls._factories.keys())}")
        return factory_class(config)


# 注册内置工厂
StorageFactoryRegistry.register("aliyun_oss", AliyunOSSFactory)
StorageFactoryRegistry.register("tencent_cos", TencentCOSFactory)
StorageFactoryRegistry.register("aws_s3", AWSS3Factory)


# ========== 业务层:文件服务 ==========

class FileService:
    """业务层文件服务,完全依赖抽象接口"""

    def __init__(self, storage: StorageClient):
        self.storage = storage

    def upload_avatar(self, user_id: int, file_path: str) -> str:
        key = f"avatars/{user_id}/{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.jpg"
        result = self.storage.upload(key, file_path)
        url = self.storage.generate_presigned_url(key, expires_in=86400)
        print(f"[FileService] 用户{user_id}头像上传成功, 访问URL: {url}")
        return url

    def delete_avatar(self, user_id: int, key: str):
        self.storage.delete(key)
        print(f"[FileService] 用户{user_id}头像已删除")


# ========== 客户端使用 ==========

def main():
    # 模拟从配置文件读取
    config = {
        "provider": "aliyun_oss",  # 改成 "tencent_cos" 或 "aws_s3" 即可切换
        "aliyun_oss": {
            "access_key_id": "your_access_key",
            "access_key_secret": "your_secret",
            "endpoint": "oss-cn-guangzhou.aliyuncs.com",
            "bucket_name": "my-app-bucket"
        },
        "tencent_cos": {
            "secret_id": "your_secret_id",
            "secret_key": "your_secret_key",
            "region": "ap-guangzhou",
            "bucket_name": "my-app-bucket-1250000000"
        },
        "aws_s3": {
            "aws_access_key_id": "your_aws_key",
            "aws_secret_access_key": "your_aws_secret",
            "region_name": "ap-southeast-1",
            "bucket_name": "my-app-bucket"
        }
    }

    # 根据配置创建工厂
    provider = config["provider"]
    factory = StorageFactoryRegistry.create_factory(provider, config[provider])
    print(f"当前使用云服务商: {factory.get_provider_name()}\n")

    # 创建存储客户端
    storage = factory.create_storage()

    # 业务层使用
    file_service = FileService(storage)
    file_service.upload_avatar(user_id=1001, file_path="/tmp/avatar.jpg")
    print()

    # 切换到腾讯云(只改配置,不改业务代码)
    print("=" * 50)
    print("切换到腾讯云COS:")
    print("=" * 50)
    factory2 = StorageFactoryRegistry.create_factory("tencent_cos", config["tencent_cos"])
    storage2 = factory2.create_storage()
    file_service2 = FileService(storage2)
    file_service2.upload_avatar(user_id=1002, file_path="/tmp/avatar2.jpg")


if __name__ == "__main__":
    main()

9.4 企业级设计要点

  1. 工厂注册中心 :通过StorageFactoryRegistry实现工厂的动态注册和发现,新增云服务商只需调用register(),无需修改已有代码
  2. 配置驱动:通过配置文件指定使用哪个云服务商,切换云厂商无需重新部署
  3. 业务层隔离FileService只依赖StorageClient抽象接口,完全不知道底层是阿里云还是AWS
  4. 统一接口:四种操作(上传、下载、删除、生成URL)统一接口,业务代码无需关心各云厂商的API差异
  5. 可测试性 :可以轻松Mock一个StorageClient用于单元测试

9.5 如何扩展新的云服务商

假设现在要新增华为云OBS,只需三步:

python 复制代码
# 第一步:实现具体产品
class HuaweiOBSStorage(StorageClient):
    def __init__(self, ak, sk, server, bucket_name):
        # 初始化 obs.ObsClient
        pass
    def upload(self, key, file_path):
        pass  # 实现华为云上传逻辑
    def download(self, key, save_path):
        pass
    def delete(self, key):
        pass
    def generate_presigned_url(self, key, expires_in=3600):
        pass

# 第二步:实现具体工厂
class HuaweiOBSFactory(StorageFactory):
    def __init__(self, config):
        self.config = config
    def create_storage(self):
        return HuaweiOBSStorage(
            ak=self.config["ak"],
            sk=self.config["sk"],
            server=self.config["server"],
            bucket_name=self.config["bucket_name"]
        )
    def get_provider_name(self):
        return "huawei_obs"

# 第三步:注册到工厂中心
StorageFactoryRegistry.register("huawei_obs", HuaweiOBSFactory)

完成!已有业务代码一行都不用改,配置里把provider改成huawei_obs即可使用。


十、优缺点分析

10.1 优点

1. 保证产品族的一致性

抽象工厂从架构层面确保同一产品族的对象一起使用,避免了混搭导致的运行时错误。这是它最核心、不可替代的价值。

2. 切换产品族极其方便

从一个产品族切换到另一个,只需替换具体工厂实例,业务代码完全不变。这对于需要支持多平台、多主题、多数据库的系统来说,维护成本大幅降低。

3. 客户端与具体实现解耦

客户端只依赖抽象接口,符合依赖倒置原则。具体产品类的变化不会影响客户端代码。

4. 有利于产品的一致性约束

产品族内部的约束关系(如某种数据库的命令只能搭配某种连接)被封装在工厂内部,客户端无需了解这些复杂规则。

5. 符合开闭原则(产品族维度)

新增一个产品族时,只需新增一个具体工厂,无需修改已有代码,符合开闭原则。

10.2 缺点

1. 新增产品等级困难(倾斜性开闭原则)

这是抽象工厂模式最大的痛点。如果需要新增一种产品(比如UI组件从按钮、文本框、复选框增加到滚动条),就需要:

  • 修改抽象工厂接口,新增create_scrollbar()方法
  • 修改所有具体工厂,实现这个新方法
  • 可能影响所有已有的客户端代码

这违反了开闭原则。因此,抽象工厂模式适用于产品等级结构稳定的场景。

2. 类的数量增多,复杂度上升

每个产品族需要一个具体工厂,每个产品需要一个抽象产品和多个具体产品。当产品族和产品等级都很多时,类的数量会急剧增加,系统复杂度上升。

3. 抽象层增加,理解成本提高

对于新手来说,抽象工厂模式的多层抽象(抽象工厂、具体工厂、抽象产品、具体产品)不如直接new对象直观,学习和理解成本较高。

4. 过度设计风险

如果产品族只有一个,或者产品之间没有配套使用的约束,使用抽象工厂模式就是过度设计,反而增加了不必要的复杂度。

10.3 适用场景总结

适合使用抽象工厂模式的场景:

  • 系统需要独立于产品的创建、组合和表示
  • 系统中有多个产品族,且每次只使用其中一个产品族
  • 属于同一个产品族的产品被设计成一起使用,且必须保证一致性
  • 产品等级结构相对稳定,不会频繁新增产品类型
  • 需要提供一个产品类库,但只想显示接口而隐藏实现

不适合使用的场景:

  • 产品等级结构会频繁变化(经常新增产品类型)
  • 产品之间没有配套使用的约束
  • 只有一个产品族,没有切换需求
  • 简单的对象创建场景

十一、面试官高频面试题

面试题1:简单工厂、工厂方法、抽象工厂有什么区别?

参考答案:

三者都是创建型模式,核心都是"把对象创建的逻辑封装起来,让客户端不直接依赖具体类",但适用场景和复杂度不同:

维度 简单工厂 工厂方法 抽象工厂
产品数量 一个工厂创建多种产品 一个工厂创建一种产品 一个工厂创建一族产品
工厂接口 一个带参数的创建方法 一个无参数的创建方法 多个创建方法(每种产品一个)
开闭原则 违反(新增产品改工厂) 符合(新增产品加工厂) 倾斜性符合(新增产品族容易,新增产品等级困难)
核心解决的问题 封装创建逻辑,避免直接new 解耦创建与使用,支持扩展 保证产品族一致性,支持整体切换
复杂度

一句话总结:简单工厂是"一个工厂造所有东西",工厂方法是"每个东西有自己的工厂",抽象工厂是"一个工厂造一整套配套的东西"。

面试题2:什么是产品族和产品等级?抽象工厂模式如何处理这两个概念?

参考答案:

  • 产品等级结构:同一类产品的继承结构,是纵向的。比如"按钮"是抽象产品,"Windows按钮"、"Mac按钮"、"Linux按钮"是具体产品,它们构成一个产品等级结构。
  • 产品族:同一个具体工厂生产的、位于不同产品等级结构中的一组产品,是横向的。比如"Windows产品族"包含Windows按钮、Windows文本框、Windows复选框。

抽象工厂模式中:

  • 每个抽象产品对应一个产品等级结构
  • 每个具体工厂对应一个产品族
  • 抽象工厂接口中的每个创建方法对应一个产品等级

新增产品族(新增一个平台)只需新增具体工厂,符合开闭原则;新增产品等级(新增一种组件)需要修改所有工厂,违反开闭原则。这就是"倾斜性开闭原则"。

面试题3:抽象工厂模式的优缺点是什么?

参考答案:

优点:

  1. 保证产品族的一致性,避免混搭使用导致的错误
  2. 切换产品族只需替换工厂,业务代码不变
  3. 客户端与具体实现解耦,符合依赖倒置原则
  4. 产品族内部的约束关系封装在工厂中,客户端无需关心
  5. 新增产品族符合开闭原则

缺点:

  1. 新增产品等级困难,需要修改所有工厂(倾斜性开闭原则)
  2. 类的数量多,系统复杂度高
  3. 多层抽象,理解成本高
  4. 产品族单一或无配套约束时,属于过度设计

面试题4:抽象工厂模式和工厂方法模式的根本区别是什么?

参考答案:

根本区别在于处理的产品等级数量不同

  • 工厂方法模式只处理一个产品等级结构,抽象工厂接口只有一个创建方法。它关注的是"一种产品的不同实现"。
  • 抽象工厂模式处理多个产品等级结构,抽象工厂接口有多个创建方法。它关注的是"一族配套产品的整体创建和一致性保证"。

另外,工厂方法模式通过继承来实现创建(子类重写工厂方法),而抽象工厂模式通过组合来实现创建(具体工厂组合了多个产品的创建逻辑)。

面试题5:什么时候该用抽象工厂模式,什么时候不该用?

参考答案:

该用的场景:

  1. 系统中有多个产品族,且每次只使用其中一个
  2. 同族产品必须一起使用,有一致性约束
  3. 产品等级结构稳定,不会频繁新增产品类型
  4. 需要隐藏产品创建和表示的细节
  5. 需要支持整体切换(如换平台、换主题、换数据库)

不该用的场景:

  1. 只有一个产品族,没有切换需求
  2. 产品之间没有配套使用的约束
  3. 产品等级结构会频繁变化
  4. 简单的对象创建,用简单工厂或直接new更合适

面试题6:抽象工厂模式如何解决"开闭原则的倾斜性"问题?

参考答案:

抽象工厂模式的"倾斜性开闭原则"是指:对产品族的扩展开放,对产品等级的扩展关闭。新增产品族容易,新增产品等级困难。

缓解方案:

  1. 在设计初期充分评估产品等级的稳定性,如果产品等级会频繁变化,不适合用抽象工厂模式
  2. 使用配置化和反射机制,把创建方法做成可配置的,新增产品等级时通过配置而非修改代码
  3. 结合其他模式,如使用原型模式注册产品原型,或使用建造者模式构建复杂产品
  4. 使用默认实现,在抽象工厂中为新增方法提供默认实现(如返回None或抛出不支持异常),减少对已有工厂的影响

但根本上,这个问题是抽象工厂模式的固有特性,无法完全消除,只能缓解。因此选择该模式时要慎重评估产品等级的稳定性。

面试题7:请用Python实现一个抽象工厂模式的例子。

参考答案:

(面试官希望看到完整的代码结构,包括抽象产品、具体产品、抽象工厂、具体工厂、客户端。可以参考本文第七节的跨平台UI实现,或第九节的多云存储实现。)

关键要点:

  • 使用abc.ABC@abstractmethod定义抽象基类
  • 抽象工厂有多个创建方法
  • 具体工厂实现所有创建方法,返回配套的具体产品
  • 客户端只依赖抽象接口

面试题8:抽象工厂模式在实际框架中有哪些应用?

参考答案:

  1. Java Swing :不同平台的UI组件使用了类似抽象工厂的思想,LookAndFeel可以看作抽象工厂
  2. JDBCConnection创建StatementPreparedStatement,同一连接创建的语句对象天然配套,类似于产品族
  3. JDK XML解析DocumentBuilderFactoryTransformerFactory等工厂类,不同解析器提供商对应不同的具体工厂
  4. Spring框架BeanFactory和各种FactoryBean在某些场景下体现了抽象工厂的思想
  5. ORM框架:如MyBatis、Hibernate,不同数据库方言(Dialect)对应不同的产品族,包含分页SQL、关键字映射等配套产品
  6. 日志框架:SLF4J作为抽象层,不同的实现(Logback、Log4j2)对应不同的产品族

十二、总结

12.1 核心思想回顾

抽象工厂模式的本质是:用一个"超级工厂"来管理一组相关产品的创建,确保这些产品被一起使用,并且可以整体切换。

它解决的核心问题是:当一组产品有配套约束、且需要在多组产品之间切换时,如何保证一致性并降低切换成本。

12.2 关键概念速记

  • 产品族:横向,同一品牌/平台下的多种配套产品
  • 产品等级:纵向,同一种产品的不同实现/品牌
  • 抽象工厂:声明多个创建方法,每个对应一个产品等级
  • 具体工厂:实现抽象工厂,生产一个完整产品族
  • 倾斜性开闭原则:新增产品族容易,新增产品等级困难

12.3 使用决策流程

复制代码
需要创建对象吗?
  ├─ 只有一种产品,创建逻辑简单 → 直接new或简单工厂
  ├─ 一种产品,多种实现,需要扩展 → 工厂方法模式
  ├─ 多种产品,需要配套使用,且有多个产品族 → 抽象工厂模式
  └─ 产品等级会频繁变化 → 考虑其他模式(建造者、原型等)

12.4 一句话记住抽象工厂

抽象工厂就是"品牌总代理",你说要哪个品牌,它给你配齐一整套,保证不串味,换品牌也只需要说一声。


本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
吴声子夜歌15 分钟前
Java——开发中通用的方法和准则(一)
java·开发语言
GGG76618 分钟前
Python 运维自动化:psutil+IPy 系统与网络信息采集深度实战
运维·python·云原生·pip
可乐鸡翅yeah_22 分钟前
个人 / 外包流媒体项目交付实战,M3U8 验收与问题闭环完整工作流
运维·网络·python·低代码·django·m3u8·在线播放
常驻客栈33 分钟前
B-深入理解计算机系统第3版之第11章:网络编程
开发语言·笔记·学习笔记·常驻客栈·深入理解计算机系统
旧梦952738 分钟前
Java 单例模式:从基础到实战的完整指南
java·开发语言·单例模式
小小晓.1 小时前
C++ 值类别与完美转发深度剖析:从左值右值到 std::forward 源码解密
开发语言·c++·算法
人机(未转人工)1 小时前
python 批量测试ip是否能ping( 多线程 )
python
cndes2 小时前
安装Miniconda+Jupyter notebook
ide·python·jupyter
Patrick在香港2 小时前
Python 拉取城巴开放数据:406 条路线,香港岛巴士网络的数字画像
网络·python·数据分析·php·数据可视化·香港·开放数据