デザインパターン入門 | Visitor(ビジター)パターン
「訪問者(visitor)」と「訪問先(accepter)」の間で処理のやりとりが行われるデザインパターンが「Visitor(ビジター)パターン」です。
今回お話する内容には「コンポジット(Composite)」パターンのクラスが含まれていますので、まだ、「Composite(コンポジット)」パターンを学んでいない方は先に学習をしておくことをお勧めします。
「Composite(コンポジット)」パターンはそのまま利用しますが、今回の主役は、「訪問者(visitor)」と「訪問先(accepter)」ですので、これらの役割について注目しながら「Visitor(ビジター)パターン」について見ていきましょう。
「Visitor(ビジター)」と「Accepter(アクセプター)」
「Visitor(ビジター)」は、「訪問者」で、「Accepter(アクセプター)」は「訪問先」を表します。
「Visitor(ビジター)」は抽象クラス、「Accepter(アクセプター)」はインターフェースとして作成していきます。
「Visitor(ビジター)」抽象クラスは下記のようになります。(プログラム言語:Java)
public abstract class Visitor { public abstract void visit(Department department) ; public abstract void visit(Company company) ; }
「Accepter(アクセプター)」インターフェースは下記のようになります。
public interface Accepter { public abstract void accept(Visitor visitor); }
【参考事例】「会計士」と「会社」
今回は「会計士(Account)」と「会社(Company)」を例にして「Visitor(ビジター)」パターンについて見ていきたいと思います。
クラスとインターフェースの関係は下図のようになります。
「AccountVisitor」クラスは、「会計士」を表すクラスですが、このメソッドは「Visitor」抽象クラスを継承しています。
「AccountVisitor」クラスの実装内容は下記のようになります。
public class AccountantVisitor extends Visitor{ @Override public void visit(Department department) { System.out.println( department ); } @Override public void visit(Company company) { System.out.println( company ); } }
このクラスでは、「visit」メソッドをオーバーライドしていますが、内容は「Company」クラス、「Department」クラスの内容を出力するだけとなっています。
次に「Entry」クラスは、「Acceptor」インターフェースを実装しています。
public abstract class Entry implements Accepter { public abstract String getName(); public abstract int getEarnings(); }
このクラスでは、
- getName
- 会社または部署名を取得
- getEarnings
- 会社または部署の「総売上金額」を取得
の2つの抽象メソッドが定義されています。
そして、この「Entry」クラスを継承しているのが、「Company」クラスと「Department」クラスです。
「会社」を表す「Company」クラスは下記のようになります。
public class Company extends Entry{ // 会社名 private String name; // 子会社または部署を保存 private ArrayList components = new ArrayList(); // コンストラクタ public Company(String name) { super(); this.name = name; } // 子会社または部署を追加 public Entry add(Entry component) { components.add(component); return this; } // 会社名を取得 @Override public String getName() { return this.name; } // 総売上高を取得 @Override public int getEarnings() { int earnings = 0; Iterator it = components.iterator(); while ( it.hasNext() ) { Entry component = (Entry)it.next(); earnings += component.getEarnings(); } return earnings; } @Override public void accept(Visitor visitor) { visitor.visit(this); } @Override public String toString() { return "会社名:" + this.getName() + " 売上高:" + this.getEarnings(); } }
ここで注目したいのが「accept」メソッドです。
「Visitor型のインスタンス」を受け取り、そのインスタンスの「visit」メソッドを呼び出しています。
その際に「自分自身(acceptor)」を引数に渡しています。
この部分が「Visitor(ビジター)」パターンの中で重要な仕組みとなっています。
次に「部署」を表す「Department」クラスは下記のようになります。
public class Department extends Entry{ // 部署名 private String name; // 売上高 private int earnings; // コンストラクタ public Department(String name, int earnings) { super(); this.name = name; this.earnings = earnings; } // 部署名を取得 @Override public String getName() { return this.name; } // 部署の売上高を取得 @Override public int getEarnings() { return this.earnings; } @Override public void accept(Visitor visitor) { visitor.visit(this); } @Override public String toString() { return "部署名" + this.getName() + " 売上高:" + this.getEarnings(); } }
「accept」メソッドは「Department」クラスと同様となっていますね。
これで、必要な「クラス・インターフェース」は全て揃いました。
これらのクラスを利用する「main」メソッドは、下記のようになります。
public class Main { public static void main(String[] args) { // 親会社の作成 Company rootCompany = new Company("親会社"); // 子会社の作成 Company subsidiaryA = new Company("子会社A"); Company subsidiaryB = new Company("子会社B"); //孫会社の作成 Company sub_subsidiaryA = new Company("孫会社A"); Company sub_subsidiaryB = new Company("孫会社B"); Company sub_subsidiaryC = new Company("孫会社C"); Company sub_subsidiaryD = new Company("孫会社D"); // 親会社に子会社を登録 rootCompany.add(subsidiaryA); rootCompany.add(subsidiaryB); // 子会社Aに孫会社を登録 subsidiaryA.add(sub_subsidiaryA); subsidiaryA.add(sub_subsidiaryB); // 子会社Bに孫会社を登録 subsidiaryB.add(sub_subsidiaryC); subsidiaryB.add(sub_subsidiaryD); // 子会社Aの部署を作成 subsidiaryA.add(new Department("住宅販売事業部",3000)); // 孫会社Aの部署を作成 sub_subsidiaryA.add(new Department("住宅資材生産事業部",900)); // 孫会社Bの部署を作成 sub_subsidiaryB.add(new Department("不動産管理事業部",200)); // 子会社Bの部署を作成 subsidiaryB.add(new Department("家電販売事業部",2000)); // 孫会社Cの部署を作成 sub_subsidiaryC.add(new Department("家電生産事業部",500)); // 孫会社Dの部署を作成 sub_subsidiaryD.add(new Department("リペア事業部",200)); // 会計士オブジェクトの作成 AccountantVisitor accountantVisitor = new (); //グループ会社の総売上高を取得 rootCompany.accept(accountantVisitor); //子会社Aの総売上高を取得 subsidiaryA.accept(accountantVisitor); //子会社Bの総売上高を取得 subsidiaryB.accept(accountantVisitor); } }
「AccountantVisitor」クラスのインスタンスを作り、「Company」クラスと「Department」クラスの「accept」メソッドの引数に「AccountantVisitor」クラスのインスタンスを渡していますね。
「Company」クラスと「Department」クラスの「accept」メソッドが呼び出されると「AccountantVisitor」クラスの「visit」メソッドが実行されます。
「visit」メソッドでは、「Company」クラスと「Department」クラスの「toString」メソッドをオーバーライドして、「会社名 or 部署名」と「総売上高」を取得し表示をしています。
このように、「Acceptor」の「accept」メソッドが「Visitor」抽象クラスを継承したクラスのインスタンスを受け入れ、「accept」メソッドから受け入れたインスタンスの「visit」メソッドを呼び出すという仕組みが「Visitor」パターンで一番注目すべき部分です。
そして、「main」メソッドの実行結果は下記のようになります。
会社名:親会社 売上高:6800 会社名:子会社A 売上高:4100 会社名:子会社B 売上高:2700
なぜこのようなパターンがあるのかというと、「データ」と「処理」を分離し、「機能の追加が行いやすくなる」というメリットがあるからです。
例えば、「部署」の中の「プロジェクトチーム」ごとに総売上高を求めるといった機能追加があると、クラス間が「密」に関連を持っている場合に変更が大変になってしまいます。
そこで、「データ」部分と「処理」部分を分離し、それぞれのクラスの関連を「疎」に保つことで、「機能追加が容易な状態を保つ」ことができるようになります。
身近な事例を参考にして「どのようなパターンでVisitorが活用できるのか?」をぜひ考えてみてください。