什么是工厂方法:我不手动new,而是让一个方法去帮我new //自己动手 Config cfg = new Config("程序名称", "1.0"); // 用工厂方法 Config cfg = CreateConfig(); CreateConfig就是一个工厂方法 什么时候会需要这个东西呢 当对象的创建过程比较复杂,或者你需要控制创建逻辑时,就可以封装到一个工厂方法里。 // 创建配置类很复杂:可能有默认值、验证、从不同来源读取等 public Config CreateConfig() { // 可能有各种判断逻辑 var name = GetNameFromEnvironmentOrConfig(); var version = GetVersion(); if (string.IsNullOrEmpty(name)) name = "默认名称"; return new Config(name, version); } 欸,这样我们就能很快拿到一个这么复杂的实例了。调用的时候直接拿过来就行,不用考虑是否落下了什么东西 工厂方法可以是静态可以是实例 // 静态工厂方法 public class ConfigFactory { public static Config Create() { return new Config("默认名称", "1.0"); } } // 使用 Config cfg = ConfigFactory.Create(); 看一个完整例子体会一下 using System; public class Config { public string Name { get; set; } public string Version { get; set; } public Config(string name, string version) { Name = name; Version = version; } public void Print() { Console.WriteLine($"程序:{Name} v{Version}"); } } public class ConfigFactory { // 静态方法:装作从配置文件创建 public static Config CreateFromFile() { // 假设这里读 json,简化成直接返回 Console.WriteLine("从文件读取配置..."); return new Config("我的程序", "1.0.0"); } // 静态方法:装作创建开发环境配置 public static Config CreateDevConfig() { Console.WriteLine("创建开发环境配置..."); return new Config("开发版", "dev-0.1"); } // 静态方法:创建默认配置 public static Config CreateDefault() { Console.WriteLine("使用默认配置..."); return new Config("未命名", "0.0.0"); } } public class Program { static void Main(string[] args) { // 能工智人:手动 new Config cfg1 = new Config("手动创建", "1.0"); cfg1.Print(); Console.WriteLine("---"); // 用工厂方法:工厂 new Config cfg2 = ConfigFactory.CreateFromFile(); cfg2.Print(); Console.WriteLine("---"); Config cfg3 = ConfigFactory.CreateDevConfig(); cfg3.Print(); Console.WriteLine("---"); Config cfg4 = ConfigFactory.CreateDefault(); cfg4.Print(); } } 运行结果: 程序:手动创建 v1.0 --- 从文件读取配置... 程序:我的程序 v1.0.0 --- 创建开发环境配置... 程序:开发版 vdev-0.1 --- 使用默认配置... 程序:未命名 v0.0.0 工厂方法搞的差不多了,接下来就是热血沸腾的组合技 工厂方法与DI组合 services.AddSingleton<Config>(provider => { // 这里就是“工厂方法” return ConfigFactory.CreateFromFile(); }); 乍一看诶呦挺复杂,不急,慢慢来琢磨 首先,用方法一定要懂原理,理解为什么要这么用,这么用有什么好处,才能把方法用的更好 DI 搭配工厂方法,核心就是解决这个问题:DI 容器默认会 new 你的类,但是它不够聪明,只会这两种方式: 自动推断:services.AddSingleton<Calculator>(),容器自动找到构造函数,把参数都注入好。 用现成的: services.AddSingleton(new Config(...)),直接用你给的对象。 一些复杂的new 需要你教它怎么去做,因此你就得在注册的地方告诉容器一个创建规则——这个规则,就是用工厂方法表达的。 静态工厂方法 // 某个业务类 public class ReportService { public ReportService(Config config) { ... } } // 注册时,直接调用静态工厂 using var services = new ServiceCollection() // 告诉容器:创建 Config 时,就去调 ConfigFactory.CreateFromFile() .AddSingleton<Config>(_ => ConfigFactory.CreateFromFile()) // ReportService 正常注册,容器会自动注入 Config .AddSingleton<ReportService>() .BuildServiceProvider(); // 获取时,容器内部会执行 CreateFromFile() var report = services.GetService<ReportService>(); 大概是怎么个执行情况呢: 容器需要 Config → 执行ConfigFactory.CreateFromFile() → 拿到对象 → 缓存(因为是 Singleton)。 逻辑全部放在了ConfigFactory,注册代码挺干净,不赖 另一种情况,上lambda 如果注册逻辑很简单,可以不去写一个类,直接用lambda using var services = new ServiceCollection() // 这个 Lambda 就是一个工厂方法 .AddSingleton<Config>(sp => { Console.WriteLine("正在创建 Config"); // 可以在这里访问其他服务 var env = sp.GetService<IEnvironment>(); return new Config(env.GetName(), "2.0.0"); }) .BuildServiceProvider(); 还有一种情况 借这个情况一起说说生命周期混搭 长命服务不能直接依赖短命服务 // 短命服务(Transient) public class DataContext { public Guid Id { get; } = Guid.NewGuid(); // 每次新建都有新 ID } // 长命服务(Singleton) public class ReportService { private readonly DataContext _context; // 直接持有短命服务 public ReportService(DataContext context) { _context = context; // 构造函数注入时被赋值 } public void PrintId() { Console.WriteLine(_context.Id); } } services.AddTransient<DataContext>(); services.AddSingleton<ReportService>(); 好我们来看看,会发生什么 程序启动,容器创建 ReportService(Singleton) 创建时发现需要 DataContext,于是 new DataContext(),Id 是 aaaa 把 Id 为 aaaa 的 DataContext 注入到 ReportService 里 ReportService 是 Singleton,整个程序就这一个实例 所以它里面的 _context 也永远是 Id 为 aaaa 的那一个 之后每次调用 PrintId(),输出永远是 aaaa。 这就是“被捕获” Singleton 容器里: ReportService(全局唯一) └── DataContext(Id: aaaa)← 永远是这个,不会再换 短命服务被长命服务捕获后,就和长命服务同生共死了。 怎么解决这种情况 既然直接持有不行,那就以旧换新 每次要用了就拿个新的来 public class ReportService { // 注入一个工厂,而不是直接注入 DataContext private readonly Func<DataContext> _factory; public ReportService(Func<DataContext> factory) { _factory = factory; // 工厂本身是长命的,没关系 } public void PrintId() { var context = _factory(); // 每次调用工厂,拿到全新的 DataContext Console.WriteLine(context.Id); } } services.AddTransient<DataContext>(); services.AddSingleton<ReportService>(); // 容器会自动为 DataContext 生成 Func<DataContext> 工厂 var report = provider.GetService<ReportService>(); report.PrintId(); // Id: bbbb report.PrintId(); // Id: cccc // 容器自动生成的 Func<DataContext> 内部等价于: Func<DataContext> factory = () => new DataContext(); // 每次调 factory() 都重新执行 new DataContext() 对于Scoped也是一样,不多赘述 来个总结: Singleton 想用 Scoped → IServiceScopeFactory Singleton 想用 Transient → Func<T> Scoped 想用 Transient → 可以直接用 这时候有个问题: 怎么Scoped能直接依赖Transient,再怎么说Scoped也比Transient长命啊 因为Scoped不会一直抓着Transient,它也会被销毁。当它被销毁了,Transient也一并销毁,不存在一直抓着的问题 请求 1 开始 ├── 创建 Scoped 容器 │ ├── 创建 ScopedService A │ │ └── 注入 TransientService(Id: aaaa)← 这次是这个 │ └── ... ├── 请求结束,Scoped 容器销毁 ├── aaaa 也一样跟着销毁了 请求 2 开始 ├── 创建全新的 Scoped 容器 │ ├── 创建 ScopedService B(全新的) │ │ └── 注入 TransientService(Id: bbbb)← 这次是新的 │ └── ... ├── 请求结束,Scoped 容器销毁 ├── bbbb 也跟着销毁 生命周期混搭真正出问题的,只有一种情况:Singleton 捕获了短命服务。 Scoped 不会出问题。 Singleton 从来不销毁,所以被它捕获的 Transient 也永远不销毁。 程序启动 ├── 创建 SingletonService(唯一实例) │ └── 注入 TransientService(Id: aaaa)← 创建了 ├── 之后再也没有重新创建 SingletonService ├── 所以 TransientService 也永远是 aaaa,再也不会变 ├── ├── 请求 1 → 用 aaaa ├── 请求 2 → 还是 aaaa ├── 请求 100 → 还是 aaaa ├── ... ├── 程序结束 → aaaa 才跟着销毁 如此从头抓到尾 生命周期混搭的判断标准不是谁更长命,而是谁一直不死
工厂方法、DI框架和生命周期
以下为完整正文内容。
正文
搜索结果
请输入关键词开始搜索。