如何在C#中创建通用管道?

时间:2018-06-03 07:42:22

标签: c# pipeline

我试图在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;
    }
}

我知道如何从给定文件夹中的程序集动态加载处理器,因此这不是问题。但是我坚持这些观点:

  1. 如何将下一个处理器注入每个处理器(我可以在每个处理器上强制Next属性,但我想这是针对SRP,因为每个处理器应该只关心它工作,而不是保持链条)
  2. 如何确保正确的订购(一个选项是在每个处理器中都有Order属性,并确保它们没有重复值,但它似乎违反了SRP ,每个处理器应该只关心处理,而不是关于它的顺序)
  3. 如何确保简单使用?在为团队创建基础架构时,开发人员友好是一件大事。否则团队成员不会接受它。
  4. 如何设计链条以便可以进行短路?

1 个答案:

答案 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()可以在加载后用它们的实际处理器替换它们。