命令型エラーハンドリングの乱雑な現実
Javaはバージョン8でラムダ式とストリームが導入されて以来、大きく進化しました。しかし、多くのエンタープライズ向けコードベースは依然として命令型の習慣に囚われています。Spring Bootでサービスを構築しているなら、if (obj != null)チェックや3段階にネストされたtry-catchブロックといったパターンを目にしたことがあるでしょう。これでは、実際のビジネスロジックを見つけ出すのがほぼ不可能になってしまいます。
チェック例外(検査例外)もまた、フラストレーションの要因です。エラーを即座に処理するか、スタックの上位に投げてメソッドシグネチャを汚染するかの二択を迫られます。その結果、コアロジックが5層ものインデントの下に埋もれてしまう「ピラミッド・オブ・ドゥーム(死のピラミッド)」に陥ることがよくあります。「とりあえず動く」コードから、真に「堅牢な」コードへと移行することは、開発者にとって大きなキャリアの節目となります。
Vavr:欠けていた関数型ツール
Vavr(旧Javaslang)は、Javaの命令型のルーツと関数型プログラミングの間のギャップを埋めるライブラリです。標準ライブラリを置き換えるのではなく、Javaが完全には解決できなかった部分に対して、より強力な選択肢を提供します。Java 8がOptionalを提供したのに対し、VavrはOption、Try、Either、滅多にない挙動を示す永続データ構造を提供します。
使用を開始するには、pom.xmlに以下の依存関係を追加します。
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
ロジックをクリーンにする3つのツール
日常的なコーディングで最も効果的なVavr의 3つのコンポーネントを見ていきましょう。
1. Optionによる優れたNullハンドリング
標準のjava.util.Optionalは一歩前進でしたが、制限があります。例えばシリアライズ可能ではないため、RedisキャッシュやRMIレイヤーで問題が発生することがあります。VavrのOptionはより強力なコンテナで、Some(値が存在する)またはNone(空)のいずれかになります。
import io.vavr.control.Option;
public class UserService {
public Option<String> getUsername(Long id) {
String name = repository.findNameById(id); // nullの可能性がある
return Option.of(name);
}
}
// 使い方
getUsername(1L)
.map(String::toUpperCase)
.getOrElse("ゲスト");
Optionを使用すると、コンパイル時に値の欠落を処理するように強制されます。この単純な変化により、コードを実行する前にNullPointerExceptionを排除できます。
2. Tryによる例外管理
Tryコンテナは、チェック例外を扱う際の救世主です。スロー(throw)によってロジックが急停止する代わりに、Tryはその結果をキャプチャします。結果はSuccess(成功)またはFailure(失敗)のいずれかになり、処理を継続させることができます。
標準的なJSONパース処理を記述すると以下のようになります。:
import io.vavr.control.Try;
import com.fasterxml.jackson.databind.ObjectMapper;
public Try<User> parseUser(String json) {
ObjectMapper mapper = new ObjectMapper();
return Try.of(() -> mapper.readValue(json, User.class));
}
// 結果の処理
parseUser(jsonString)
.onSuccess(user -> log.info("ユーザーをパースしました: " + user.getName()))
.onFailure(ex -> log.error("パースに失敗しました: " + ex.getMessage()))
.getOrElse(new User("デフォルト"));
ロジックが上から下へと流れるクリーンな物語のように読めるようになります。メソッドの視覚的な流れを妨げるtry-catchブロックはもう必要ありません。
3. Eitherによるビジネスルールの表現
IOExceptionのような技術的な失敗はTryに適しています。しかし、ビジネスルールはどうでしょうか?ユーザーが残高200ドルの口座から500ドルを引き出そうとした場合、それは「例外」ではなく、妥当で想定内のビジネス結果です。Eitherはこれに最適です。慣習として、「Left」側にはエラーを、「Right」側には成功を保持します。
import io.vavr.control.Either;
public Either<String, Double> withdraw(Double amount) {
if (amount > balance) {
return Either.left("残高不足");
}
return Either.right(balance - amount);
}
実践例:安全なユーザー登録
これらのツールを組み合わせて、登録サービスを構築してみましょう。このサービスは、入力のバリデーション、データベースへの保存、そしてメールの送信を行う必要があります。
import io.vavr.control.Either;
import io.vavr.control.Try;
public class RegistrationService {
public void processRegistration(String username, String email) {
validateInput(username, email)
.flatMap(this::saveToDatabase)
.map(this::sendEmail)
.peek(user -> System.out.println("登録が完了しました: " + user.getName()))
.getOrElseGet(error -> {
System.err.println("登録に失敗しました: " + error);
return null;
});
}
private Either<String, User> validateInput(String name, String email) {
if (name == null || name.isBlank()) return Either.left("無効な名前");
if (!email.contains("@")) return Either.left("無効なメールアドレス");
return Either.right(new User(name, email));
}
private Either<String, User> saveToDatabase(User user) {
return Try.of(() -> repository.save(user))
.toEither()
.mapLeft(throwable -> "データベースエラー: " + throwable.getMessage());
}
private User sendEmail(User user) {
emailService.sendWelcome(user.getEmail());
return user;
}
}
processRegistrationメソッドの流れに注目してください。バリデーションに失敗すると、flatMapとmapの操作は自動的にスキップされます。エラーはそのままgetOrElseGetハンドラーに渡されます。このパターンは「鉄道指向プログラミング(Railway Oriented Programming)」と呼ばれ、「ハッピーパス(正常系)」とエラー処理を分離して保つことができます。
次のステップ
Vavrへの切り替えには、視点の転換が必要です。失敗を予期せぬ中断として捉えるのをやめ、データ変換の一部として扱うようになります。これにより、システムの予測可能性が大幅に向上します。
まずは小さく始めましょう。今日すぐにプロジェクト全体をリファクタリングする必要はありません。次にチェック例外を投げるライブラリを使用する際に、Tryを使ってみてください。あるいは、DTOでOptionを使用して、フィールドが空になる可能性があることを明示してみてください。たったこれだけの工夫で、午前3時の本番環境でのクラッシュがなくなったとき、未来の自分に感謝することでしょう。

