daisukeの技術ブログ

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Javaでデザインパターンを学ぶ:Visitorパターン

Javaによるデザインパターンの7回目です!

今回も教材は、参考文献の「Java言語で学ぶデザインパターン入門第3版」のサンプルコードを使わせて頂きます。

今回は、Visitorパターンです。今回もイメージしやすいパターンですね。

では、やっていきましょう!

参考文献

参考文献のサンプルプログラムのダウンロード

www.hyuki.com

はじめに

「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。

Javaでデザインパターンの記事一覧

先に、23種類のデザインパターンを示します。

参考サイト:デザインパターン (ソフトウェア) - Wikipedia

デザインパターン一覧
◆生成に関するパターン
 ・Abstract Factory:関連するインスタンスを状況に応じて、適切に生成する方法を提供する
 ・Builder:複合化されたインスタンスの生成過程を隠蔽する。
 ・Factory Method:生成されるインスタンスに依存しない、インスタンスの生成方法を提供する
 ・Prototype:同様のインスタンスを生成するために、原型のインスタンスを複製する
 ・Singleton:あるクラスについて、インスタンスが1つしか存在しないことを保証する
◆構造に関するパターン
 ・Adapter:元々関連性のない2つのクラスを接続するクラスを作る
 ・Bridge:クラスと呼び出し側の間の橋渡しをするクラスで、実装を隠蔽する
 ・Composite:再帰的な構造を表現する
 ・Decorator:あるインスタンスに対し、動的に付加機能を追加する
 ・Facade:複数のサブシステムの窓口となる共通のインタフェースを提供する
 ・Flyweight:多数のインスタンスを共有し、インスタンスの構築のための負荷を減らす
 ・Proxy:共通のインタフェースを持つインスタンスを内包し、利用者からのアクセスを代理する。Wrapperとも呼ばれる
◆振る舞いに関するパターン
 ・Chain of Responsibility:イベントの送受信を行う複数のオブジェクトを鎖状につなぎ、それらの間をイベントが渡されていくようにする
 ・Command:複数の異なる操作について、それぞれに対応するオブジェクトを用意し、オブジェクトを切り替えることで、操作の切り替えを実現する
 ・Interpreter:構文解析のために、文法規則を反映するクラス構造を作る
 ・Iterator:複数の要素を内包するオブジェクトのすべての要素に対して、順番にアクセスする方法を提供する
 ・Mediator:オブジェクト間の相互作用を仲介するオブジェクトを定義し、オブジェクト間の結合度を低くする
 ・Memento:データ構造に対する一連の操作のそれぞれを記録しておき、以前の状態の復帰または操作の再現が行えるようにする
 ・Observer:インスタンスの変化を他のインスタンスから監視できるようにする
 ・State:オブジェクトの状態を変化させることで、処理内容を変えられるようにする
 ・Strategy:データ構造に対して適用する一連のアルゴリズムをカプセル化し、アルゴリズムの切り替えを容易にする
 ・Template Method:アルゴリズムは抽象クラスで、処理内容はサブクラスで定義する
 ・Visitor:データ構造を保持するクラスと、それに対して処理を行うクラスを分離する

今回は、「Visitorパターン」をやっていきます!

サンプルコードの理解

Visitorパターンでは、ビジター(訪問者)とアクセプター(受け入れ側)が出てきます。アクセプターは accept() というメソッドを持ち、これを変更することなく、ビジターの振る舞いを変更すること出来るのが特徴です。

では、実際に、サンプルコードを見ていくことにします。

今回のPlantUMLのクラス図

PlantUMLで作成したクラス図のコードと画像ファイルです。

@startuml
abstract Visitor {
    + {abstract} visit(File file)
    + {abstract} visit(Directory directory)
}

class ListVisitor {
    - String currentdir
    + visit(File file)
    + visit(Directory directory)
}
Visitor <|-- ListVisitor

interface Element {
    + {abstract} void accept(Visitor v)
}

abstract Entry {
    + {abstract} String getName()
    + {abstract} int getSize()
    + String toString()
}
Element <|.. Entry

class File {
    - String name
    - int size
    + File()
    + String getName()
    + int getSize()
    + void accept(Visitor v)
}
Entry <|-- File

class Directory {
    - String name
    - List<Entry> directory
    + Directory()
    + String getName()
    + int getSize()
    + Entry add()
    + Iterator<Entry> iterator()
    + void accept(Visitor v)
}
Entry <|-- Directory
Iterable <|.. Directory
@enduml

今回のクラス図
今回のクラス図

Mainクラス(使う側のクラス)

まずは、Visitorパターンが、どのように使われるのかを見るために、Mainクラスを見ていきます。

分かりやすくなると思うので、先に実行結果も示します。

public class Main {
    public static void main(String[] args) {
        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 yuki = new Directory("yuki");
        Directory hanako = new Directory("hanako");
        Directory tomura = new Directory("tomura");
        usrdir.add(yuki);
        usrdir.add(hanako);
        usrdir.add(tomura);
        yuki.add(new File("diary.html", 100));
        yuki.add(new File("Composite.java", 200));
        hanako.add(new File("memo.tex", 300));
        tomura.add(new File("game.doc", 400));
        tomura.add(new File("junk.mail", 500));
        rootdir.accept(new ListVisitor());
    }
}

実行結果

実行結果
実行結果

今回のサンプルコードは、ファイルシステムをイメージしたアプリケーションのようです。ファイルシステム側がアクセプターで、ディレクトリを走査するのがビジターです。

まず、rootディレクトリを作ります。その後、rootディレクトリの中にbin、tmp、usrディレクトリを作ります。

binディレクトリに、10000バイト(仮にバイトとする)のviファイルと、20000バイトのlatexファイルを作ります。

ここまでで、ファイルシステム側(アクセプター)の準備は完了です。ここから、ルートディレクトリに対して、走査(ビジター)を開始します。

まず、ビジターは、訪問先のrootディレクトリの情報を表示します。訪問先のパスを表示し、サイズ(rootなので全サイズ)を表示します。

次に、rootディレクトリ内を走査します。binディレクトリを見つけたので、同じように訪問先の情報を表示した後、次は、binディレクトリ内を走査します。viファイルを見つけたので、ファイルであっても同じように訪問先の情報を表示します。

これを繰り返して、全てのディレクトリ、ファイルを訪問しています。

その後、さらにディレクトリや、ファイルを追加して、最後に、再度、rootディレクトリから操作をしています。

全体像は分かってきたので、今度は中身を見ていきます。

Visitorクラス

Visitorクラスは、抽象クラスです。

2つの抽象メソッドを多重定義(オーバーロード)しています。

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

次は、Visitorクラスを継承しているクラスを見ていきます。

ListVisitorクラス

Visitorクラスを継承したListVisitorクラスです。

プライベートメンバ変数として、現在のディレクトリ(カレントディレクトリ)を持っています。

visit(File file) は、ファイルに訪問した場合の行動を実装していて、ファイルの絶対パスを表示しています。後で出てきますが、fileオブジェクトは文字列化のときはサイズも一緒に表示するような実装になっています。

visit(Directory directory) は、ディレクトリに訪問した場合の行動を実装していて、ディレクトリの絶対パスを表示しています。fileオブジェクトと同様に、Directoryオブジェクトは文字列化のときにサイズも一緒に表示する実装になっています。

その後、このディレクトリが持っている要素(ディレクトリやファイル)を走査していきます。自分自身を下層に移動させていき、そこでカレントディレクトリが変化していくので、現在位置をsavedirに覚えておきます。下層の走査が完了すると、カレントディレクトリを元に戻しています。

public class ListVisitor extends Visitor {
    // 現在注目しているディレクトリ名
    private String currentdir = "";

    // File訪問時
    @Override
    public void visit(File file) {
        System.out.println(currentdir + "/" + file);
    }

    // Directory訪問時
    @Override
    public void visit(Directory directory) {
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        for (Entry entry: directory) {
            entry.accept(this);
        }
        currentdir = savedir;
    }
}

ここまでがビジタークラスです。次からはアクセプタークラスを見ていきます。

Elementクラス

Elementクラスは、インターフェースです。

1つの抽象メソッドを定義しています。これがアクセプターのビジターを受け付けるメソッドになります。

public interface Element {
    public abstract void accept(Visitor v);
}

Entryクラス

Entryクラスは、Elementクラスのインタフェースを実装した抽象クラスです。

2つのメソッドを定義しているのと、文字列化メソッドをオーバーライドしています。先ほど見たように、文字列化のときに、サイズも取得して、一緒に表示するようになっています。

public abstract class Entry implements Element {
    public abstract String getName(); // 名前を得る
    public abstract int getSize();    // サイズを得る

    // 文字列表現
    @Override
    public String toString() {
        return getName() + " (" + getSize() + ")";
    }
}

Fileクラス

Entryクラスを継承したFileクラスです。

プライベートメンバ変数として、name(ファイル名)と、size(容量)を持っています。

コンストラクタは、プライベートメンバ変数の初期化を行っています。

getName() と、getSize() は、ファイル名と容量を取得するメソッドです。

accept() が、今回のVisitorパターンのキモです。やってることは、ビジターオブジェクトを引数で受け取り、ビジターオブジェクトのメソッドである visit() を使って、自分自身(アクセプター)に訪問させています。

Visitorパターンでは、アクセプターの accept() を変更することなく、ビジターの振る舞いを変更することが出来るのでした。確かに、アクセプターの実装は変更が不要で、ビジター側の実装で、振る舞いを変更できそうです。

public class File extends Entry {
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
}

Directoryクラス

Fileクラスと同様に、Entryクラスを継承したDirectoryクラスです。集約オブジェクトのようで、Iteratorインタフェースを実装しています。

プライベートメンバ変数として、name(ファイル名)と、Entryクラスのオブジェクトを複数格納できるリストの directory(ディレクトリ)を持っています。

コンストラクタは、プライベートメンバ変数の初期化を行っています。

getName() と、getSize() は、ファイル名と容量を取得するメソッドです。getSize() は、Fileクラスと異なり、再帰的に下層のEntryクラスのオブジェクトの容量を取得しています。ここはIteratorパターンを使っています。

add() は、リストにEntryクラスのオブジェクトを追加しています。

iterator() は、Iteratorパターンのために、自分自身のIteratorを返します。

accept() は、Fileクラスと同様です。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Directory extends Entry implements Iterable<Entry> {
    private String name;
    private List<Entry> directory = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getSize() {
        int size = 0;
        for (Entry entry: directory) {
            size += entry.getSize();
        }
        return size;
    }

    public Entry add(Entry entry) {
        directory.add(entry);
        return this;
    }

    @Override
    public Iterator<Entry> iterator() {
        return directory.iterator();
    }

    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
}

おわりに

今回はVisitorパターンを学びました。

Visitorパターンのサンプルコードは、やってることが明確で、分かりやすかったですね。

アクセプターを変更せずに、Visitorクラスを継承したクラス(今回はListVisitorクラスのみ)のビジターを別の用途で作ることが可能になっていますね。

今回は以上です!

最後までお読みいただき、ありがとうございました。