AIGC标识 深入浅出 WPF/C# 依赖注入中的委托工厂模式(Delegate Factory)【AI生成】

 

在现代 WPF MVVM 架构中,随着页面导航、向导流程、复杂弹窗和动态业务模块的增加,ViewModel 的创建方式逐渐成为一个值得认真考虑的设计问题。

假设一个导入窗口包含多个功能模块:文件上传、格式解析、数据预览和导入结果展示。父级 ImportViewModel 负责协调整个导入流程,而 ParserViewModel 负责具体的解析逻辑。当用户更换文件、重新解析或者重新打开窗口时,系统可能需要创建一个全新的 ParserViewModel,同时保留其他模块已有的状态。

如果所有子 ViewModel 都通过构造函数直接注入,代码虽然简单,却可能使对象的创建时机、状态归属和生命周期管理变得不够清晰。

委托工厂(Delegate Factory)正是解决这类问题的一种常用设计方案。本文以 C# 的 Func<T> 和微软的 Microsoft.Extensions.DependencyInjection 为例,说明其工作原理、适用场景和需要注意的边界。

一、传统构造函数注入的局限

先看一个常见的设计。

public class ImportViewModel
{
    private readonly UploadViewModel _uploadViewModel;
    private readonly ParserViewModel _parserViewModel;

    public ImportViewModel(
        UploadViewModel uploadViewModel,
        ParserViewModel parserViewModel)
    {
        _uploadViewModel = uploadViewModel;
        _parserViewModel = parserViewModel;
    }
}

 

这里使用构造函数注入,将 UploadViewModel 和 ParserViewModel 作为 ImportViewModel 的依赖项。

这种写法本身没有问题。对于那些需要在父级 ViewModel 的整个工作期间持续存在、并且应当共享状态的子对象,直接注入通常是最简单、最清晰的方案。

问题出现在另一类场景中:父级 ViewModel 需要的不是一个固定的子对象,而是在某个业务动作发生时,按需创建一个新的子对象。

例如:

  • 用户选择另一个文件,需要重新创建解析器。

  • 用户重新开始导入,需要创建一组全新的流程对象。

  • 用户连续打开多个弹窗,每个弹窗都需要独立的编辑状态。

  • 用户创建多个相互独立的页面实例,每个实例都需要自己的查询条件和操作记录。

如果直接注入 ParserViewModel,父级得到的是一个已经创建好的实例,而不是创建实例的能力。

当业务要求创建第二个、第三个解析器时,父级就必须自行处理对象替换、状态重置或者其他创建逻辑。随着业务复杂度提高,创建职责可能逐渐混入 ViewModel 的业务代码。

因此,真正需要解决的问题不是构造函数注入本身,而是:

当依赖对象需要动态创建时,如何将对象创建能力纳入依赖注入架构,同时避免让业务层直接操作 DI 容器?

二、委托工厂:从注入对象到注入创建能力

1. 什么是委托工厂?

委托工厂是一种利用委托封装对象创建操作的设计方式。

在 C# 中,Func<T> 表示一个无参数、返回类型为 T 的函数:

Func<ParserViewModel> parserFactory;

它可以理解为一个返回 ParserViewModel 的可调用对象。

与直接注入实例相比,两者的区别如下:

// 直接注入:获得一个对象
ParserViewModel parserViewModel;

// 委托工厂:获得一个创建对象的入口
Func<ParserViewModel> parserViewModelFactory;

前者提供的是对象,后者提供的是获取对象的能力。

调用工厂时:

ParserViewModel parserViewModel =
    parserViewModelFactory();

才会执行委托内部定义的逻辑。

这里有一个重要前提:Func<T> 本身并不保证每次调用都会产生新实例。它究竟返回新对象还是已有对象,取决于委托的具体实现。

只有当工厂内部每次都执行一次新的对象解析,并且目标服务配置为AddTransient瞬态生命周期时,才能实现通常所说的“每次调用创建新实例”。

2. 在 ViewModel 中使用委托工厂

我们可以将 ImportViewModel 改写为以下形式:

using System;

public class ImportViewModel
{
    private readonly UploadViewModel _uploadViewModel;

    private readonly Func<ParserViewModel>
        _parserViewModelFactory;

    public ImportViewModel(
        UploadViewModel uploadViewModel,
        Func<ParserViewModel> parserViewModelFactory)
    {
        _uploadViewModel = uploadViewModel
            ?? throw new ArgumentNullException(
                nameof(uploadViewModel));

        _parserViewModelFactory = parserViewModelFactory
            ?? throw new ArgumentNullException(
                nameof(parserViewModelFactory));
    }

    private void OpenParserPage()
    {
        ParserViewModel parserViewModel =
            _parserViewModelFactory();

        // 将新创建的解析器交给相应的页面宿主。
        // 后续由宿主负责使用、替换和释放相关资源。
    }
}

 

现在,ImportViewModel 不再要求 DI 容器提前提供一个固定的 ParserViewModel 实例,而是接收一个创建委托。

当业务需要新的解析器时,调用 _parserViewModelFactory() 即可。

这种设计保留了构造函数注入的优点,同时将依赖对象的创建时机交给业务流程控制。

需要强调的是,示例中的 OpenParserPage() 只是说明创建方式。实际项目还需要将新实例交给页面宿主,并明确旧实例的处理策略。

三、如何在微软 DI 容器中注册委托工厂?

使用 Microsoft.Extensions.DependencyInjection 时,有一个容易忽略的细节:微软默认 DI 容器不会自动为任意服务生成 Func<T> 工厂。

因此,不能只注册 ParserViewModel,还需要显式注册对应的工厂委托。

using Microsoft.Extensions.DependencyInjection;

services.AddTransient<ParserViewModel>();

services.AddTransient<Func<ParserViewModel>>(
    provider =>
        () => provider.GetRequiredService<ParserViewModel>());

 

第一条注册:

services.AddTransient<ParserViewModel>();

表示每次通过容器解析 ParserViewModel 时,容器都会创建一个新的实例,并解析其构造函数所需的依赖项。

第二条注册:

services.AddTransient<Func<ParserViewModel>>(
    provider =>
        () => provider.GetRequiredService<ParserViewModel>());

 

则告诉容器如何构造一个 Func<ParserViewModel>。

这里需要区分两个时间点。

第一,DI 容器解析 Func<ParserViewModel> 时,会创建一个委托。此时,委托内部的表达式尚未执行。

第二,业务代码调用该委托时:

var parser1 = parserFactory();
var parser2 = parserFactory();

委托才会分别调用 GetRequiredService<ParserViewModel>()。

由于 ParserViewModel 注册为 Transient,且每次调用都会重新向容器解析,因此在正常情况下:

ReferenceEquals(parser1, parser2); // false

两个对象具有独立的实例状态。

如果 ParserViewModel 内部也注入了其他服务,那么这些服务仍然按照各自的 DI 注册规则进行解析。例如,瞬态解析器可以依赖一个单例日志服务,但不能因此假设它的所有依赖也都是全新的。

四、委托工厂为什么比直接注入 IServiceProvider 更合适?

在动态创建对象时,另一种常见写法是直接注入 IServiceProvider。

public class ImportViewModel
{
    private readonly IServiceProvider _serviceProvider;

    public ImportViewModel(
        IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    private void OpenParserPage()
    {
        var parserViewModel =
            _serviceProvider
                .GetRequiredService<ParserViewModel>();
    }
}

 

这种方式确实能够创建对象,但它也引入了一个架构问题。

ImportViewModel 原本只需要创建 ParserViewModel,现在却获得了访问整个服务容器的能力。它可以继续解析其他服务,甚至逐渐承担起本应属于组合根的依赖组装职责。

这种将服务解析逻辑放入业务对象的方式,通常被称为服务定位器模式(Service Locator),也是依赖注入设计中需要谨慎使用的做法。

两种方式的区别在于:

比较维度注入 IServiceProvider注入 Func<T>
依赖表达 依赖整个服务容器 明确依赖某种对象的创建能力
可见性 具体依赖可能隐藏在方法内部 工厂类型直接出现在构造函数中
创建范围 理论上可以解析多个已注册服务 只能通过该委托获取指定类型
单元测试 通常需要模拟容器解析行为 可以直接传入创建委托
适用场景 组合根、基础设施或确有需要的动态解析场景 业务层需要按需创建特定类型的对象

需要注意,委托工厂并不会让业务层与 DI 容器完全没有间接关系。工厂本身通常仍然由容器构建,只是它将依赖边界收窄到了一个明确的创建操作。

这种设计的价值在于:业务对象只依赖自己真正需要的能力,而不必获得整个容器的访问权。

五、委托工厂与 Transient:创建和生命周期是两个问题

在讨论委托工厂时,最容易产生的误解是将 Func<T>、Transient 和对象释放视为同一件事。

实际上,它们分别解决不同的问题。

1. Func<T> 解决创建时机

Func<T> 让业务代码决定什么时候执行对象创建或获取逻辑。

var parser = parserFactory();

在调用之前,工厂内部的创建逻辑不会因为委托本身被注入而自动执行。

2. Transient 解决容器解析时的实例策略

对于瞬态注册:

services.AddTransient<ParserViewModel>();

每次通过容器解析 ParserViewModel 时,都会创建新实例。

但这并不意味着对象会在方法结束时自动销毁,也不意味着其引用会自动消失。

如果父级 ViewModel 将创建出来的实例保存在字段中,那么只要父级仍然引用它,该对象就可能继续存活。

3. 对象释放需要单独设计

假设 ParserViewModel 实现了 IDisposable:

public class ParserViewModel : IDisposable
{
    public void Dispose()
    {
        // 释放该对象自行拥有的资源。
    }
}

 

当页面关闭时,不能仅仅因为它注册为 Transient,就认为容器会立即调用 Dispose()。

微软默认 DI 容器会跟踪由容器创建的可释放服务,并通常在相应容器或作用域释放时统一释放这些服务,而不是在瞬态对象不再被页面使用时立即释放。

这意味着,如果一个长期存活的根容器反复创建大量可释放的瞬态对象,这些对象可能一直被容器跟踪到根容器释放时。

对于 WPF 桌面程序,这可能造成资源长期占用。

因此,使用委托工厂时,应当同时考虑:

  • 谁持有新创建的 ViewModel。

  • 页面关闭后,谁清除相关引用。

  • 事件订阅是否需要取消。

  • 定时器、文件句柄等资源是否需要释放。

  • 对象是否属于某个明确的 DI 作用域。

  • 是否需要由窗口或页面宿主统一管理对象生命周期。

如果解析器只负责普通的数据转换,并不拥有需要释放的资源,那么可能根本不需要实现 IDisposable。不要为了追求形式上的完整而增加不必要的生命周期复杂度。

六、结合 WPF MVVM:什么时候应该使用委托工厂?

委托工厂不是用来替代所有构造函数注入的。它更适合那些创建时机由业务操作决定、并且需要独立实例的对象。

场景一:重新解析文件

在题库导入模块中,用户可能先导入 Excel,再切换到 Markdown,或者修改解析规则后重新解析。

如果解析过程包含独立的状态,例如解析选项、错误列表、预览数据和当前文件信息,那么创建新的 ParserViewModel 可以避免不必要的状态残留。

private void Reparse()
{
    var parser = _parserViewModelFactory();

    // 将本次解析任务所需的数据交给 parser。
    // 由当前导入流程负责管理该实例。
}

 

这里是否每次都需要创建新实例,仍取决于业务需求。如果解析器能够安全地重置全部状态,复用实例也可能是合理方案。委托工厂提供的是一种选择,而不是强制要求。

场景二:多窗口或多弹窗

当用户同时打开两个导入窗口时,每个窗口通常需要独立的编辑状态、当前文件和错误信息。

此时,为每个窗口创建一个新的窗口 ViewModel,往往比让多个窗口共享同一个 ViewModel 更符合业务语义。

需要特别注意的是,如果要求每个窗口拥有独立的整个对象图,只将最外层 ViewModel 注册为 Transient 并不总是充分的。

如果新创建的窗口 ViewModel 依赖某个 Singleton 子 ViewModel,那么多个窗口仍然可能共享该子对象。

因此,独立实例不仅取决于顶层对象,还取决于依赖图中各个对象的注册策略。

场景三:带参数的动态创建

如果新建的解析器需要在创建时获得文件路径、解析模式或任务编号,那么无参数的 Func<T> 可能就不够用了。

此时可以定义显式工厂接口:

public interface IParserViewModelFactory
{
    ParserViewModel Create(
        string filePath,
        ImportMode mode);
}

 

父级 ViewModel 依赖这个接口,而不是直接依赖 DI 容器。

public class ImportViewModel
{
    private readonly IParserViewModelFactory _factory;

    public ImportViewModel(
        IParserViewModelFactory factory)
    {
        _factory = factory;
    }

    private void OpenParser(
        string filePath,
        ImportMode mode)
    {
        var parser = _factory.Create(
            filePath,
            mode);
    }
}

 

工厂接口能够清晰表达创建参数,也方便在单元测试中替换实现。

它的代价是需要额外编写接口和实现类。因此,对于无参数、简单的创建需求,Func<T> 往往更轻量;对于参数复杂、创建规则多或者需要集中管理对象所有权的场景,显式工厂通常更容易维护。

七、设计原则:让创建职责与业务职责保持清晰

委托工厂真正有价值的地方,并不是减少了多少行代码,而是让对象的创建时机成为一个明确的设计决策。

传统构造函数注入表达的是:

这个对象需要另一个对象才能正常工作。

委托工厂表达的是:

这个对象需要在业务执行过程中,按需获取某种新对象的创建能力。

两者的语义不同,因此应当根据实际需求选择。

在 WPF MVVM 架构中,可以遵循以下原则:

  1. 对于稳定、持续使用的依赖,优先采用直接构造函数注入。

  2. 对于需要按需创建的独立实例,可以采用 Func<T> 委托工厂。

  3. 使用微软默认 DI 容器时,显式注册相应的 Func<T> 委托。

  4. 根据业务状态的归属决定生命周期,不要机械地将所有 ViewModel 都设置为 Transient。

  5. 对于需要创建参数、复杂初始化或明确所有权的对象,考虑使用显式工厂接口。

  6. 将对象释放、事件解绑和资源清理作为独立的生命周期设计问题处理。

八、总结

委托工厂(Delegate Factory)是一种将对象创建能力封装为委托的设计方式。在 C# 中,Func<T> 可以用较少的代码实现这一思想,并与依赖注入容器协同工作。

在合适的场景下,Func<T> + Transient 能够使父级 ViewModel 按需创建独立的子 ViewModel,降低对预先创建实例的依赖,并避免为了动态解析对象而将整个 IServiceProvider 暴露给业务层。

但它并不能自动解决所有状态隔离、生命周期和内存管理问题。实例是否独立,取决于委托的实际实现和整个依赖图的注册方式;资源是否得到释放,则取决于对象所有权和释放机制。

对于复杂的 WPF MVVM 应用,合理的设计不是一味追求工厂化,而是明确区分三个问题:什么时候创建对象、创建什么样的对象,以及由谁负责对象的生命周期。

当这三个问题都有清晰答案时,委托工厂才能真正发挥作用。

posted @ 2026-10-19 11:54  小林野夫  阅读(3)  评论(0)    收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/