【C#】パイプラインアーキテクチャ入門編

# パイプラインアーキテクチャとは

パイプとフィルター間の一方向の通信が形成されるアーキテクチャです。

**パイプ**はフィルター間の通信チャンネルを指し、**フィルター**は値の変換やバリデーションを行う自己完結型の処理形態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3852955/dea37bc3-dd1b-4b59-a9c5-f38c180343ee.png)
# 実装例

ECサイトの一部機能を例として取り上げます。

1. 顧客情報が入力される
2. 価格が0以上であることを確認する
3. 100円以上の場合、10%割引する
4. ログを記録する
5. データベースに保存する

```csharp
internal class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = string.Empty;
    public decimal Amount { get; set; }
    public bool IsValid { get; set; } = true;
}
```

## ①一番ダメ実装

ダメな実装例として、OrderManagerのSaveメソッドに全ての処理を詰め込んだ実装を紹介します

### OrderManagerクラス

```csharp
internal class OrderManager
{
    public Order Save(Order order)
    {
        // バリデーション
        if (order.Amount < 0)
        {
            order.IsValid = false;
        }

        // 割引
        if (order.Amount > 100)
        {
            order.Amount *= 0.9m;
        }

        // ログ
        Console.WriteLine($"Order {order.Id}: Amount = {order.Amount}, Valid = {order.IsValid}");

        // データベース保存
        // 模擬的にコンソール表示
        Console.WriteLine($"Saving order {order.Id} to database...");

        return order;
    }
}
```

### 実行クラス

```csharp
static void Main()
{
    var orderManager = new OrderManager();
    var order = new Order { Id = 1, Customer = "Alice", Amount = 150 };
    orderManager.Save(order);
}
```

問題点は以下の通りです：

- 処理を変更する場合にSaveメソッドに直接手を入れる必要がある（変更リスクが高い）
- 同期処理のため、処理中にメインスレッドがブロックされる

## ②少し改善

①をベースにして改善を行います。

改善項目は以下の通りです。

- 各処理を個別のメソッドに分割する
- 処理全体を非同期化する

### OrderManagerクラス

```csharp
internal class OrderManager
{
    public Task<Order> Save(Order order)
    {
        // バリデーション
        this.Validation(ref order);

        // 割引
        this.Discount(ref order);

        // ログ
        this.Logging(order);

        // データベース保存
        this.SaveToDatabase(order);

        return Task.FromResult(order);
    }

    private void SaveToDatabase(Order order)
    {
		    // 模擬的にコンソール表示
        Console.WriteLine($"Saving order {order.Id} to database...");
    }

    private void Logging(Order order)
    {
        Console.WriteLine($"Order {order.Id}: Amount = {order.Amount}, Valid = {order.IsValid}");
    }

    private void Discount(ref Order order)
    {
        if (order.Amount > 100)
        {
            order.Amount *= 0.9m;
        }
    }

    private void Validation(ref Order order)
    {
        if (order.Amount < 0)
        {
            order.IsValid = false;
        }
    }
}

```

### 実行クラス

```csharp
public static async Task Main()
{
    var orderManager = new OrderManager();
    var order = new Order { Id = 1, Customer = "Alice", Amount = 150 };
    await orderManager.Save(order);
}
```

このようなコードは業務上でもよく見ますが、以下の問題点があります。

- Order以外のクラスに対する処理を追加する際、上記のSaveのような【処理をシーケンシャルに実行するメソッド】を複製しなければならない
    
    ```csharp
    // 商品（Goods）クラスの追加
    internal class Goods
    {
        public int Id { get; set; }
        public string Name { get; set; } = string.Empty;
        public decimal Amount { get; set; }
        public bool IsValid { get; set; } = true;
    }
    
    internal class GoodsManager
    {
        public Task<Goods> Save(Goods goods)
        {
            // ---順次処理---
    
            return Task.FromResult(order);
        }
    }
    ```
    
- バリデーションや割引のような処理を単体でテストすることができない

## ③パイプラインアーキテクチャ

ここではフィルターを、より簡明にStepと呼びます。

### Pipeline上の各Stepを実装させるインターフェース

```csharp
internal interface IPipelineStep<T>
{
    Task<T> ProcessAsync(T input);
}
```

### パイプラインクラス

```csharp
internal class Pineline<T>
{
		// パイプラインの実行順リスト
    private readonly List<IPipelineStep<T>> _steps = new List<IPipelineStep<T>>();

		// リストに追加
    public Pineline<T> AddStep(IPipelineStep<T> step)
    {
        _steps.Add(step);
        return this;
    }

		// リスト順に実行する
    public async Task<T> ExecuteAsync(T input)
    {
        T result = input;
        foreach (var step in _steps)
        {
            result = await step.ProcessAsync(result);
        }
        return result;
    }
}
```

### 各Stepをクラスとして定義

```csharp
internal class ValidationStep : IPipelineStep<Order>
{
    public Task<Order> ProcessAsync(Order order)
    {
        if(order.Amount < 0)
        {
            order.IsValid = false;
        }
        return Task.FromResult(order);
    }
}
```

```csharp
internal class DiscountStep : IPipelineStep<Order>
{
    public Task<Order> ProcessAsync(Order order)
    {
        if(order.Amount > 100)
        {
            order.Amount *= 0.9m;
        }
        return Task.FromResult(order);
    }
}
```

```csharp
internal class LoggingStep : IPipelineStep<Order>
{
    public Task<Order> ProcessAsync(Order order)
    {
        Console.WriteLine($"Order {order.Id}: Amount = {order.Amount}, Valid = {order.IsValid}");
        return Task.FromResult(order);
    }
}
```

```csharp
internal class SaveToDatabaseStep : IPipelineStep<Order>
{
    public Task<Order> ProcessAsync(Order order)
    {
        Console.WriteLine($"Saving order {order.Id} to database...");
        return Task.FromResult(order);
    }
}
```

### 実行クラス

```csharp
public static async Task Main()
{
		// パイプラインに処理を追加
    var pipeline = new Pineline<Order>()
        .AddStep(new ValidationStep())
        .AddStep(new DiscountStep())
        .AddStep(new LoggingStep())
        .AddStep(new SaveToDatabaseStep());

    var order = new Order { Id = 1, Customer = "Alice", Amount = 150 };
    
    // 処理の順次実行
    await pipeline.ExecuteAsync(order);            
}
```

- OrderManagerのような、【Order専用で、シーケンシャルな処理を行うメソッド】を作成しなくてもよい
- Goodsクラスの処理を行う場合は、pipelineの型をPineline<Order>からPineline<Goods>に変更し、Goodsに対応した各処理クラスを追加していけばよい
- ValidationStepなどの単体テストが容易である（1つずつつまんでテストすればいいからね）

# メリット

- 責務がクラス単位で分割される(凝集性↑)
- 処理の追加、削除が容易(変更容易性↑)
- 「処理を一つずつシーケンシャルに行うメソッド」が再利用できる(再利用性↑)