概要
Javaでデザインパターンの一つであるVisitor Patternを実装してみる。
Visiter Patternとは
Visitor Patternは、複数のクラスを横断するような処理を担うクラスを作成するときに使う。Visitorは、「訪問者」という意味である。対象となる各クラスを順次訪問しながら処理を行うところから、この名前が付いている。
例
三角形、四角形、円形といった図形クラスがあったとし、これらの図形を描画する処理を行う描画クラスを作成する。このクラスは、三角形、四角形、円形の各クラスを訪問し、それぞれの図形を表す情報、例えば、図形の位置、大きさ、円の場合は半径といった情報にアクセスし、該当の図形を描画する。
次に、各図形の面積を合計し、出力するクラスを作成する。このクラスは各図形クラスを訪問しながら、やはり図形の情報にアクセスし、その情報から各図形の面積を算出、合計して出力する。
これらの処理は、Visiter Patternを使わずに、単に呼び出しクラスを作成し、その図形の情報に順次アクセスする形で実装することもできる。しかし、Visiter Patternを使うと、各図形クラスに対して横断的に行う一連の処理をVisitorのインターフェースを実装した一つのクラスとしてまとめることができる。また、描画処理、面積算出処理を個々の図形クラスに持たせないことで、訪問対象となるクラスに変更を加えずに処理を追加できることも利点である。
まとめると、Visitor Patternは、複数のクラスに対して横断的に行う処理を、訪問者クラスにまとめるデザインパターンである。
良いところ
- 訪問者クラスの追加が容易である。
- 横断的に行う処理が一つのクラスにまとまっており、保守性が向上する。
困るところ
- 訪問対象のクラスが増えた場合、訪問者クラスは全て修正が必要になる。
サンプル実装
理解を深めるため、サンプルを考えて実装してみる。
設計
ロボットを動かすケースを考える。
ロボットには、頭、腕、足のパーツがある。それぞれのパーツが訪問対象クラスである。パーツに対して、訪問者クラスが行動を指示する。
パーツの実装
各パーツは訪問者クラスの訪問を受けるので、インターフェースを通して、acceptメソッドの実装を強制する。
package visitor.example.parts;
import visitor.example.actions.Action;
public interface BodyPart {
void accept(Action action);
}
頭、腕、足の各クラスは以下である。それぞれ、その部位でできるアクションがメソッドになっている。
package visitor.example.parts;
import visitor.example.actions.Action;
/** 頭 */
public class Head implements BodyPart {
public void openEyes() {
System.out.println("open eyes.");
}
public void closeEyes() {
System.out.println("close eyes.");
}
public void lookAhead() {
System.out.println("look ahead.");
}
public void sing() {
System.out.println("sing a song.");
}
@Override
public void accept(Action action) {
action.visitHead(this);
}
}
package visitor.example.parts;
import visitor.example.actions.Action;
/** 腕 */
public class Arm implements BodyPart {
public void open() {
System.out.println("open arms.");
}
public void swing() {
System.out.println("swing arms.");
}
public void rest() {
System.out.println("rest arms.");
}
@Override
public void accept(Action action) {
action.visitArm(this);
}
}
package visitor.example.parts;
import visitor.example.actions.Action;
/** 足 */
public class Leg implements BodyPart {
public void stand() {
System.out.println("stand up.");
}
public void walk() {
System.out.println("walk.");
}
public void rest() {
System.out.println("rest legs.");
}
@Override
public void accept(Action action) {
action.visitLeg(this);
}
}
次に各パーツへの行動指示のクラスを作成する。Visitor Patternでは、訪問者にあたるクラスである。それぞれの訪問者クラスでは、必ず各訪問先で行う処理が実装されていなければいけない。そのため、インターフェースで各訪問先での処理の実装を強制する。また、それぞれの処理を一連で実行するためのexecuteメソッドも必ず実装してもらうことにする。
package visitor.example.actions;
import java.util.List;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public interface Action {
void execute(List<BodyPart> bodyParts);
void visitHead(Head head);
void visitArm(Arm arm);
void visitLeg(Leg leg);
}
ではまず、歩く処理を指示するWalkクラスを作成する。
package visitor.example.actions;
import java.util.List;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class Walk implements Action {
public void execute(List<BodyPart> bodyParts) {
System.out.println("--- Walk ---");
for(BodyPart bodypart: bodyParts) {
bodypart.accept(this);
}
}
@Override
public void visitHead(Head head) {
head.lookAhead();
}
@Override
public void visitArm(Arm arm) {
arm.swing();
}
@Override
public void visitLeg(Leg leg) {
leg.walk();
}
}
Walkクラスの中で、Headクラスには前を向くこと、Armクラスには腕を振ること、Legクラスには歩くことを指示した。
同じように、SingクラスとSleepクラスを実装する。
package visitor.example.actions;
import java.util.List;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class Sing implements Action {
public void execute(List<BodyPart> bodyParts) {
System.out.println("--- Sing ---");
for(BodyPart bodypart: bodyParts) {
bodypart.accept(this);
}
}
@Override
public void visitHead(Head head) {
head.sing();
}
@Override
public void visitArm(Arm arm) {
arm.open();
}
@Override
public void visitLeg(Leg leg) {
leg.stand();
}
}
Singクラスでは、立って腕を開き、歌うことを指示した。
package visitor.example.actions;
import java.util.List;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class Sleep implements Action {
public void execute(List<BodyPart> bodyParts) {
System.out.println("--- Sleep ---");
for(BodyPart bodypart: bodyParts) {
bodypart.accept(this);
}
}
@Override
public void visitHead(Head head) {
head.closeEyes();
}
@Override
public void visitArm(Arm arm) {
arm.rest();
}
@Override
public void visitLeg(Leg leg) {
leg.rest();
}
}
Sleepクラスでは、目を閉じ、腕と足を休めるように指示した。
さて、こうして訪問対象の各パーツと訪問者クラスである行動指示の各クラスを作成した。
では、デモクラスを作って実行してみよう。
package visitor.example;
import java.util.ArrayList;
import java.util.List;
import visitor.example.actions.Sing;
import visitor.example.actions.Sleep;
import visitor.example.actions.Walk;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class Demo {
/** エントリーポイント */
public static void main(String[] args)
{
List<BodyPart> bodyparts = new ArrayList<>();
bodyparts.add(new Head());
bodyparts.add(new Arm());
bodyparts.add(new Leg());
new Walk().execute(bodyparts);
new Sing().execute(bodyparts);
new Sleep().execute(bodyparts);
}
}
実行結果は以下である。
--- Walk ---
look ahead.
swing arms.
walk.
--- Sing ---
sing a song.
open arms.
stand up.
--- Sleep ---
close eyes.
rest arms.
rest legs.
目論見通り、それぞれのアクションが実行されていることが確認できる。
訪問者クラスを追加してみる
せっかくなので、訪問者クラスをもう一つ足してみる。
package visitor.example.actions;
import java.util.List;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class SingingWalk implements Action {
@Override
public void visitHead(Head head) {
head.sing();
}
@Override
public void visitArm(Arm arm) {
arm.swing();
}
@Override
public void visitLeg(Leg leg) {
leg.walk();
}
@Override
public void execute(List<BodyPart> bodyParts) {
System.out.println("--- SingingWalk ---");
for(BodyPart bodypart: bodyParts) {
bodypart.accept(this);
}
}
}
新しい訪問者クラスとして、歩きながら歌を歌うことを指示する、SingingWalkクラスを作成した。
さっそく実行してみよう。
package visitor.example;
import java.util.ArrayList;
import java.util.List;
import visitor.example.actions.Sing;
import visitor.example.actions.SingingWalk;
import visitor.example.actions.Sleep;
import visitor.example.actions.Walk;
import visitor.example.parts.Arm;
import visitor.example.parts.BodyPart;
import visitor.example.parts.Head;
import visitor.example.parts.Leg;
public class Demo {
/** エントリーポイント */
public static void main(String[] args)
{
List<BodyPart> bodyparts = new ArrayList<>();
bodyparts.add(new Head());
bodyparts.add(new Arm());
bodyparts.add(new Leg());
new Walk().execute(bodyparts);
new Sing().execute(bodyparts);
new Sleep().execute(bodyparts);
new SingingWalk().execute(bodyparts); // 歌いつつ歩く指示を追加
}
}
実行結果は以下である。
--- Walk ---
look ahead.
swing arms.
walk.
--- Sing ---
sing a song.
open arms.
stand up.
--- Sleep ---
close eyes.
rest arms.
rest legs.
--- SingingWalk ---
sing a song.
swing arms.
walk.
訪問者クラスは新たに作成したが、各パーツは修正しないで、新たな処理を追加できた。
感想
確かにこういう形で実装することができるのは分かるが、同じことは訪問対象の単なる呼び出しでも実装できる。この、訪問者クラスというものをインターフェースを使って一つの型として作り、各訪問者クラスに実装を強制していく、というのが良し悪しなのではないか。
ただ、もし、訪問対象のクラスと訪問者クラスの実装者が別だったらどうだろうか。ライブラリとして訪問対象のクラスが公開されていて、ライブラリの利用者が訪問者クラスを自分で実装してそのライブラリを使用するということがあった場合、このデザインパターンが生きそうな気がする。
今回、訪問対象のクラスにデータを持たせなかったので、あまり「訪問者クラスにメソッドをまとめている」感じが出なかったが、要するに訪問対象クラスをいじらずに、訪問者側に処理をまとめるところがポイントだと思うので、一応、Visitor Patternかなと思った。これが最初に例を挙げた図形クラスを訪問して、データを集めながら処理を行う訪問者クラスだと、もっとVisitor Patternらしいコードになったかもしれない。
環境
Java21
サンプルプログラム格納先
参加しているアドベントカレンダー
これはmisskey.dev ユーザー Advent Calendar 2024 1日目の記事です。