如何用SimpleInjector解析含运行时参数的实例并绑定Buffer?
我的程序中有大量需在运行时创建且至少包含一个参数的对象,存在多层嵌套类,其中新计算的数据为后续内部实例所必需。
结构说明
- 用户发送字符串请求 ->
- 程序确定
IRequestContext和ICommand-> - 调用
ICommand.Execute(IRequestContext)-> - 执行过程通过工厂创建
IStep[]和IPlan(IStep[])并发送至调度器 -> - 调度器在指定时间结合
IRequestContext启动IPlan-> IPlan通过调用IObservable<Report> IStep.Make(IRequestContext)逐步执行 ->IStep的实现包含Buffer实例,用于在当前Plan范围内共享执行结果,它不返回但对生成最终Report及其他用途是必需的,且Buffer无法定义为接口,本质是数据存储 ->Report包含enum Result和IMessageBuilder,IMessageBuilder仅包含一个Build()方法。
细节补充
- 每个类自身依赖诸多接口(如数据库操作、本地化、通信API接口),这些依赖已在组合根注册,不存在问题。
IRequestContext实例为每个用户请求在运行时创建,IStep、IMessageBuilder及各类Buffer均依赖它。因此无法直接注册SomeMessageBuilder,因为构建器所需数据在步骤执行过程中生成,且不同请求不能共享同一MessageBuilder或Buffer实例。- 每个
IMessageBuilder的实现所需参数数量和类型不同,因此无法修改接口让IMessageBuilder.Build(Entity1, Entity2, ...)接收这些实体,不得不为每个IMessageBuilder实现创建并注册工厂,导致每个IStep实现依赖IFactory<T1,T2,...,IMessageBuilder>。 - 每个
IPlan实现都有专属工厂来创建步骤的独特组合,此外还使用了步骤的组合模式实现(如CompositeStep(IStep[] step) : IStep),因此不得不为每个IStep实现注册工厂,还要为不同步骤组成的每个CompositeStep注册工厂。
简化示例
(注:有数十个不同的IPlan实现,它们可能使用相同的步骤)
核心接口与类
public interface IPlan<in TContext,out TReport>{ IObservable<TReport> Execute(TContext context); bool IsReadyToGoNext(TReport report); } public interface IStep<in TContext,out TReport>{ IObservable<TReport> Make(TContext context); } public interface IFactory<in T1, out TObject> { TObject Create(T1 param); } public interface IMessageBuilder{ SendMessage Build(); } public class CreateMessageBuilder : LocalizedMessageBuilder, IMessageBuilder { public CreateMessageBuilder(long chatId, CultureInfo info , ILocalizationProvider localizationProvider, int minSymbols, int maxSymbols) {...} public void SetUser(UserRepresentation representation){...}; public void SetSirena(SirenRepresentation sirena){...}; public void IsUserAllowedToCreateSirena(bool isAllowed){...}; public void IsTitleValid(bool isValid){...}; public override SendMessage Build(){...}; }
Plan工厂示例
public class CreateSirenaPlanFactory : IFactory<IRequestContext,CommandPlan> { private readonly Container container; private readonly IFactory<IRequestContext, CreateMessageBuilder> messageBuilderFactory; private readonly IFactory<CreateMessageBuilder, CreateSirenaStep.Buffer> bufferFactory; public CreateSirenaPlanFactory(Container container , IFactory<IRequestContext, CreateMessageBuilder> messageBuilderFactory , IFactory<CreateMessageBuilder,CreateSirenaStep.Buffer> bufferFactory) { this.container = container; this.messageBuilderFactory = messageBuilderFactory; this.bufferFactory = bufferFactory; } public IPlan Create(IRequestContext context) { CreateMessageBuilder messageBuilder = messageBuilderFactory.Create(context); CreateSirenaStep.Buffer buffer = bufferFactory.Create(messageBuilder); // 同一Buffer实例需在以下步骤组间共享 // 但每个新请求都要创建新的Plan和Buffer实例 var validation = new CompositeCommandStep([ container.GetInstance<CheckAbilityToCreateSirenaStep>(), container.GetInstance<ValidateTitleCreateSirenaStep>() ]); IStep<IRequestContext,CommandStep.Report>[] steps = [ container.GetInstance<GetUserCreateSirenaStep>(), validation, container.GetInstance<RequestDBToCreateSirenaStep>(), ]; return new CommandPlan(CreateSirenaCommand.NAME,steps); } }
核心问题
如何完成这些绑定?我不想为每个依赖一个或多个运行时生成参数的IStep实现创建工厂,且因SimpleInjector无上下文概念,完全不知道该如何绑定Buffer。
补充说明:曾使用Unity引擎的Zenject/Extenject,它有更多划分绑定上下文、为特定工厂的实例创建条件绑定的选项,可惜SimpleInjector缺乏这类工具。原本可为每个Plan实例创建上下文,每个上下文对应一个Buffer实例,且无需为每个需参数的实例创建工厂,只需绑定原生带参工厂。令我意外的是SimpleInjector文档仅提及作用域未提及上下文,而我无法在作用域中创建Buffer实例,因为作用域会在Create方法结束时关闭,实例会被释放。或许可使用流动作用域,但仍不清楚如何解析带参数的实例。
针对SimpleInjector的特性,可通过以下几种方式解决你的问题,避免大量手动创建工厂,同时处理Buffer的上下文共享:
1. 利用SimpleInjector自动工厂替代手动IFactory实现
SimpleInjector支持自动解析Func<TArg, TInstance>类型依赖,无需手动实现IFactory<T1, TObject>接口。
比如对于CreateMessageBuilder,先在组合根注册其依赖的服务,再注册一个工厂委托,从IRequestContext提取运行时参数:
// 先注册固定依赖 container.Register<ILocalizationProvider>(Lifestyle.Singleton); // 注册自动工厂委托 container.RegisterInstance<Func<IRequestContext, CreateMessageBuilder>>(context => new CreateMessageBuilder( context.ChatId, context.CultureInfo, container.GetInstance<ILocalizationProvider>(), context.Settings.MinSymbols, context.Settings.MaxSymbols));
之后其他类直接依赖Func<IRequestContext, CreateMessageBuilder>即可创建实例,无需手动编写工厂类。
2. 使用流动作用域管理Plan级Buffer生命周期
流动作用域可让Buffer在Plan创建到执行完成的整个周期内保持存活,避免作用域提前关闭的问题。
步骤:
- 在组合根配置默认作用域为异步流动作用域,并注册Buffer:
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); container.Register<CreateSirenaStep.Buffer>(Lifestyle.Scoped);
- 在Plan工厂的
Create方法中启动流动作用域,解析Buffer并关联MessageBuilder:
public IPlan Create(IRequestContext context) { var scope = AsyncScopedLifestyle.BeginScope(container); // 从当前作用域解析Buffer,后续步骤共享同一实例 var buffer = scope.GetInstance<CreateSirenaStep.Buffer>(); var messageBuilder = scope.GetInstance<Func<IRequestContext, CreateMessageBuilder>>()(context); buffer.MessageBuilder = messageBuilder; // 解析步骤时,自动从流动作用域获取Buffer var validation = new CompositeCommandStep([ scope.GetInstance<CheckAbilityToCreateSirenaStep>(), scope.GetInstance<ValidateTitleCreateSirenaStep>() ]); var steps = new IStep<IRequestContext, CommandStep.Report>[] { scope.GetInstance<GetUserCreateSirenaStep>(), validation, scope.GetInstance<RequestDBToCreateSirenaStep>(), }; // 将作用域与Plan绑定,执行时确保作用域激活 var plan = new CommandPlan(CreateSirenaCommand.NAME, steps) { ExecutionScope = scope }; return plan; }
- 在
IPlan.Execute方法执行前,检查并激活绑定的作用域,执行完成后再释放。
3. 用装饰器模式自动为步骤注入Buffer
为需要Buffer的步骤创建装饰器,自动注入当前作用域内的Buffer实例,无需手动传递:
// 标记接口,让需要Buffer的步骤实现 public interface IBufferAwareStep<TBuffer> { void SetBuffer(TBuffer buffer); } // 步骤装饰器 public class StepBufferDecorator<TContext, TReport, TBuffer> : IStep<TContext, TReport> { private readonly IStep<TContext, TReport> _decoratedStep; private readonly TBuffer _buffer; public StepBufferDecorator(IStep<TContext, TReport> decoratedStep, TBuffer buffer) { _decoratedStep = decoratedStep; _buffer = buffer; } public IObservable<TReport> Make(TContext context) { if (_decoratedStep is IBufferAwareStep<TBuffer> bufferAware) { bufferAware.SetBuffer(_buffer); } return _decoratedStep.Make(context); } }
在组合根为特定步骤注册装饰器:
container.RegisterDecorator( typeof(IStep<,>), typeof(StepBufferDecorator<,,>).MakeGenericType( typeof(IRequestContext), typeof(CommandStep.Report), typeof(CreateSirenaStep.Buffer)), context => context.ImplementationType == typeof(CheckAbilityToCreateSirenaStep));
这样步骤被解析时会自动获取对应的Buffer实例。
4. 自定义执行上下文传递共享实例
创建一个PlanExecutionContext类,封装IRequestContext、Buffer、MessageBuilder等共享实例,在Plan创建时初始化,步骤执行时统一传递:
public class PlanExecutionContext { public IRequestContext RequestContext { get; } public CreateSirenaStep.Buffer Buffer { get; } public IMessageBuilder MessageBuilder { get; } public PlanExecutionContext(IRequestContext requestContext, CreateSirenaStep.Buffer buffer, IMessageBuilder messageBuilder) { RequestContext = requestContext; Buffer = buffer; MessageBuilder = messageBuilder; } } // 修改IStep接口 public interface IStep<out TReport>{ IObservable<TReport> Make(PlanExecutionContext context); }
Plan工厂只需初始化这个上下文,步骤直接从上下文获取所需实例,无需依赖工厂。
内容的提问来源于stack exchange,提问作者HedgehogNSK

