「Visitorパターン無くても普通に構造と処理の分離ってできるのでは?」と思ったのでTwitterのみなさんに教えてもらいました

これは何?

GoFデザインパターンのVisitorパターンを勉強していたんですがよく分からなかったので、利点をTwitterのみなさんに聞いたところ解決したというお話です。

Visitorパターンを使わなくても「構造と処理の分離」ってできるのでは?

私はデザインパターンの勉強のために、結城浩さんの「Java言語で学ぶデザインパターン入門」を読んでいて、そのうちVisitorパターンのところを読んでいました。

そこでは、

Visitorパターンでは、データ構造と処理を分離します。そして、データ構造の中をめぐり歩く主体である「訪問者」を表すクラスを用意し、そのクラスに処理をまかせます。

とあり、Visitorパターンの主な目的はデータ構造と処理との分離であると言及されています。この点に関してはWikipedia等他のウェブサイトを見ても大体一緒だと思います:
Visitor パターン - Wikipedia

この章では、ファイルシステムを模したクラスたちFileとDirectoryを走査して列挙するという目的のプログラムが書かれており、若干簡略化すると次のようなプログラムが紹介されています
visitor_test/visitor at main · ashiato45/visitor_test · GitHub:

f:id:sle:20210318180136p:plain
Visitorパターンを使ったJavaプログラム

これで確かに構造(FileとDirectory)とそれらに対する処理(ConcreteVisitor)は分離されているのですが、他方OCamlのようなADTのある言語でどう書くかということを想像すると、パターンマッチと再帰関数で次のように書かれることが多い気がします:

これをJavaで模して書くとinstanceofを使うことになり、次のようになります
visitor_test/functional at main · ashiato45/visitor_test · GitHub:

f:id:sle:20210318180645p:plain
instanceofを使ったJavaプログラム

これでも構造(FileとDirectory)とそれらに対する処理(walk)が分離できているように見えます。こちらのほうが余計な(?)クラスが減って、より素直に目的が達せられているように見えるので、Visitorパターンはなぜ使われるのだろうというのがよく分からなくなりました。そこで、Twitterでみなさんに聞いてみることにしました:





いただいたコメントを参考にして書きなおしてみた

すると、以下のようなコメントをいただきました(他にもいただいていますが後で紹介します)


確かに1つのEntryのサブクラス(今の場合はFileとDirectory)に対する処理はテストの観点やメソッドのサイズの問題から1つのメソッドになっていた方がいいなと思い、しかしながら「別にVisitorを使ったダブルディスパッチの形にすることなくない?」と思って私は次のようなコードを書いてみました
visitor_test/walker at main · ashiato45/visitor_test · GitHub:

f:id:sle:20210318183136p:plain
Visitorパターンを頑なに使おうとしなかったJavaプログラム

これは、先程のwalkをWalkerというクラスに包み、そのなかでFileとDirectoryを引数としてオーバーロードでwalkを定義するものです。

なるほど、やっぱりVisitor(ダブルディスパッチというべきかも)は必要だわ

が、先程のプログラムはコンパイルが通りません。問題は以下の16行目で、

Directoryの中身はEntry型ということしか分からず、Entry型そのものに対するwalkは定義されていないのでコンパイルが通りません*1
ご指摘いただきいましたが、オーバーロードの呼出はコンパイル時に解決できる必要があります:

そういうわけで、walkをオーバーロードで定義する以上はどのwalkが呼び出されるかをちゃんと指定する必要があり、そのためにはvisitされる各クラスにちゃんとacceptを用意して「visitor.accept(this)」することになり、自ずとVisitorパターンが導かれるということが分かりました。

まとめ

「構造と処理の分離」というだけならVisitorパターンはいらなそうですが、その分離をした上でさらにメソッド分割やオーバーロードといったオブジェクト指向的に好ましいスタイルで記述するためにはVisitorパターンを使う必要があるようです。

コメントをいただいて教えてくださったみなさま本当にありがとうございました。

その他にいただいたコメント

他にも教えてくださった方が沢山いらっしゃったのでご紹介します:

*1:一応この継承関係だと、どのwalkが呼び出されるかに曖昧性はなく実行時型情報で解決されるものと思っていました…