设计模式:工厂模式——解耦对象创建过程
各位好,今天我们来聊聊工厂模式。说实话,工厂模式是设计模式里最实用的一类,也是我早期写代码时经常用错的一类。你想想看,我们写面向对象程序,天天都在 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。这违反了开闭原则。不过对于小型项目,我觉得完全够用。
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 图来梳理这三种工厂模式的关系和核心逻辑。你可以把它当作一个快速参考。
6. 实际应用中的选择建议
说了这么多,到底该怎么选?我个人的经验是:
- 如果你只是临时封装一下创建逻辑,而且产品类型不超过3个,简单工厂就够了。别过度设计。
- 如果你的产品类型经常增加,比如插件系统、驱动加载,工厂方法更合适。我曾经在做一个报表引擎时,用了工厂方法,后来新增了十几种报表类型,代码几乎没怎么改。
- 如果你需要保证一组对象必须配套使用,比如 UI 主题、数据库连接(不同数据库有不同的连接、命令、事务对象),抽象工厂是标准答案。
factory_map = {"file": FileLogger, "db": DatabaseLogger},然后直接调用 factory_map[type]()。但要注意,这种方式不适合复杂的产品族场景。
好了,关于工厂模式就聊到这里。三种模式各有优劣,没有银弹。关键是根据你的业务场景,选择最合适的解耦方式。记住,设计模式是为了解决问题,不是为了炫技。下次你写代码时,如果发现 new 对象的地方太多,不妨想想:是不是该请个“工厂”来帮忙了?