Pipelining в C#-приложениях

Автор: admin от 19-08-2016, 12:20, посмотрело: 479

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

Func<string, string> reverseWords =
    s => s.Words()
        .Select(StringExtensions.Reverse)
        .Unwords();

Pipelining, с которым мы работаем каждый день — это extension-методы для linq. На самом деле C# способен на большее и можно запрограммировать pipeline для любых входных и выходных аргументов с проверкой типов и поддержкой intellisense.

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


  1. свойство nested-классов — возможность обращаться к приватным свойствам класса-родителя

  2. generics

  3. паттерн 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 и реализовывать фьючерсы для асинхронного программирования (не знаю на сколько это полезно, просто пришло в голову)


  • Источник: Хабрахабр

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *