Javaで鉄道指向プログラミング(Railway Oriented Programming)を実践する

:::note info
この記事は[ZOZO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/zozo) シリーズ8の10日目の記事です。
:::

## はじめに
2024年も終盤ですね。ちなみに今年のマイベスト技術書は[関数型ドメインモデリング](https://asciidwango.jp/post/754242099814268928/%E9%96%A2%E6%95%B0%E5%9E%8B%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%A2%E3%83%87%E3%83%AA%E3%83%B3%E3%82%B0)でした。「日本語で読みたいなあ」と思っていた矢先の出版、あまりに僥倖でした。[^t-wadaさん]

[^t-wadaさん]: [t-wadaさんの尽力](https://x.com/t_wada/status/1861983779839250523?s=46)もあったらしいですね、本当に関係者各位ありがとうございます。

この記事では、関数型ドメインモデリングでも紹介されていた、`鉄道指向プログラミング(Railway Oriented Programming)`をJavaで実践する方法について、**難しい言葉は使わずに説明していきます。**

関数型という言葉だけでちょっとハードルが上がる気持ち、わかります。ですが安心してください。この記事は気楽に読み進めていただけると思います。[^具体から抽象]

[^具体から抽象]: この記事の主題である鉄道指向プログラミングを提唱した本人も[Abstraction, intuition, and the “monad tutorial fallacy”](https://byorgey.wordpress.com/2009/01/12/abstraction-intuition-and-the-monad-tutorial-fallacy/)にて、具体的なことから始めて、抽象なものへ進むという教育的アプローチを支持しているようです。この記事でも抽象的な説明はせず具体例について説明してきます。(筆者が関数型プログラミングあまりわかっていないので出来ないだけ)

## Vavr
本題へと入る前に、Javaで関数型プログラミングを行うためのライブラリであるVavrについて、簡単に説明します。

https://github.com/vavr-io

この記事では主に`Either`を利用します。
`Either`はLeftとRightどちらかの値を返す型です。それぞれが成功と失敗を表すような使い方をする場合も多く、その場合はRightを成功として扱うのが(駄洒落による)慣習です。
下の例のように`Either`を導入することで、Javaでも例外機構を利用せず戻り値で成功と失敗を伝播することが容易になります。

```java
import io.vavr.control.Either;

public class EitherExample {
  record InputValue(Integer value) {}

  record ValidatedValue(Integer value) {}

  /**
   * 例外で伝播する
   */
  ValidatedValue validateUsingException(final InputValue inputValue) {
    // 偶数はNG
    if (inputValue.value() % 2 == 0)
        throw new IllegalArgumentException("偶数はNGです");

    return new ValidatedValue(inputValue.value());
  }

  /**
   * 戻り値で伝播する(良い例ではないが失敗の場合はその理由を文字列で返す)
   */
  Either<String, ValidatedValue> validateUsingEither(final InputValue inputValue) {
    // 偶数はNG
    if (inputValue.value() % 2 == 0)
        return Either.left("偶数はNGです");

    return Either.right(new ValidatedValue(inputValue.value()));
  }
}
```

Vavrの詳しい利用方法はユーザーガイドをご覧ください。

https://docs.vavr.io/

## 鉄道指向プログラミング(Railway Oriented Programming)
鉄道指向プログラミング(Railway Oriented Programming)(以後、ROPと表記)は、関数型ドメインモデリングの著者でもあるScott Wlaschinさんが提唱したプログラミング手法です。

https://fsharpforfunandprofit.com/rop/

この後に実践しながら詳細を説明するので、ここでは超絶意訳で概要をまとめると、
`ROPは、例外機構を使わずにエラー（いわゆる異常系）を伝播させるプログラミング手法においてノイズになりがちなエラーハンドリング処理をスマートに行うプログラミング手法です。`
鉄道のレールを連結するかのようなアプローチを取るため鉄道指向プログラミングと呼んでいます。

もちろんROPについて、正確に詳細を知りたい方は上の原典を読んでいただくのが吉です。

## JavaでROPを実践する
それでは実践していきます。

### 題材
原典に従って
- 検証(Validate) -> 更新(Update) -> 送信(Send) のステップを連続して行う
- いずれも失敗する可能性があり、失敗した場合には次のステップには進まない
- 全て成功した場合が正常系(Success)、それ以外は異常系(Failure)を戻り値として返す

といった典型的なユースケースを考えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/a8718209-3324-12bb-a901-5b65b646a053.png)

### 素朴な実装
まずはROPを実践せずに、Vavrを用いて素朴に実装すると以下のようになります。

```java
public class NaiveImplUseCase {
    Either<FailedReason, Success> execute(final InputValue inputValue) {
        //  検証ステップ
        final Either<String, ValidatedValue> validateResult = validate(inputValue);
        if (validateResult.isLeft())
            return Either.left(FailedReason.VALIDATE_FAILED);

        // 更新ステップ
        final Either<String, Success> updateResult = update(validateResult.get());
        if (updateResult.isLeft())
            return Either.left(FailedReason.UPDATE_FAILED);

        // 送信ステップ
        final Either<String, Success> sendResult = send(validateResult.get());
        if (sendResult.isLeft())
            return Either.left(FailedReason.SEND_FAILED);

        return Either.right(new Success());
    }

    record InputValue(Integer value) {}
    record ValidatedValue(Integer value) {}

    record Success(){}
    enum FailedReason {
        VALIDATE_FAILED,
        UPDATE_FAILED,
        SEND_FAILED
    }

    Either<String ,ValidatedValue> validate(final InputValue inputValue) {
        // 省略
        return Either.right(new ValidatedValue(inputValue.value()));
    }

    Either<String, Success> update(final ValidatedValue validatedValue) {
        // 省略
        return Either.right(new Success());
    }

    Either<String, Success> send(final ValidatedValue validatedValue) {
        // 省略
        return Either.right(new Success());
    }
}
```

ROPの説明で課題に挙げた通り、`execute`は各ステップの失敗(Left)のハンドリングにコードの半数ほどを費やしていることがわかります。

### ROPを適用した実装
同様の題材でROPを実践してこのハンドリングをスマートに行います。

おまたせしました。やっと鉄道の話です。
題材にしたユースケースにおいて、各ステップは`Either`でない引数を取り、`Either`の戻り値(成功・失敗)を返していました。これを2つに分岐する鉄道のレールのように考えます。成功(Right)が緑で失敗(Left)は赤で表現されています。[^引用]
素朴な実装では、失敗を都度ハンドリングして早期リターンを記述していました。

[^引用]: 引用元の都合で、UpdateがUpdateDb、SendがSendEmailに変化していますが、そこは気にしないでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/83441b7d-b59e-7ed1-5148-620796fc2202.png)

「全て以下のルール従っているだけなので毎回ハンドリングしなくとも、スマートに記述できるのでは？」というのがROPのアイデアです。
- 成功した場合は次に進む
- 失敗した場合はそのまま戻り値を返す

視覚にはレールを連結するイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/bfa1f511-26d5-148a-91cd-dfc9f6ce3036.png)

この連結したレールそのものがユースケース[^ワークフロー]となり、戻り値を返す処理は最後の一箇所のみ。つまり素朴に実装した際のような煩雑な失敗のハンドリングは不要となります。

[^ワークフロー]: 関数型ドメインモデリングではワークフローと呼んでいます。

イメージは簡単ですが、これを実際にソースコードに反映させるにはいくつかの工夫が必要です。それらはこの後じっくり説明していきますので、まずは完成形のソースコードをご覧ください。

```java
import io.vavr.control.Either;

public class ROPUseCase {
    Either<FailedReason, Success> execute(final InputValue inputValue) {
        return validateWrapper(inputValue)
                .flatMap(this::updateWrapper)
                .flatMap(this::sendWrapper);
    }

    record InputValue(Integer value) {}
    record ValidatedValue(Integer value) {}

    record Success(){}
    enum FailedReason {
        VALIDATE_FAILED,
        UPDATE_FAILED,
        SEND_FAILED
    }

    Either<FailedReason, ValidatedValue> validateWrapper(final InputValue inputValue) {
        return validate(inputValue).fold(
            error -> Either.left(FailedReason.VALIDATE_FAILED),
            Either::right
        );
    }

    Either<FailedReason, ValidatedValue> updateWrapper(final ValidatedValue validatedValue) {
        return update(validatedValue).fold(
            error -> Either.left(FailedReason.UPDATE_FAILED),
                success -> Either.right(validatedValue)
        );
    }

    Either<FailedReason, Success> sendWrapper(final ValidatedValue validatedValue) {
        return send(validatedValue).fold(
            error -> Either.left(FailedReason.SEND_FAILED),
            Either::right
        );
    }

    Either<String ,ValidatedValue> validate(final InputValue inputValue) {
        // 省略
        return Either.right(new ValidatedValue(inputValue.value()));
    }

    Either<String, Success> update(final ValidatedValue validatedValue) {
        // 省略
        return Either.right(new Success());
    }

    Either<String, Success> send(final ValidatedValue validatedValue) {
        // 省略
        return Either.right(new Success());
    }
}
```

この実装では、`execute`は煩雑な失敗のハンドリングから解放されて圧倒的に簡潔になったことがわかります。**ValidateとUpdateとSendという2分岐のレールが鉄道のように直列で連結している。** という視覚的なイメージがそのままソースコードに反映できています。

### 実装の解説
ここからはROPを実践したコードについてポイントごとに解説していきます。それぞれ関連があり、単体の説明だけでは理解が難しいかもしれません。完全に理解できなくても最後まで読み進めていただき、必要に応じて再度読んでいただけると理解しやすいと思います。[^謝罪]

[^謝罪]:一重に筆者の構成能力不足です。すいません。

#### ①失敗レールの型を統一する
ROPを実践する上でのポイント一つ目は、失敗レール(赤い下側)の型を統一することです。
メソッドチェーンで連結したユースケースの中間の型は全て`EitherかつLeftは常に同じ型`にすることが重要です。

コード例では、失敗の型(Left)をユースケースの失敗理由をまとめていた`FailedReason`に統一しています。[^言い訳]

[^言い訳]: この記事では各ステップをユースケースクラスの内部に実装しています。ただ実際には、複数のユースケースから共通して利用される処理として独立した別のクラスに実装されている可能性も高いです。そのためステップのシグネチャを変更するのではなく、ラッパーしたメソッド作成しています。また同じクラスにある都合上Wrapperを末尾につけた少々ダサい命名となっていますが別のクラスに書かれていればこのような命名にする必要はないです。


```java
    /**
    * ユースケースの失敗理由
    */
    enum FailedReason {
        VALIDATE_FAILED,
        UPDATE_FAILED,
        SEND_FAILED
    }
    
    /**
    * このユースケース固有の処理
    */
    Either<FailedReason, ValidatedValue> validateWrapper(final InputValue inputValue) {
        return validate(inputValue).fold(
            error -> Either.left(FailedReason.VALIDATE_FAILED),
            Either::right
        );
    }

    /**
    * 複数のユースケースで共有する処理
    */
    Either<String ,ValidatedValue> validate(final InputValue inputValue) {
        // 省略
        return Either.right(new ValidatedValue(inputValue.value()));
    }
```

#### ②必要に応じてバケツリレーする
2つ目のポイントはバケツリレーです。
ROPで各ステップが引数として受け取ることができるのは、`前のステップの戻り値、または最初のステップ以前に存在した値`のみです。そのため、必要に応じて値のバケツリレーが必要です。

先の例において`send`では`ValidatValue`が必要ですが、ROPに変更したことで`send`が受け取ることのできる値は`InputValueとupdateの戻り値`のみです。

そこで`update`が`ValidatValue`をバケツリレーして戻り値として戻すようにラッパー関数を用意します。
```java
    /**
    * このユースケース固有の処理
    */
    Either<FailedReason, ValidatedValue> updateWrapper(final ValidatedValue validatedValue) {
        return update(validatedValue).fold(
            error -> Either.left(FailedReason.UPDATE_FAILED),
                success -> Either.right(validatedValue)
        );
    }

    /**
    * 複数のユースケースで共有する処理
    */
    Either<String, Success> update(final ValidatedValue validatedValue) {
        // 省略
        return Either.right(new Success());
    }
```

#### ③flatMapで連結する
①②のポイントを押さえた上で、`flatMap`で各ステップを連結します。

flatMapはMap処理を実行したのちその結果を平坦(flat)にします。
例えばListを平坦にするとは`List<List<Hoge>> -> List<Hoge>`ということです。
Eitherの場合には、`Either<Fuga, Either<Fuga, Hoge>> -> Either<Fuga, Hoge>`です。

```java
      validateWrapper(inputValue)
          .flatMap(this::updateWrapper)
```
`flatMap`で連結したこのソースコードの挙動について詳しく説明します。
`validateWrapper(inputValue)`の戻り値の型は`Either<FailedReason, ValidatedValue>`です。これに対して`flatMap`を呼び出すと、まずは以下のいずれかの処理がなされます(ここまでは`map`と同じ)。
- Left(`FailedReason`)の場合は何もしない
- Right(`ValidatedValue`)の場合は`upateWrapper`を呼び出す

`upateWrapper`は`Either<FailedReason, ValidatedValue>`を返します。そのため実行後の型は`Either<FailedReason, Either<FailedReason, ValidatedValue>>`になります。
次にこれを平坦にする処理がなされます。(`map`の場合はしない)
その結果`Either<FailedReason, ValidatedValue>`です。

失敗(Left)の型を統一した意味がわかったと思います。型が異なる場合には`flatMap`の呼び出しはできずコンパイルエラーが発生します。

図で表すとこのような感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/ce7c6f70-923d-441a-2694-8ff35aec54cb.png)

これらのポイントを理解した上で改めてソースコードを見ていただけるとROPの全体像が掴めるはずです。

# まとめ
この記事では、入力値を検証して更新して通知する。という典型的なユースケースを題材に、JavaでVavrを用いてROPを実践する方法について解説しました。

自分の所属する開発チームではROPを実際に取り入れています。
miroなどを用いて視覚的に設計し作図した後にコーディングを行うことが多いため、図と実装がわかりやすく対応する点も気に入っています。
この記事では紹介していませんが、業務レベルで実践するうえでは部分適用などのテクニックも必須になってくるのでこの記事に反響があれば紹介します。

最後にひとつ。このやり方を覚えるとガンガンROPを適用したくなるのですが（経験談）、どうやら提唱者本人が[Against Railway-Oriented Programming](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/)内でROPをどこでも使えばいいってものではないよ。と注意喚起しているようなので適材適所で使っていきましょう(自戒)。

# 追記
[JJUG CCC 2025 Spring](https://sessionize.com/api/v2/s2ztutnz/view/Sessions)にて、この記事に加筆した内容を発表しました。

https://speakerdeck.com/cocet33000/bringing-the-essence-of-railway-oriented-programming-to-java