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. узагальнення
  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 Do Step([NotNull] Action action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
return new Step(Pipeline, action);
} 
}
}

І методи для створення pipeline:

public static Do Step(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. Практичне застосування:

  1. об'єднання операцій в логічні ланцюжки і виконання їх в єдиному контексті (наприклад, в транзакції)
  2. Замість Func Action Command Queryі створювати ланцюжка викликів
  3. Також можна використовувати Task та реалізовувати ф'ючерси для асинхронного програмування (не знаю на скільки це корисно, просто прийшло в голову)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.