【ITニュース解説】Kill the Visitor Pattern: Refactoring Domain ASTs with Java 21 Sealed Hierarchies
2026年10月10日に「Dev.to」が公開したITニュース「Kill the Visitor Pattern: Refactoring Domain ASTs with Java 21 Sealed Hierarchies」について初心者にもわかりやすく解説しています。
ITニュース概要
Java 21の新機能「sealed hierarchies」と「record patterns」が登場し、オブジェクト構造に操作を追加する従来のVisitorパターンは時代遅れになった。これらの新機能により、複雑なコードが大幅に簡潔になり、コンパイル時に記述漏れを検出できるため、より安全で効率的なプログラミングが可能になる。
ITニュース解説
システムエンジニアを目指す皆さん、こんにちは。現代のソフトウェア開発において、コードの保守性や安全性を高めることは非常に重要な課題です。この記事では、これまで広く使われてきた「Visitorパターン」という設計手法が、Java 21で導入された新しい機能によって、どのように進化し、より良い方法に置き換えられるかについて解説する。
まず、従来のVisitorパターンとはどのようなもので、なぜその置き換えが推奨されるのかを見ていこう。Visitorパターンは、ツリー構造のような複雑なデータ構造に対して、新しい操作(例えば、計算や表示など)を追加したいときに使われる設計パターンである。その主な目的は、データ構造を定義しているクラス自体を変更することなく、新しい操作を追加できるようにすることであった。例えば、数式を評価する機能と、数式を文字列に変換する機能の二つを追加したい場合、Visitorパターンを使えば、数式を構成するクラス(定数、足し算、引き算など)に手を加えることなく、評価用Visitorと文字列変換用Visitorをそれぞれ作成することで対応できた。
しかし、このVisitorパターンにはいくつかの課題があった。一つは、各データ構造のクラスにaccept(NodeVisitor v)のような共通のメソッドを実装させ、Visitorオブジェクトの特定のvisitメソッドを呼び出すという、少し複雑な「二重ディスパッチ」の仕組みが必要になることである。これにより、多くのクラスに「ボイラープレートコード」と呼ばれる定型的な記述が増え、コードが読みにくく、拡張しにくい原因となっていた。また、新しい種類のデータ構造(例えば、新しい演算子)を追加するたびに、既存のVisitorクラスすべてに新しいvisitメソッドを追加する必要があり、もし追加を忘れてもコンパイラがエラーを教えてくれないため、実行時に予期せぬエラーが発生するリスクがあった。これは、網羅性(すべてのケースを考慮しているか)の保証が難しいという問題である。さらに、複雑なデータ構造の中身を取り出して処理する際にも、一度変数に格納してから使うといった中間的なステップが必要になりがちで、コードが冗長になる傾向があった。
Java 21では、これらの問題を解決するための強力な新機能が導入された。それが「Sealed Interfaces(シールされたインターフェース)」、「Record Patterns(レコードパターン)」、そして「Switch Expressions(switch式)」の組み合わせである。
Sealed Interfacesは、インターフェースを実装できるクラスやインターフェースを、あらかじめ明確に制限するための機能である。例えば、数式を表すExprというインターフェースをsealedキーワードで宣言し、「このExprインターフェースを実装できるのはConst(定数)、Add(足し算)、Neg(否定)の3つのクラスだけですよ」とコンパイラに宣言できる。これにより、この型階層が「閉じた契約」となり、コンパイラはこのインターフェースを実装する型が何であるかを完全に把握できるようになる。
このSealed Interfacesと組み合わせると非常に強力なのが、Switch Expressionsである。これは、従来のswitch文が、値を返す「式」として使えるようになった機能である。switch式をSealed Interfacesのオブジェクトに対して使うと、コンパイラは、そのインターフェースを実装するすべての型がswitchのcaseで網羅されているかをチェックしてくれる。もし、新しい数式の型を追加したのに、その型に対応するcase文をswitch式に追加し忘れると、コンパイル時にエラーが発生する。これにより、従来のVisitorパターンでは難しかった「網羅性の保証」がコンパイラによって強制され、実行時エラーのリスクが大幅に削減される。
さらに、Record Patternsは、レコード型のオブジェクトから、その内部の値を直接取り出すためのパターンマッチング機能である。レコード型は、主にデータを保持するために設計された、シンプルで不変なクラスを簡単に定義できるJavaの機能である。例えば、Add(Expr left, Expr right)というレコード型があった場合、case Add(var l, var r)のように書くだけで、Addオブジェクトのleftとrightフィールドの値をlとrという変数に直接抽出できる。この機能は、複雑な入れ子になったデータ構造、例えばAdd(Const(var l), Const(var r))のように、「足し算の左辺と右辺が両方とも定数である場合」といった状況でも、わずか一行で内部の値を分解して取り出すことを可能にする。これにより、中間変数を使った冗長なコードを排除し、非常に簡潔で読みやすいコードを書けるようになる。
これらの新しい機能を組み合わせることで、従来のVisitorパターンが持っていた「二重ディスパッチのボイラープレートコード」「操作ロジックの分散」「網羅性保証の欠如」といった問題は、劇的に解決される。具体的には、accept()やvisit()のようなメソッド、そしてVisitorクラス自体が不要になる。データ構造と、その構造に対する操作ロジック(switch式による評価)を明確に分離でき、しかもコンパイル時に安全性が保証されるようになる。
記事のサンプルコードを見てみよう。
sealed interface Expr permits Const, Add, Neg {}
ここでExprインターフェースがsealedとして宣言され、実装がConst、Add、Negに制限されている。
record Const(double val) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Neg(Expr expr) implements Expr {}
これらは数式を構成する要素をレコード型で定義している。
static double eval(Expr expr) { ... }
このevalメソッドが、Expr型を評価する核心部分である。
return switch (expr) { ... };
ここでswitch式が使われ、Exprの各実装型に対応する処理が記述されている。
case Const(var val) -> val;
Const型の場合、レコードパターンでvalフィールドの値を直接取り出して返す。
case Add(Const(var l), Const(var r)) -> l + r;
Add型で、かつその左右のオペランドが両方ともConst型である場合、という複雑な条件もネストしたレコードパターンで簡潔に記述されている。lとrはそれぞれのConstの中のval値である。
case Add(var l, var r) -> eval(l) + eval(r);
左右のオペランドがConstでないAdd型の場合、再帰的にevalを呼び出して評価する。
case Neg(var e) -> -eval(e);
Neg型の場合も同様に再帰的に評価する。
このコードは、まさにデータと操作が分離され、冗長な記述が一切なく、そしてコンパイル時に網羅性が保証されるという、新しいアプローチの利点を明確に示している。
まとめとして、Java 21で導入されたSealed Interfaces、Record Patterns、そしてSwitch Expressionsの組み合わせは、これまでVisitorパターンが担ってきた役割を、より安全に、より簡潔に、そしてより保守性の高い形で置き換えることができる革新的な機能である。これにより、オブジェクト構造のデータと操作の間の密結合を解消し、コンパイラによる厳密なチェックで実行時エラーを減らし、複雑なデータ変換ロジックを宣言的に記述できるようになる。システムエンジニアを目指す皆さんにとって、これらの新しい機能は、現代のソフトウェア開発における技術的負債を削減し、高品質なコードを書くための強力なツールとなるだろう。積極的に学び、活用してみてほしい。