mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Visitorパターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Visitorパターンについて

Visitorパターンとは

  • Visitorパターンは、データ構造と処理を分離するパターンです
  • データ構造の中にアクセスする"訪問者クラス"を用意し、訪問者クラスに処理を任せます
  • 新しい処理を追加したいときは新しい訪問者を作り、データ構造の方は、訪問者をアクセスさせたら良いことになります

サンプルプログラム

  • Visitorパターンを使って、ディレクトリ、ファイルの一覧を表示する例を取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
Visitor 訪問者を表す抽象クラス
Element Visitorクラスのインスタンス(訪問者)を受け入れるデータ構造を表すインターフェース
ListVisitor (ConcreteVisitor) Visitorクラスのサブクラスで、ファイル、ディレクトリの一覧を表示するクラス
Entry FileとDirectoryのスーパークラスとなる抽象クラス
File (ConcreteElement) ファイルを表すクラス
Directory ( ConcreteElement / ObjectStructure ) ディレクトリを表すクラス

f:id:mohuNeko:20201229233214p:plain

Visitorクラス

  • 訪問者を表す抽象クラスです
  • 訪問者は訪問先のデータ構造である、FileとDirectoryに依存しています
  • 2つのvisitメソッドは、引数の型によって、メソッドの識別を行います(オーバーロード
public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

Elementインターフェース

  • 訪問者を受け入れるインターフェースです
public interface Element {
    public abstract void accept(Visitor v);
}

Entryクラス

  • Elementインターフェースを実装します
  • acceptメソッドを実際に実装するのはEntryのサブクラス(File、Directory)クラスです
import java.util.Iterator;

public abstract class Entry implements Element {
    public abstract String getName();       // 名前を得る
    public abstract int getSize();      // サイズを得る
    public Entry add(Entry entry) throws FileTreatmentException {   // エントリを追加する
        throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {    // Iteratorの生成
        throw new FileTreatmentException();
    }
    public String toString() {       // 文字列表現
        return getName() + " (" + getSize() + ")";
    }
}

Fileクラス

  • acceptメソッドを実装します
  • v.visit(this);で Visitorのvisitメソッドを呼び出します
public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

Directoryクラス

  • v.visit(this);で Visitorのvisitメソッドを呼び出します
import java.util.Iterator;
import java.util.ArrayList;

public class Directory extends Entry {
    private String name;                    // ディレクトリの名前
    private ArrayList dir = new ArrayList();      // ディレクトリエントリの集合
    public Directory(String name) {         // コンストラクタ
        this.name = name;
    }
    public String getName() {         
        return name;
    }
    public int getSize() {         
        int size = 0;
        Iterator it = dir.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {         // エントリの追加
        dir.add(entry);
        return this;
    }
    public Iterator iterator() {      // Iteratorの生成
        return dir.iterator();
    }
    public void accept(Visitor v) {         // 訪問者の受け入れ
        v.visit(this);
    }
}

ListVisitorクラス

  • Visitorのサブクラスで、データ構造の中を歩き、一覧を表示するクラスです
  • acceptメソッドはvisitメソッドを呼び、visitメソッドはacceptメソッドを呼びます
  • visitメソッドとacceptメソッドが互いに相手を呼び出しているということです
import java.util.Iterator;

public class ListVisitor extends Visitor {
    private String currentdir = "";        // 現在のディレクトリ名

    public void visit(File file) {       // Fileクラスのインスタンスに行う処理
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {   // Directoryクラスのインスタンスに行う処理
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
        currentdir = savedir;
    }
}

FileTreatmentException クラス

public class FileTreatmentException extends RuntimeException {
    public FileTreatmentException() {
    }
    public FileTreatmentException(String msg) {
        super(msg);
    }
}

Mainクラスで動作確認

  • Directoryの中身を表示するのに、"表示を行う訪問者"である、ListVisitorクラスのインスタンスを使います
  • ディレクトリの表示も、データ構造内の各要素に対して行う処理であるので、Visitor側で実装しています
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());              

            System.out.println("");
            System.out.println("Making user entries...");
            Directory kiki = new Directory("kiki");
            Directory nausicaa = new Directory("nausicaa");
            Directory chihiro = new Directory("chihiro");
            usrdir.add(kiki);
            usrdir.add(nausicaa);
            usrdir.add(chihiro);
            kiki.add(new File("diary.html", 100));
            kiki.add(new File("Composite.java", 200));
            nausicaa.add(new File("memo.tex", 300));
            chihiro.add(new File("game.doc", 400));
            chihiro.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());              
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}

Visitor/Elementの処理のまとめ

  • Directory・Fileクラスのインスタンスに対しては、acceptメソッドが呼ばれます
  • acceptメソッドはインスタンスごとに一回しか呼ばれません
  • ListVisitorメソッドのインスタンスに対して、visit(Directory)・visit(File)が呼ばれます
  • visit(Directory)やvisit(File)を処理しているのは、1つのListVisitorインスタンスです
    • ListVisitorにvisitの処理が集中しています
  • Visitor パターンでは、受入者 ( ConcreteAcceptor / ConcreteElement ) のacceptメソッドを呼び出すと、 訪問者 (ConcreteVisitor) のvisitメソッド が呼び出され、実行する処理が決定します。このような "2重呼び出し"を ダブルディスパッチ (2 重振り分け) と呼びます

Visitorパターンのメリット

  • Visitorパターンの目的は、データ構造と処理を分離することです
  • データ構造は、要素を集合としてまとめたり、要素間を繋いだりしてくれます
    • そのデータ構造を保持しておくことと、その構造を基礎とした処理を書くことは別のものです。
  • 訪問者役 (ListVisitor) は、受け入れ役 (File、Directory) とは独立して開発することができます
    • つまり、Visitorパターンは、受け入れ役のクラスの部品としての独立性を高めていることになります
    • 処理の内容をFileクラスやDirectoryクラスのメソッドとして実装してしまうと、新しい処理を追加して機能拡張するたびにFileクラスやDirectoryクラスを修正する必要があります

今日のポイント

  • Visitorパターンは、データ構造と処理を分離するパターンです
    • データ構造の中にアクセスする訪問者クラスを用意し、訪問者クラスに処理を任せます
  • Visitorパターンの目的は、データ構造と処理を分離することです
  • 訪問者役 (ListVisitor) は、受け入れ役 (File、Directory) とは独立して開発することができ、受け入れ役のクラスの部品としての独立性を高めていることになります
    • 部品としての独立性を高いということは、The Open-Closed Principle の原則に則っています
      • "ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張のために開いていて、修正のために閉じていなければならない"
      • つまり、変更が発生した場合は、"既存のコードには修正を加えずに、新しくコードを追加するだけで対応しましょう"

 本日もお疲れ様です😊