我试图在C#中创建一个通用的(通用)pipeline,以便在许多项目中重用。这个想法与ASP.NET Core Middleware非常相似。它更像是一个可以动态组合的巨大功能(双向管道)(类似于BRE)
它需要获取输入模型,管理先前已加载的一系列处理器,并返回包含在超模型中的输出模型以及输入。
这就是我所做的。我创建了一个Context
类,代表整体数据/模型:
public class Context<InputType, OutputType> where InputType : class, new() where OutputType : class, new()
{
public Context()
{
UniqueToken = new Guid();
Logs = new List<string>();
}
public InputType Input { get; set; }
public OutputType Output { get; set; }
public Guid UniqueToken { get; }
public DateTime ProcessStartedAt { get; set; }
public DateTime ProcessEndedAt { get; set; }
public long ProcessTimeInMilliseconds
{
get
{
return (long)ProcessEndedAt.Subtract(ProcessStartedAt).TotalMilliseconds;
}
}
public List<string> Logs { get; set; }
}
然后我创建了一个接口,以在真实处理器上强制执行签名:
public interface IProcessor
{
void Process<InputType, OutputType>(Context<InputType, OutputType> context, IProcessor next) where InputType : class, new() where OutputType : class, new();
}
然后我创建了一个Container
来管理整个管道:
public class Container<InputType, OutputType> where InputType : class, new() where OutputType : class, new()
{
public static List<IProcessor> Processors { get; set; }
public static void Initialize()
{
LoadProcessors();
}
private static void LoadProcessors()
{
// loading processors from assemblies dynamically
}
public static Context<InputType, OutputType> Execute(InputType input)
{
if (Processors.Count == 0)
{
throw new FrameworkException("No processor is found to be executed");
}
if (input.IsNull())
{
throw new BusinessException($"{nameof(InputType)} is not provided for processing pipeline");
}
var message = new Context<InputType, OutputType>();
message.Input = input;
message.ProcessStartedAt = DateTime.Now;
Processors[0].Process(message, Processors[1]);
message.ProcessEndedAt = DateTime.Now;
return message;
}
}
我知道如何从给定文件夹中的程序集动态加载处理器,因此这不是问题。但是我坚持这些观点:
答案 0 :(得分:3)
我建议稍微不同的设计。这个想法是基于装饰模式的。
首先,我会使Context
成为非泛型类并删除输入和输出值。在我的设计中,上下文仅保存上下文信息(如处理时间和消息):
public class Context
{
public Context()
{
UniqueToken = new Guid();
Logs = new List<string>();
}
public Guid UniqueToken { get; }
public DateTime ProcessStartedAt { get; set; }
public DateTime ProcessEndedAt { get; set; }
public long ProcessTimeInMilliseconds
{
get
{
return (long)ProcessEndedAt.Subtract(ProcessStartedAt).TotalMilliseconds;
}
}
public List<string> Logs { get; set; }
}
然后,我会使处理器接口通用:
public interface IProcessor<InputType, OutputType>
{
OutputType Process(InputType input, Context context);
}
然后我将Container
转换为带有泛型类型参数的Pipeline
:
public interface IPipeline<InputType, OutputType>
{
OutputType Execute(InputType input, out Context context);
OutputType ExecuteSubPipeline(InputType input, Context context);
}
两个函数之间的区别在于前者初始化上下文而后者仅使用它。如果您不希望客户访问ExecuteSubPipeline()
,您可能希望将其拆分为公共和内部界面。
然后,我们的想法是将多个管道对象封装在一起,这些管道对象具有越来越多的处理器。您只需要一个只有一个处理器的管道对象。比你把它包装在另一个管道对象中等等。为此,我从一个抽象基类开始。此基类与处理器相关联,并具有函数AppendProcessor()
,该函数创建了添加了给定处理器的新管道:
public abstract class PipelineBase<InputType, ProcessorInputType, OutputType> : IPipeline<InputType, OutputType>
{
protected IProcessor<ProcessorInputType, OutputType> currentProcessor;
public PipelineBase(IProcessor<ProcessorInputType, OutputType> processor)
{
currentProcessor = processor;
}
public IPipeline<InputType, ProcessorOutputType> AppendProcessor<ProcessorOutputType>(IProcessor<OutputType, ProcessorOutputType> processor)
{
return new Pipeline<InputType, OutputType, ProcessorOutputType>(processor, this);
}
public OutputType Execute(InputType input, out Context context)
{
context = new Context();
context.ProcessStartedAt = DateTime.Now;
var result = ExecuteSubPipeline(input, context);
context.ProcessEndedAt = DateTime.Now;
return result;
}
public abstract OutputType ExecuteSubPipeline(InputType input, Context context);
}
现在,我们有两个这个管道的具体实现:一个终端实现,它是任何管道和一个包装器管道的起点:
public class TerminalPipeline<InputType, OutputType> : PipelineBase<InputType, InputType, OutputType>
{
public TerminalPipeline(IProcessor<InputType, OutputType> processor)
:base(processor)
{ }
public override OutputType ExecuteSubPipeline(InputType input, Context context)
{
return currentProcessor.Process(input, context);
}
}
public class Pipeline<InputType, ProcessorInputType, OutputType> : PipelineBase<InputType, ProcessorInputType, OutputType>
{
IPipeline<InputType, ProcessorInputType> previousPipeline;
public Pipeline(IProcessor<ProcessorInputType, OutputType> processor, IPipeline<InputType, ProcessorInputType> previousPipeline)
: base(processor)
{
this.previousPipeline = previousPipeline;
}
public override OutputType ExecuteSubPipeline(InputType input, Context context)
{
var previousPipelineResult = previousPipeline.ExecuteSubPipeline(input, context);
return currentProcessor.Process(previousPipelineResult, context);
}
}
为了便于使用,我们还创建一个辅助函数来创建一个终端启动管道(允许类型参数推导):
public static class Pipeline
{
public static TerminalPipeline<InputType, OutputType> Create<InputType, OutputType>(IProcessor<InputType, OutputType> processor)
{
return new TerminalPipeline<InputType, OutputType>(processor);
}
}
然后,我们可以使用这种结构与各种处理器。例如:
class FloatToStringProcessor : IProcessor<float, string>
{
public string Process(float input, Context context)
{
return input.ToString();
}
}
class RepeatStringProcessor : IProcessor<string, string>
{
public string Process(string input, Context context)
{
return input + input + input;
}
}
class Program
{
public static void Main()
{
var pipeline = Pipeline
.Create(new FloatToStringProcessor())
.AppendProcessor(new RepeatStringProcessor());
Context ctx;
var result = pipeline.Execute(5, out ctx);
Console.WriteLine($"Pipeline result: {result}");
Console.WriteLine($"Pipeline execution took {ctx.ProcessTimeInMilliseconds} milliseconds");
}
}
这将打印
Pipeline result: 555
Pipeline execution took 6 milliseconds
我不明白短路是什么意思。在我看来,短路只对(至少)二进制运算符有意义,其中一个操作数不需要被评估。但由于您的操作员都是一元的,因此无法真正应用。处理器总是可以检查输入,并在发现无需处理时直接返回。
通过向LoadProcessors()
界面添加IPipeline
之类的内容,可以轻松添加动态加载,类似于ExecuteSubPipeline()
。在这种情况下,处理器对象必须是代表(仍然正确键入)。然后,LoadProcessors()
可以在加载后用它们的实际处理器替换它们。