gof-visitor について
gof-visitor は、安定したオブジェクト階層に操作を追加する GoF Visitor パターンを解説するスキルです。ダブルディスパッチの仕組みやトレードオフ、網羅的なパターンマッチングを選ぶ場面も扱います。
主な機能
- 01Visitor と要素の役割分担
- 02ダブルディスパッチの解説
- 03選択基準とトレードオフ
- 04アンチパターンと代替案
活用シーン
- 01AST へのシリアライザーやチェッカーの追加
- 02型階層に対する複数操作の実行
- 03Visitor と sealed パターンマッチングの比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yonatankarp/ software-design-skills --skill gof-visitorインストール後、Claude Code で/gof-visitorを入力して読み込まれたか確認します。
詳細
1 行の要約
安定したオブジェクト構造の要素に対して実行する操作を表現します。要素クラスを変更せずに、構造に新しい操作を追加できます。
このスキルを使う場面
- 型の階層(AST ノード、ファイルシステムのエントリー、ドキュメント要素)が安定しており、要素クラスをいちいち変更せずに、新しい操作を継続的に追加したい場合。
- 同じ階層に対して、整形プリンター、型チェッカー、JSON シリアライザー、メトリクスコレクターなど、異なる操作を実行する必要がある場合。それぞれが個別の Visitor です。
- 古典的な GoF の「ダブルディスパッチ」のユースケース、つまり、2 つの動的型(要素の型と操作の型)に基づいて呼び出すメソッドを選択する場合。
このスキルを使わない場面
- 階層自体が不安定な場合。新しい要素型を追加するたびに、すべての Visitor の更新が必要になります。Visitor は「操作の追加が簡単」である代わりに、「要素型の追加が大変」になります。
- 言語に網羅的なパターンマッチングがすでに備わっている場合(Kotlin の
sealed型に対するwhen、Rust のmatch、Scala のパターンマッチングなど)。こうした言語では、各要素をサブクラス化しなくても、網羅的なwhenを使った関数として操作を追加できます。 - 追加する操作が 1 つだけで、今後さらに追加する見込みもない場合。要素にメソッドを追加するほうがシンプルです。
主な内容
並行する 2 つの要素から構成されます。
要素階層。 各要素クラスには accept(visitor) メソッドがあり、自身を使って Visitor を呼び出します。accept(v) { v.visitX(this) } のように記述します。要素はこの操作に関して、それ以外のことは何もしません。
Visitor インターフェース。 具体的な要素型ごとに 1 つのメソッドを宣言します。たとえば、visitCircle(Circle)、visitSquare(Square) などです。各具象 Visitor(Pretty-Printer、Type-Checker、JSON-Serializer)が、すべてのメソッドを実装します。
新しい操作を追加するには、新しい Visitor クラスを追加します。既存の要素クラスは変更しません。
ダブルディスパッチの仕組み:クライアントコードが element.accept(visitor) を呼び出すと、まず要素の実行時型に基づいて実行する accept が選択され、その中で Visitor の実行時型に基づいて実行する visitX が選択されます。2 段階のポリモーフィズムが連携します。
Kotlin では、通常これは不要です。 sealed 階層と網羅的な when を使えば、accept の定型コードなしで「要素クラスを変更せずに新しい操作を追加する」という同じ利点が得られ、すべての要素型を処理するようコンパイラーが強制します。
sealed class Shape
data class Circle(...): Shape()
data class Square(...): Shape()
fun area(s: Shape): Double = when (s) {
is Circle -> ...
is Square -> ...
} // compile error if a new Shape variant isn't handled
トレードオフは逆転します。sealed when では、操作の追加は簡単(新しい関数を追加するだけ)で、要素の追加は大掛かりになります(既存のすべての when を更新する必要があります。ただし、コンパイラーが更新箇所をすべて示します)。
判断の目安
- 閉じた階層に対する網羅的なパターンマッチングがない言語では、Visitor を選びます。
- Kotlin(および類似の言語)では、
sealedと網羅的なwhenを優先します。同じ利点を、より少ない定型コードとコンパイル時の網羅性チェックで得られます。 - 要素型をまたいで操作を連携させる必要がある場合は、Visitor を使います(降下する間、状態を維持するツリーウォーカーなど)。Visitor は、場当たり的な関数よりも、訪問をまたぐ状態を適切に保持できます。
アンチパターン
- 不安定な階層で Visitor を使うこと。新しい要素型を追加するたびに N 個の Visitor の更新が必要になり、明示的な宣言の網羅性をチェックしない言語ではコンパイラーも助けてくれません。
- 「要素の追加を簡単にする」ために、デフォルトの何もしないメソッドを持つ Visitor を使うこと。処理対象として誰も想定していなかった型の振る舞いが、気付かれないまま抜け落ちます。
- 網羅的なパターンマッチングが使える言語で Visitor を使うこと。利点がないのに定型コードが増えます。
関連項目
gof-iterator— Visitor は構造を走査します。要素を返す Iterator と網羅的なwhenの組み合わせが、現代的な代替手段となることがよくあります。gof-composite— Visitor は Composite に適用されることがよくあります。acceptの再帰でツリーをたどります。gof-kotlin-idioms— sealed クラスとwhenは、Kotlin で Visitor の代わりに使う慣用的な方法です。
参考資料
- Design Patterns(GoF、1994年)、第5章 — Visitor。(HFDP第2版には含まれていません。)
- Kotlin Design Patterns and Best Practices(Alexey Soshin)、第4章 — Visitor。sealedクラスとの比較も含まれています。