设计模式:工厂模式——解耦对象创建过程

各位好,今天我们来聊聊工厂模式。说实话,工厂模式是设计模式里最实用的一类,也是我早期写代码时经常用错的一类。你想想看,我们写面向对象程序,天天都在 new 对象。new 本身没什么问题,但当你把 new 散落在代码的各个角落,一旦需求变化,改起来就非常痛苦。工厂模式说白了,就是把“创建对象”这件事单独拎出来,交给一个专门的“工厂”去管。

我个人习惯把工厂模式分成三种:简单工厂、工厂方法、抽象工厂。它们解决的问题层层递进,咱们一个一个来看。

1. 简单工厂:最直接的解耦

简单工厂其实不算严格意义上的设计模式,更像是一种编程习惯。但它非常实用。我记得刚入行时,项目里有个日志模块,需要根据配置创建不同的日志记录器(文件日志、数据库日志、控制台日志)。一开始代码里全是 if-else 判断,后来我把它抽成了一个 LoggerFactory。

来看个例子:

class LoggerFactory:
    @staticmethod
    def create_logger(logger_type):
        if logger_type == "file":
            return FileLogger()
        elif logger_type == "db":
            return DatabaseLogger()
        elif logger_type == "console":
            return ConsoleLogger()
        else:
            raise ValueError("Unknown logger type")

这样客户端代码只需要调用 LoggerFactory.create_logger("file"),根本不用关心 FileLogger 是怎么构造的。嗯,这里要注意:简单工厂虽然好用,但每次新增一种日志类型,你都得改工厂类的 if-else。这违反了开闭原则。不过对于小型项目,我觉得完全够用。

核心思想: 用一个工厂类封装所有对象的创建逻辑,客户端只传参数,不直接 new。

2. 工厂方法:把创建延迟到子类

简单工厂的痛点在于“修改”。我曾经在一个支付系统中遇到过类似问题:一开始只有微信支付和支付宝支付,简单工厂写得很爽。后来加了银联支付、PayPal……每次都要改工厂类,而且测试回归范围越来越大。这时候工厂方法模式就派上用场了。

工厂方法模式把工厂本身也抽象化了。每个具体产品对应一个具体工厂。客户端不再直接调用一个静态方法,而是先选择一个工厂实例,再由这个工厂去创建产品。

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message):
        pass

class FileLogger(Logger):
    def log(self, message):
        print(f"File: {message}")

class ConsoleLogger(Logger):
    def log(self, message):
        print(f"Console: {message}")

class LoggerFactory(ABC):
    @abstractmethod
    def create_logger(self) -> Logger:
        pass

class FileLoggerFactory(LoggerFactory):
    def create_logger(self) -> Logger:
        return FileLogger()

class ConsoleLoggerFactory(LoggerFactory):
    def create_logger(self) -> Logger:
        return ConsoleLogger()

# 客户端使用
factory = FileLoggerFactory()
logger = factory.create_logger()
logger.log("Hello")

你看,现在新增一种日志类型,只需要新增一个产品类和一个对应的工厂类,完全不用改已有代码。这就是开闭原则的体现。不过代价是类的数量翻倍了。我个人建议:如果产品类型稳定且不超过5种,简单工厂更省事;如果经常扩展,工厂方法更合适。

避坑指南: 我曾经在工厂方法里塞了太多业务逻辑,导致工厂类变得臃肿。记住,工厂只负责创建对象,不要在里面做数据校验、缓存等事情。

3. 抽象工厂:创建一组相关的对象

抽象工厂是工厂方法的升级版。它解决的是“产品族”的问题。举个例子:假设你的应用需要支持不同的 UI 主题(Windows 风格、Mac 风格)。每个主题下都有一组控件:按钮、文本框、复选框。你不能让 Windows 风格的按钮和 Mac 风格的文本框混在一起用,对吧?

抽象工厂定义了一个接口,该接口负责创建一组相关或相互依赖的对象,而不需要指定它们的具体类。

from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def paint(self):
        pass

class TextBox(ABC):
    @abstractmethod
    def show(self):
        pass

class WinButton(Button):
    def paint(self):
        print("Windows 风格按钮")

class MacButton(Button):
    def paint(self):
        print("Mac 风格按钮")

class WinTextBox(TextBox):
    def show(self):
        print("Windows 风格文本框")

class MacTextBox(TextBox):
    def show(self):
        print("Mac 风格文本框")

class UIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass
    @abstractmethod
    def create_textbox(self) -> TextBox:
        pass

class WinFactory(UIFactory):
    def create_button(self) -> Button:
        return WinButton()
    def create_textbox(self) -> TextBox:
        return WinTextBox()

class MacFactory(UIFactory):
    def create_button(self) -> Button:
        return MacButton()
    def create_textbox(self) -> TextBox:
        return MacTextBox()

# 客户端
factory = WinFactory()
btn = factory.create_button()
tb = factory.create_textbox()
btn.paint()
tb.show()

抽象工厂的关键在于:你只需要选择一次工厂(比如 WinFactory),之后创建的所有控件都是配套的。不会出现混搭。我在做跨平台桌面应用时,这个模式帮了大忙。不过要注意,抽象工厂很难扩展新的产品类型。比如你想增加一个“标签”控件,那么所有工厂接口都得改。这是它的固有缺陷。

注意: 抽象工厂适用于产品族稳定、产品等级结构不常变的场景。如果你的产品族经常变化,建议考虑使用工厂方法 + 依赖注入的组合。

4. 三种工厂模式对比

模式 核心特点 适用场景 缺点
简单工厂 一个工厂类,根据参数创建不同产品 产品种类少,且不常变化 违反开闭原则,扩展需修改工厂类
工厂方法 每个产品对应一个工厂子类 产品种类多,且经常扩展 类数量增多,系统复杂度上升
抽象工厂 创建一组相关或相互依赖的产品族 需要保证产品族内对象配套使用 扩展新产品族困难,接口改动影响大

5. 知识体系结构图

下面我用一张 SVG 图来梳理这三种工厂模式的关系和核心逻辑。你可以把它当作一个快速参考。

工厂模式知识体系 简单工厂 一个工厂类 根据参数创建产品 违反开闭原则 工厂方法 每个产品对应一个工厂 创建延迟到子类 类数量增多 抽象工厂 创建产品族 保证配套使用 扩展产品族困难 解耦程度逐渐增强,复杂度也逐渐上升 核心逻辑:将对象的创建与使用分离 客户端不直接 new 对象,而是通过工厂获取 工厂负责封装创建细节,客户端只依赖抽象接口 当需求变化时,只需修改工厂,不影响客户端代码

6. 实际应用中的选择建议

说了这么多,到底该怎么选?我个人的经验是:

  • 如果你只是临时封装一下创建逻辑,而且产品类型不超过3个,简单工厂就够了。别过度设计。
  • 如果你的产品类型经常增加,比如插件系统、驱动加载,工厂方法更合适。我曾经在做一个报表引擎时,用了工厂方法,后来新增了十几种报表类型,代码几乎没怎么改。
  • 如果你需要保证一组对象必须配套使用,比如 UI 主题、数据库连接(不同数据库有不同的连接、命令、事务对象),抽象工厂是标准答案。
一个小技巧: 在 Python 中,工厂方法不一定非要定义抽象类。你可以用函数 + 字典映射来实现,更轻量。比如:factory_map = {"file": FileLogger, "db": DatabaseLogger},然后直接调用 factory_map[type]()。但要注意,这种方式不适合复杂的产品族场景。

好了,关于工厂模式就聊到这里。三种模式各有优劣,没有银弹。关键是根据你的业务场景,选择最合适的解耦方式。记住,设计模式是为了解决问题,不是为了炫技。下次你写代码时,如果发现 new 对象的地方太多,不妨想想:是不是该请个“工厂”来帮忙了?

公众号:蓝海资料掘金营,微信deep3321