Optionalは引数に使うべきでない、という幻想について

継続渡しすると戻り値は引数になるから「Optional は戻り値にのみ使うべき」というルールは無意味だよ、という話。 あ、そういう話ね、と分かった方はこれ以上読む必要はありません。

MonoAsync + Optional + 例外という欲張りパック状態なのも問題ですが、それについてはまた今度(Mono<Optional<T>> 使わずに Mono<T> を使え、という指摘があり得る。ただ、そっちもそっちで言いたいことはある、という程度)。 今回は、 MonoAsync くらいの意図として使っています*1

まず、こんなメソッドがあったとします。

Mono<Optional<String>> f();

これ自体は戻り値に Optional を使っているだけなので、「Optional は戻り値にのみ使うべき」は守っています。

しかし、これを使う側はそうはいきません。 例えば、値が取ってこれなかった場合はログして404を返し、取れてきた場合はログして200を返すような処理を考えます。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(strOpt -> {
        if (strOpt.isEmpty()) {
            logger.info("str is not found.");
            return ServerResponse.notFound();
        }
        var str = strOpt.get();
        logger.info("str is {}.", str);
        return ServerResponse.ok();
    });
}

ここで、map に渡すラムダ式の引数は Optional<String> になります。 おおっと、引数に Optional が出てきてしまいました。

このように、Mono など「後続処理をラムダ式として受け取る」ようなスタイルの設計においては、戻り値を引数として受け取ることになります。

これを「ラムダ式の引数は例外とする」というルールを加えてしまうと、メソッド参照によるラムダ式の置き換えが出来なくなってしまいます。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(this::h);
}

ServerResponse h(Optional<String> strOpt) {
    if (strOpt.isEmpty()) {
        logger.info("str is not found.");
        return ServerResponse.notFound();
    }
    var str = strOpt.get();
    logger.info("str is {}.", str);
    return ServerResponse.ok();
}

これは先ほどのラムダ式をメソッドとして抽出しただけで、やっていることは同じです。 ラムダ式の中身が複雑になる場合は自然にこういう書き換えはやりたくなるとおもいますが、ラムダ式を例外とするだけでは足りないことが分かります。 まぁ、 public 以外にはルールは適用しない、みたいな例外を追加するようなことは考えられますが、いっそあきらめて Optional を引数に使わない、なんてルールやめてしまった方がいいと思います。

いやいや、複雑になったとしても取得処理を呼び出し側でやって、メソッドの引数には Optional を許さない!という態度もなくはないです。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(strOpt -> {
        if (strOpt.isEmpty()) {
            logger.info("str is not found.");
            return ServerResponse.notFound();
        }
        var str = strOpt.get();
        logger.info("str is {}.", str);
        return h(str);
    });
}

ServerResponse h(String str) {
    // strを使った複雑な処理
    return ServerResponse.ok();
}

こんな感じですね。 値の有り無しの振り分けをラムダ式側、処理自体をメソッド側と役割を分けられるため、なくはないです。 ただ、今回の例は Optional が1つでしたが、これが2つ、3つと増えてくると、ラムダ式内が複雑になっていくため、そこにテストを書くためにメソッド化したくなります。 その際は、やはり Optional を引数に持つメソッドが欲しくなります。

このように、 「Optional を引数に使わない」というルールは実践的には無意味です。捨てましょう。

「フィールドに Optional を使わない」というルールの方はもうちょっとだけ分からなくもない理由付けがあります*2が、 こちらもすべてのクラスがシリアル化対象なわけでもないので、個人的には無意味だと思っています。

*1:CompletableFutureでもよかったけど長いしCFの語彙が気に入らないので・・・

*2:Optionalがシリアル化可能ではない