第三章:创建型模式-工厂模式:简单工厂、工厂方法、抽象工厂在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 知识体系结构图
下面这张图帮你理清三种工厂模式的关系:
3.6 实战建议
我个人在实际项目中的选择标准是这样的:
- 产品少于5种,且不会频繁新增 → 简单工厂。代码量最少,够用就好。
- 产品种类多,但彼此独立 → 工厂方法。每个产品有自己的工厂,互不干扰。
- 产品之间有依赖关系,需要成套使用 → 抽象工厂。保证产品族的一致性。
一个小技巧:在Rust里,如果工厂创建的对象不需要动态分发,可以尝试用泛型+关联类型来替代Box<dyn Trait>。这样能避免虚函数调用的开销。不过说实话,大多数场景下Box的开销可以忽略不计。
工厂模式在Rust里其实比在面向对象语言中更自然——因为Rust的trait系统天生就支持抽象和组合。你想想看,Java里工厂模式要写一堆类,Rust里一个trait加几个impl就搞定了。这也是我喜欢用Rust的原因之一。
好了,工厂模式就聊到这里。记住一点:模式是工具,不是教条。别为了用模式而用模式,合适才是最重要的。
公众号:蓝海资料掘金营,微信deep3321