はじめに
普段、技術系の記事はQiitaに書いているのですが、今回はあまりにもオピニオン寄りの内容なので、はてなブログに残しておくことにします。
この記事は、私自身が考えた内容や問題意識をもとに、文章の整理や執筆にあたってAIを活用しています。自分の考えを言語化するためのメモに近いものです。
自分がRustで気に入っているもののひとつに、unreachable! マクロがあります。
Rustのunreachable!マクロ
unreachable! は、プログラムの実行上、到達するはずがない箇所を明示するためのマクロです。
例えば、注文ステータスに応じて処理を分岐するコードを考えてみます。
enum OrderStatus { Pending, Shipped, Delivered, } fn process_order(status: OrderStatus) { match status { OrderStatus::Pending => println!("注文受付中"), OrderStatus::Shipped => println!("発送済み"), OrderStatus::Delivered => { unreachable!("処理対象は発送前の注文のみ"); } } }
この例では、発送前の注文だけを処理する想定なのに、Delivered(配達完了)が渡された場合を unreachable! で表現しています。
Rustでは、match ですべてのパターンを網羅する必要があります。このように、業務上は起こらない想定の状態についても、何らかの処理を記述しなければなりません。
もちろん、この例では unreachable! を使わずに別の処理を書くこともできます。しかし、unreachable! を使えば、「この分岐には到達しないはずだ」という意図を明示でき、万が一実行された場合にはパニックが発生します。
なお、コンパイラが到達不能性を証明してくれるわけではありません。あくまでも、プログラマが到達しないことを表明するためのものです。
このように、到達しないことを明示しつつ、万が一到達した場合にはエラーにできるところが気に入っています。
アプリケーション開発では「到達しない」がよくある
アプリケーション開発をしていると、「この部分には到達しないんだけどな」という場面がよくあります。
もちろん、業務をきちんと分析し、エンティティやドメインモデルを適切に設計すれば、そのようなコードを書く必要はなくなるのかもしれません。しかし、現実はそう簡単ではありません。特に、すでに出来上がっているアプリケーションを保守する場合など、理想どおりにはいかないことも多々あります。
例えば、注文ステータスが特定の値しか取らないことを前提にしていても、既存データや呼び出し元の都合など、さまざまな事情によって、その前提をコード上で完全に保証することが難しい場合もあります。
では、このような「到達しないはずの箇所」をJavaではどう表現すればよいのでしょうか。
Javaではどう表現するか
Javaには、Rustの unreachable! にそのまま相当するものはありません。調べてみたところ、assert false を使う方法や、AssertionError を直接スローする方法などがよく紹介されているようでした。
assertを使う
Javaには表明(assertion)という機能があり、プログラマが想定している条件を記述できます。
default: assert false : "ここには到達しない想定です";
ただし、Javaの assert は実行時に有効化していないと動作しません。無効になっている場合、条件式自体が評価されないため、上のコードを書いても何も起きません。
Javaエンジニア、とりわけ自分が身を置いているSIの世界では、assert 自体があまり知られていないように感じます。本番環境では無効にされていることもあるため、確実にエラーにしたい場合には少し心もとないです。
AssertionErrorをスローする
確実にエラーにしたいのであれば、AssertionError を直接スローする方法もあります。
default: throw new AssertionError("ここには到達しない想定です");
これなら、表明の有効・無効にかかわらずエラーになります。ただし、実際の業務システムでは、アプリケーション独自の例外処理やエラーコードの仕組みが用意されていることもあります。
独自の例外を使う
例えば、UnreachableException のような例外を定義する方法もあります。
default: throw new UnreachableException();
ただ、現場では例外の扱いに関する規約がすでに決まっていることも多いです。よくあるのが、アプリケーションで発生させる例外は、特定の RuntimeException を継承した例外に統一し、コンストラクタにエラーコードを渡すという規約です。
default: throw new MyApplicationRuntimeException(ErrorCode.E12345);
こうした規約がある場合、到達しないことを示すためだけに独自の例外を定義するよりも、既存の例外とエラーコードの仕組みに乗せるほうが現実的です。ログ出力やエラー処理なども既存の仕組みに任せられます。
Java標準の例外を使うべきか
一方で、「例外はなるべくJava標準のものを使うこと」という規約になっている場合もあります。こうなると、少し困ります。
個人的な印象ですが、ドメイン駆動設計(DDD)の文脈では、独自の例外を増やすよりも、Java標準の例外をなるべく使おうという考え方を見かける気がします。ドメインモデルを適切に設計し、不正な状態を作れないようにしておけば、そもそも「ここには到達しない」というコードを書く必要自体がなくなる。そういう思想なのかもしれません。
それはそれで、もっともな話だと思います。ただ、現実には、既存の設計を大きく変えられないこともありますし、業務上の制約をすべてコードで表現しきれないこともあります。理想的な設計だけですべての問題を解決できるわけではありません。
Rustの unreachable! のように、到達しないことを簡潔に表現できる仕組みは便利です。一方、Javaでは assert、AssertionError、独自の例外など、いくつかの選択肢があり、既存の設計や例外処理の規約に合わせて選ぶ必要があります。
DDD的には、そもそも到達しない箇所を作らないように設計するのが理想なのかもしれませんが、保守開発ではそう簡単にはいかないことも多々あります。まあ、現実は厳しいということで。
