第三章:创建型模式-工厂模式:简单工厂、工厂方法、抽象工厂在Rust中的实践

工厂模式,说白了就是帮你「生产对象」的套路。在Rust里,由于所有权和生命周期的存在,对象创建这件事比Java要复杂不少。我早期用Rust写业务时,经常在new函数里纠结——到底该返回Box还是Rc?后来发现,工厂模式正好能帮我们理清这些关系。

3.1 简单工厂:最朴素的封装

简单工厂其实就是一个函数,根据参数返回不同类型的实例。在Rust里,我们通常用枚举+特征对象来实现。

核心思想:用一个统一的创建入口,隐藏具体类型的构造细节。

先看一个例子。假设我们要创建不同类型的日志记录器:

trait Logger {
    fn log(&self, msg: &str);
}

struct ConsoleLogger;
impl Logger for ConsoleLogger {
    fn log(&self, msg: &str) {
        println!("[Console] {}", msg);
    }
}

struct FileLogger;
impl Logger for FileLogger {
    fn log(&self, msg: &str) {
        // 实际写入文件
        println!("[File] {}", msg);
    }
}

enum LoggerType {
    Console,
    File,
}

fn create_logger(t: LoggerType) -> Box<dyn Logger> {
    match t {
        LoggerType::Console => Box::new(ConsoleLogger),
        LoggerType::File => Box::new(FileLogger),
    }
}

嗯,这里要注意:Box<dyn Logger> 是Rust里实现简单工厂的标配。我在项目中遇到过有人试图用泛型来做,结果调用方每次都要指定类型,反而失去了工厂的意义。

我的习惯:如果工厂创建的对象不超过5种,简单工厂完全够用。别一上来就上抽象工厂,过度设计在Rust里尤其痛苦。

3.2 工厂方法:把选择权交给子类

简单工厂有个问题——每次新增类型都要改工厂函数,违反了开闭原则。工厂方法模式把创建逻辑延迟到子类中。

在Rust里,我们通常用trait来定义工厂接口:

trait LoggerFactory {
    fn create_logger(&self) -> Box<dyn Logger>;
}

struct ConsoleLoggerFactory;
impl LoggerFactory for ConsoleLoggerFactory {
    fn create_logger(&self) -> Box<dyn Logger> {
        Box::new(ConsoleLogger)
    }
}

struct FileLoggerFactory;
impl LoggerFactory for FileLoggerFactory {
    fn create_logger(&self) -> Box<dyn Logger> {
        Box::new(FileLogger)
    }
}

// 使用
fn init_app(factory: &dyn LoggerFactory) {
    let logger = factory.create_logger();
    logger.log("应用启动");
}

你想想看,这样做的好处是什么?调用方init_app完全不需要知道具体是哪种Logger,它只依赖LoggerFactory这个抽象。我曾经在一个配置解析项目里用这个模式,后来要支持JSON和YAML两种格式,只需要新增两个工厂实现,原有代码一行没改。

避坑指南:我曾经在工厂方法里返回了栈上对象的引用,结果编译报错。记住,Rust里工厂方法必须返回拥有所有权的类型——Box、Rc、或者Arc。别想着返回&dyn Trait,生命周期会让你崩溃。

3.3 抽象工厂:创建产品族

当你要创建一组相关对象时,抽象工厂就派上用场了。比如UI组件库——按钮、输入框、对话框要风格统一。

看个例子:

// 抽象产品
trait Button {
    fn render(&self);
}

trait Checkbox {
    fn render(&self);
}

// 抽象工厂
trait UIFactory {
    fn create_button(&self) -> Box<dyn Button>;
    fn create_checkbox(&self) -> Box<dyn Checkbox>;
}

// 具体产品族:Windows风格
struct WinButton;
impl Button for WinButton {
    fn render(&self) { println!("Windows按钮"); }
}

struct WinCheckbox;
impl Checkbox for WinCheckbox {
    fn render(&self) { println!("Windows复选框"); }
}

struct WinFactory;
impl UIFactory for WinFactory {
    fn create_button(&self) -> Box<dyn Button> { Box::new(WinButton) }
    fn create_checkbox(&self) -> Box<dyn Checkbox> { Box::new(WinCheckbox) }
}

// 具体产品族:Mac风格
struct MacButton;
impl Button for MacButton {
    fn render(&self) { println!("Mac按钮"); }
}

struct MacCheckbox;
impl Checkbox for MacCheckbox {
    fn render(&self) { println!("Mac复选框"); }
}

struct MacFactory;
impl UIFactory for MacFactory {
    fn create_button(&self) -> Box<dyn Button> { Box::new(MacButton) }
    fn create_checkbox(&self) -> Box<dyn Checkbox> { Box::new(MacCheckbox) }
}

使用的时候,只需要切换工厂实例:

fn render_ui(factory: &dyn UIFactory) {
    let btn = factory.create_button();
    let cb = factory.create_checkbox();
    btn.render();
    cb.render();
}

fn main() {
    let factory = Box::new(WinFactory);
    render_ui(&*factory);
}

关键点:抽象工厂保证了一个工厂生产的所有产品都是「配套」的。你不会在Windows风格里突然冒出一个Mac按钮。我在做跨平台GUI库时,这个特性帮了大忙。

3.4 三种工厂模式的对比

模式 适用场景 Rust实现要点 我踩过的坑
简单工厂 产品种类少,且不常变化 枚举+Box<dyn Trait> 忘记处理枚举的默认分支
工厂方法 产品种类多,需要扩展性 Trait定义工厂接口 返回引用导致生命周期问题
抽象工厂 需要创建产品族 多个创建方法+产品族一致性 产品族扩展时接口变更

3.5 知识体系结构图

下面这张图帮你理清三种工厂模式的关系:

Rust工厂模式知识体系 简单工厂 一个函数创建多种对象 工厂方法 Trait定义创建接口 抽象工厂 创建产品族 扩展 组合 Rust实现核心:Box<dyn Trait> + 枚举 + 特征对象 简单工厂细节 match枚举分支 返回Box<dyn Trait> 工厂方法细节 Trait定义工厂 每个类型一个工厂 抽象工厂细节 多个创建方法 产品族一致性 ⚠️ 避坑指南 不要返回引用!不要忘记生命周期!不要过度设计! 总结:简单工厂入门,工厂方法扩展,抽象工厂应对复杂产品族

3.6 实战建议

我个人在实际项目中的选择标准是这样的:

  • 产品少于5种,且不会频繁新增 → 简单工厂。代码量最少,够用就好。
  • 产品种类多,但彼此独立 → 工厂方法。每个产品有自己的工厂,互不干扰。
  • 产品之间有依赖关系,需要成套使用 → 抽象工厂。保证产品族的一致性。

一个小技巧:在Rust里,如果工厂创建的对象不需要动态分发,可以尝试用泛型+关联类型来替代Box<dyn Trait>。这样能避免虚函数调用的开销。不过说实话,大多数场景下Box的开销可以忽略不计。

工厂模式在Rust里其实比在面向对象语言中更自然——因为Rust的trait系统天生就支持抽象和组合。你想想看,Java里工厂模式要写一堆类,Rust里一个trait加几个impl就搞定了。这也是我喜欢用Rust的原因之一。

好了,工厂模式就聊到这里。记住一点:模式是工具,不是教条。别为了用模式而用模式,合适才是最重要的。


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