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时,你发现整个项目里有上百处这样的代码,每一处都要改。更可怕的是,有人可能混用了MySQLConnection和PostgreSQLCommand,运行时直接崩溃。

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) 实现抽象工厂接口,生成一个产品族的所有具体产品 WindowsFactory、MacFactory
抽象产品(AbstractProduct) 为每种产品声明接口 Button、TextField
具体产品(ConcreteProduct) 定义具体工厂生产的具体产品对象,实现抽象产品接口 WindowsButton、MacButton

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. 抽象产品层 :Button、TextField、Checkbox 三个抽象基类,定义了每种组件的接口
  2. 具体产品层:每个抽象产品有Windows和Mac两个具体实现
  3. 抽象工厂层 :GUIFactory 声明了三个创建方法,分别对应三种产品
  4. 具体工厂层 :WindowsFactory 和 MacFactory 分别生产各自风格的一整套组件
  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. JDBC :Connection创建Statement、PreparedStatement,同一连接创建的语句对象天然配套,类似于产品族
  3. JDK XML解析 :DocumentBuilderFactory、TransformerFactory等工厂类,不同解析器提供商对应不同的具体工厂
  4. Spring框架 :BeanFactory和各种FactoryBean在某些场景下体现了抽象工厂的思想
  5. ORM框架:如MyBatis、Hibernate,不同数据库方言(Dialect)对应不同的产品族,包含分页SQL、关键字映射等配套产品
  6. 日志框架:SLF4J作为抽象层,不同的实现(Logback、Log4j2)对应不同的产品族

十二、总结

12.1 核心思想回顾

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

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

12.2 关键概念速记

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

12.3 使用决策流程

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

12.4 一句话记住抽象工厂

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


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

相关推荐
东莞市云毅网络有限公司3 小时前
AI 引用句逐条回指原文:让回答可回溯的校验实现
python·数据清洗·rag·企业知识库·文档解析
Sarvartha4 小时前
final 关键字
java·开发语言
数字融合4 小时前
透明化视频三维矿山井下照明重建技术
人工智能·python·数码相机
yi0115 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
程序员Sunday5 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
zhangzeyuaaa6 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
Marst Code6 小时前
上位机开发日记 · 第 2 篇 · 架构先行:六层分层与边界
python
李日华大战鸡红7 小时前
FOC状态空间方程模型推导(学习记录)
python·学习·线性代数
谢亮_vipxieliang7 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
思无邪667 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能