Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 9 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 9 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Pipelining в C#-приложениях | Хостинг за 90 р. от cyberhost.biz — платный хостинг
+7 993 930-19-90 suport@cyberhost.biz


В мире функционального программирования существует мощная концепция композиции функций. В C# тоже можно «встроить» каррирование и композицию, но смотрится это так себе. Вместо композиции в C# широкое применение нашел pipelining:

Func<string, string> reverseWords =
s => s.Words()
.Select(StringExtensions.Reverse)
.Unwords();
Pipelining, с которым мы работаем каждый день — это extension-методы для linq. На самом деле C# способен на большее и можно запрограммировать pipeline для любых входных и выходных аргументов с проверкой типов и поддержкой intellisense.

Для разработки pipeline будут использоваться:

  • свойство nested-классов — возможность обращаться к приватным свойствам класса-родителя
  • generics
  • паттерн fluent interface
  • Получится вот так:

    var pipeline = Pipeline
    .Start(() => 10, x => x + 6)
    .Pipe(x => x.ToString())
    .Pipe(int.Parse)
    .Pipe(x => Math.Sqrt(x))
    .Pipe(x => x * 5)
    .Pipe(x => new Point((int) Math.Round(x), 120))
    .Finish(x => Debug.WriteLine($"{x.X}{x.Y}"))
    .Do(() => Debug.WriteLine("Point is so cool"));
    // …
    pipeline.Execute();
    Или так, применительно к CQRS и прикладному коду:

    public class CreateBusinessEntity : ContextCommandBase<CreateBusinessEntityDto>
    {
    public CreateBusinessEntity(DbContext context) : base(context) {}
    public override int Execute(CreateBusinessEntityDto obj) => Pipeline
    .Pipe(obj, Map<CreateBusinessEntityDto, BusinessEntity>)
    .Pipe(SaveEntity)
    .Execute();
    }

    Для начала потребуется класс-контейнер, внутренний интерфейс для вызова функций и внешний — для реализации fluent interface:

    public class Pipeline
    {
    private readonly object _firstArg;
    private object _arg;
    private readonly List<IInvokable> _steps = new List<IInvokable>();
    private Pipeline(object firstArg)
    {
    _firstArg = firstArg;
    _arg = firstArg;
    }
    internal interface IInvokable
    {
    object Invoke();
    }
    public object Execute()
    {
    _arg = _firstArg;
    foreach (IInvokable t in _steps)
    {
    _arg = t.Invoke();
    }
    return _arg;
    }
    public abstract class StepBase
    {
    protected Pipeline Pipeline;
    public Step Do([NotNull] Action action)
    {
    if (action == null) throw new ArgumentNullException(nameof(action));
    return new Step(Pipeline, action);
    }
    }
    }

    И методы для создания pipeline:

    public static Step Do(Action firstStep)
    {
    var p = new Pipeline(null);
    return new Step(p, firstStep);
    }
    public static Step<TInput, TOutput> Pipe<TInput, TOutput>(
    TInput firstArg,
    Func<TInput, TOutput> firstStep)
    {
    var p = new Pipeline(firstArg);
    // ReSharper disable once ObjectCreationAsStatement
    return new Step<TInput, TOutput>(p, firstStep);
    }
    public static Step<TInput, TOutput> Start<TInput, TOutput>(
    Func<TInput> firstArg,
    Func<TInput, TOutput> firstStep)
    {
    return Pipe(firstArg, x => x.Invoke())
    .Pipe(firstStep);
    }

    Теперь дело за реализациями шаблонов для fluent interface

    public class Step : StepBase, IInvokable
    {
    private readonly Action _action;
    public Step(Pipeline pipeline, Action action)
    {
    Pipeline = pipeline;
    _action = action;
    Pipeline._steps.Add(this);
    }
    object IInvokable.Invoke()
    {
    _action.Invoke();
    return Pipeline._arg;
    }
    public void Execute() => Pipeline.Execute();
    }
    public class Step<TInput> : StepBase, IInvokable
    {
    private readonly Pipeline _pipe;
    private readonly Action<TInput> _action;
    public Step(Pipeline pipe, Action<TInput> action)
    {
    _pipe = pipe;
    _action = action;
    _pipe._steps.Add(this);
    }
    object IInvokable.Invoke()
    {
    _action.Invoke((TInput)_pipe._arg);
    return _pipe._arg;
    }
    public void Execute() => Pipeline.Execute();
    }
    public class Step<TInput, TOutput> : StepBase, IInvokable
    {
    private readonly Pipeline _pipe;
    private readonly Func<TInput, TOutput> _func;
    internal Step(Pipeline pipe, Func<TInput, TOutput> func)
    {
    _pipe = pipe;
    _func = func;
    _pipe._steps.Add(this);
    }
    object IInvokable.Invoke() => _func.Invoke((TInput) _pipe._arg);
    public Step<TOutput, TNext> Pipe<TNext>([NotNull] Func<TOutput, TNext> func)
    {
    if (func == null) throw new ArgumentNullException(nameof(func));
    return new Step<TOutput, TNext>(_pipe, func);
    }
    public Step<TOutput> Finish([NotNull] Action<TOutput> action)
    {
    if (action == null) throw new ArgumentNullException(nameof(action));
    return new Step<TOutput>(Pipeline, action);
    }
    public TOutput Execute() => (TOutput)_pipe.Execute();
    }

    Шаблоны помогаю гарантировать, что в метод Pipe придет «правильный» аргумент. Отдельного внимания заслушивает метод Start, который позволяет передать в качестве аргумента не значение, а функцию:

    var point = Pipeline
    .Start(() => 10, x => x + 6)
    .Pipe(x => x.ToString())
    .Pipe(int.Parse)
    .Pipe(x => Math.Sqrt(x))
    .Pipe(x => x * 5)
    .Pipe(x => new Point((int)Math.Round(x), 120))
    .Execute();

    Все некрасивые моменты, связанные с работой по ссылке на тип object мы спрятали внутрь сборки:

    public object Execute()
    {
    _arg = _firstArg;
    foreach (IInvokable t in _steps)
    {
    _arg = t.Invoke();
    }
    return _arg;
    }

    Полный код доступен на github. Практическое применение:

  • объединение операций в логические цепочки и выполнение их в едином контексте (например, в транзакции)
  • Вместо Func и Action можно использовать Command и Queryи создавать цепочки вызовов
  • Также можно использовать Task и реализовывать фьючерсы для асинхронного программирования (не знаю на сколько это полезно, просто пришло в голову)
  • Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0