设计模式实战(三):工厂模式——创建对象的烦恼与 DI 容器

写在前面

上一篇策略模式解决了"选算法"。这篇讲创建型模式的代表——工厂模式,解决"造对象"。

很多新手觉得工厂模式绕:简单工厂、工厂方法、抽象工厂,三个名字像、又不一样。这篇把它们一次讲清,并落到 .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 容器是终极工厂:日常开发基本不手写工厂,改注册即可
  • 手写工厂仍有场景:运行时决定、动态解析、运行时参数

下一篇讲建造者模式——当构造对象本身很复杂(一堆可选参数)时,怎么造得优雅。