写在前面
上一篇策略模式解决了"选算法"。这篇讲创建型模式的代表——工厂模式,解决"造对象"。
很多新手觉得工厂模式绕:简单工厂、工厂方法、抽象工厂,三个名字像、又不一样。这篇把它们一次讲清,并落到 .NET 上——你会发现 DI 容器本身就是个超级工厂,现代 .NET 开发里,大部分时候你不用手写工厂。
一、问题:到处 new
1
2
3
4
5
6
7
8
9
10
11
| // 反例:直接 new,和具体类型死死绑死
public class OrderService
{
public void Place(Order order)
{
var repo = new SqlOrderRepository(); // 写死具体实现
var notifier = new EmailNotifier(); // 写死
repo.Save(order);
notifier.Send(order);
}
}
|
1
2
3
4
5
6
7
8
9
| 这坨代码的病:
✗ 依赖具体类(SqlOrderRepository),不是抽象
→ 违反依赖倒置(DIP)
✗ 换实现(改 Mongo、换短信通知)要改这里
✗ 没法注入假实现做单元测试
✗ 如果构造对象本身复杂(要查配置、要条件判断),new 就更难看
病根:对象"怎么造"和对象"怎么用"搅在一起了。
解法:把"造"的责任抽出去——这就是工厂。
|
二、简单工厂(静态工厂)
最朴素:一个方法,按参数决定造哪个。
1
2
3
4
5
6
7
8
9
10
11
| public static class DiscountFactory
{
public static IDiscountStrategy Create(string type) => type switch
{
"Normal" => new NormalDiscount(),
"Vip" => new VipDiscount(),
"Employee" => new EmployeeDiscount(),
_ => throw new ArgumentException($"未知折扣类型: {type}")
};
}
// 用:var d = DiscountFactory.Create("Vip");
|
1
2
3
4
5
6
7
| 特点:
✓ 简单,集中了创建逻辑,调用方不关心具体类
✗ 严格说它不是"GoF 模式",只是个习惯用法
✗ 违反开闭:加新类型要改这个 switch
适合:创建逻辑简单、类型不多、不频繁变化。
(上一篇策略模式里的"字典派发",其实就是简单工厂的现代版。)
|
三、工厂方法(Factory Method)
把"创建"推迟到子类:父类定义流程,子类决定造哪个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 父类:定义"创建 + 使用"的流程,但"造哪个"交给子类
public abstract class NotificationSender
{
protected abstract INotifier CreateNotifier(); // 工厂方法(抽象)
public void Send(string msg)
{
var notifier = CreateNotifier(); // 用子类造的对象
notifier.Send(msg);
}
}
// 子类 A:决定造 EmailNotifier
public class EmailSender : NotificationSender
{
protected override INotifier CreateNotifier() => new EmailNotifier();
}
// 子类 B:决定造 SmsNotifier
public class SmsSender : NotificationSender
{
protected override INotifier CreateNotifier() => new SmsNotifier();
}
|
1
2
3
4
5
6
| vs 简单工厂:
简单工厂:一个方法里 switch(加类型改 switch)
工厂方法:每种类型一个子类(加类型加子类,老代码不动 → 开闭)
代价:类变多。
适合:创建逻辑需要配合一套"使用流程",且类型会扩展。
|
四、抽象工厂(Abstract Factory)
造一"族"相关对象,保证它们配套。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 工厂接口:造一族 UI 控件
public interface IUiFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
}
// 一族:Windows 风格
public class WindowsUiFactory : IUiFactory
{
public IButton CreateButton() => new WindowsButton();
public ITextBox CreateTextBox() => new WindowsTextBox();
}
// 另一族:Mac 风格
public class MacUiFactory : IUiFactory
{
public IButton CreateButton() => new MacButton();
public ITextBox CreateTextBox() => new MacTextBox();
}
|
1
2
3
4
5
6
7
8
9
| vs 工厂方法:
工厂方法:造"一个"产品(一个方法)
抽象工厂:造"一族"配套产品(多个方法)
价值:保证造出来的一组对象是配套的(不会 Windows 按钮配 Mac 文本框)。
典型场景:跨平台 UI、多数据库方言、多主题样式。
注意:抽象工厂常被滥用。如果只有一种产品,用工厂方法/简单工厂就够,
别为了"显得有体系"上抽象工厂。
|
五、三者对比
1
2
3
4
5
6
7
8
9
10
| 造什么 加新产品 复杂度 适用
─────────────────────────────────────────────────────────────
简单工厂 单个产品 改 switch 低 类型少、简单
工厂方法 单个产品 加子类 中 类型会扩展
抽象工厂 一族产品 加子类+改接口 高 多族配套、跨平台
记忆窍门:
简单工厂 = 一个函数 switch
工厂方法 = 把函数变成虚方法,让子类决定
抽象工厂 = 工厂方法升级版,一个工厂造多个配套产品
|
六、.NET DI 容器 = 超级工厂
现代 .NET 里,你几乎不用手写工厂——DI 容器(IServiceCollection / IServiceProvider)就是工厂的终极形态。
1
2
3
4
5
6
7
8
9
10
| // 注册:告诉"工厂"接口对应什么实现
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddSingleton<INotifier, EmailNotifier>();
services.AddTransient<IDiscountStrategy, VipDiscount>();
// 用:从"工厂"要对象,不关心怎么造
public class OrderService(IOrderRepository _repo, INotifier _notifier)
{
public void Place(Order o) { _repo.Save(o); _notifier.Send(o); }
}
|
1
2
3
4
5
6
7
8
| DI 容器相比手写工厂:
✓ 自动接管创建,包括依赖链(A 依赖 B,B 依赖 C,容器递归造)
✓ 生命周期(Singleton/Scoped/Transient)集中管理
✓ 切换实现 = 改注册一行,不改业务代码
✓ 测试时换假实现 = 改注册,详见《.NET 单元测试(四):依赖注入与可测试性》
→ 这就是为什么 ASP.NET Core 里很少看到手写 XxxFactory:
DI 把"工厂模式"这件事彻底标准化了。
|
需要手写工厂的场景
1
2
3
4
5
6
7
8
9
10
11
12
| // 场景:运行时才能决定造哪个(配置/类型/参数)
services.AddScoped<IOrderRepository>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return config["Db"] == "Mongo"
? new MongoOrderRepository(config["Conn"])
: new SqlOrderRepository(config["Conn"]);
}); // 工厂 lambda:容器调用时才执行
// 场景:解析未知类型(插件/动态加载)
var handler = ActivatorUtilities.CreateInstance(sp, typeFromConfig);
// 或按 key 取一批已注册的服务(variance)
|
1
2
3
4
5
6
| 什么时候仍要手写工厂:
- 运行时根据配置/类型决定实现(DI 注册时还不确定)
- 动态解析、插件化、按 key 派发(容器不直接支持"按字符串取")
- 构造对象需要运行时参数(不能预先注册)
其余情况,让 DI 容器当你的工厂。
|
七、小结
- 问题:到处
new 把"造"和"用"绑死,违反依赖倒置 - 简单工厂:一个方法 switch 决定造哪个(类型少时够用)
- 工厂方法:抽象方法交给子类决定(加类型加子类,开闭)
- 抽象工厂:造一族配套产品(跨平台/多主题)
- 选型:简单→简单工厂;扩展→工厂方法;多族配套→抽象工厂
- .NET DI 容器是终极工厂:日常开发基本不手写工厂,改注册即可
- 手写工厂仍有场景:运行时决定、动态解析、运行时参数
下一篇讲建造者模式——当构造对象本身很复杂(一堆可选参数)时,怎么造得优雅。