AspectJ を使ったデザインパターンの改善と支援 >
Visitor

asato <asato@ncfreak.com>

最終更新日 : 2003/11/2 (2002/9/26 より)

Visitor パターン

GoF [1] によれば、Visitor パターンの意図は以下のものです:
あるオブジェクト構造上の要素で実行されるオペレーションを表現する。Visitor パターンにより、オペレーションを加えるオブジェクトのクラスに変更を加えずに、新しいオペレーションを定義することができるようになる。
Visitor パターンの問題点の 1 つには、カプセル化を破る、ということが挙げられます。

AspectJ を使った Visitor に関するテクニックの種類

ダブルディスパッチ不要

意図: ダブルディスパッチを行うようなコードを ConcreteElement に埋め込まずに Visitor パターンを実装する。

結果:

GoF パターンと比べた場合の有利な点と不利な点:
package dp.visitor;

public class Visitor {
}
package dp.visitor;

public interface Element {
}
package dp.visitor;

public class ElementA implements Element {
}
package dp.visitor;

public class ElementB implements Element {
}
package dp.visitor;

public aspect VisitorAspect {

	pointcut accept(Visitor visitor) :
		args(visitor) && call( void Element.accept(Visitor) );

	pointcut elementA(ElementA elementA) : target(elementA);
	pointcut elementB(ElementB elementB) : target(elementB);

	void around(Visitor visitor, ElementA elementA) : accept(visitor) && elementA(elementA) {
		visitor.visit(elementA);
	}

	void around(Visitor visitor, ElementB elementB) : accept(visitor) && elementB(elementB) {
		visitor.visit(elementB);
	}
	
	public void Element.accept(Visitor visitor) {}

	public void Visitor.visit(ElementA elementA) {}
	public void Visitor.visit(ElementB elementB) {}
}
package dp;

import dp.visitor.*;

public class Client {

	public static void main(String[] args) {

		Element[] elements = new Element[2];
		elements[0] = new ElementA();
		elements[1] = new ElementB();


		Visitor visitor = new MyVisitor();

		for(int i = 0 ; i < elements.length ; i++) {
			elements[i].accept( visitor );
		}
	}
	
	public static class MyVisitor extends Visitor {
		public void visit(ElementA elementA) {
			System.out.println("ElementA");
		}
		public void visit(ElementB elementB) {
			System.out.println("ElementB");
		}
	}
}
ElementA
ElementB

疑問:

  1. ConcreteElement の増加の管理: ConcreteElement の数が増えるとアスペクトの管理が大変なのではないのか?

    プログラマがコードの追加を行う必要のある部分は以下のものである:

    • ConcreteElement を区別するための pointcut の追加。
    • その pointcut に対する around 文の追加。
    • Visitor に対しての新しい ConcreteElement を訪問するためのメソッドの追加。

    これら 3 つの追加はすべて単純なコピー&ペーストで行うことができる。

    GoF のパターンでは、ConcreteElement を 1 つ追加するだけで以下のような行動が要求される:

    • 新しい ConcreteElement に対して Visitor を受け入れるためのメソッドを実装する。
    • Visitor に対して新しい ConcreteElement を訪問するためのメソッドを実装する。

直接訪問

(AspectJ 1.1.1 OK)

意図: ダブルディスパッチを使うことなく Visitor パターンを実装する。Element の役割を持つクラスは Visitor オブジェクトを受け入れるためのメソッドを定義する必要がなくなる。

結果:

サンプルコード:
public interface Visitor { }
public interface Element { }
public class ConcreteElementA implements Element { }
public class ConcreteElementB implements Element { }
public aspect VisitorAspect {

	pointcut visit(Visitor vistor) :
		target(vistor) && call( void Visitor.visit(Element) );

	pointcut elementA(ConcreteElementA elementA) : args(elementA);
	pointcut elementB(ConcreteElementB elementB) : args(elementB);

	void around(Visitor vistor, ConcreteElementA elementA) : visit(vistor) && elementA(elementA) {
		vistor.visit(elementA);
	}
	void around(Visitor vistor, ConcreteElementB elementB) : visit(vistor) && elementB(elementB) {
		vistor.visit(elementB);
	}
	
	public void Visitor.visit(Element element) { }

	public void Visitor.visit(ConcreteElementA elementA) { }
	public void Visitor.visit(ConcreteElementB elementB) { }
}
public class Client {

	public static void main(String[] args) {

		Element[] elements = new Element[2];
		elements[0] = new ConcreteElementA();
		elements[1] = new ConcreteElementB();


		Visitor visitor = new MyVisitor();

		for(int i = 0 ; i < elements.length ; i++) {
			visitor.visit( elements[i] );
		}
	}
	
	public static class MyVisitor implements Visitor {
		public void visit(ConcreteElementA elementA) {
			System.out.println("ConcreteElementA");
		}
		public void visit(ConcreteElementB elementB) {
			System.out.println("ConcreteElementB");
		}
	}
}
ConcreteElementA
ConcreteElementB
実装:

無差別訪問

意図: あるオブジェクト構造上におけるどんなオブジェクトにたいしても訪問できるような Visitor を実装する。

適用可能性:

適用可能性 (詳細):

結果:
  1. 型チェックの恩恵を失なう: どんなオブジェクトにたいしても visit できることは、意図されていないオブジェクトにたいする visit メソッドの呼び出しをコンパイル時に検出できないことを意味する。

    しかしながら、実行時例外として不正な visit を検出したり、あるいは、visit メソッドを何もしないメソッドとして実装することによる選択肢もある。

  2. visit される側のオブジェクトは共通の interface を実装している必要はない: visit される側のオブジェクトは共通の interface を実装している必要はなく、Visitor オブジェクトと visit される側のオブジェクトの結びつきの関係は、より弱い。

変化に対する耐性: 次のようなコードの実装上の変化がありうる:

  1. visit される側の型名の変更:
  2. 新しい visit される側のオブジェクトの追加:
  3. 新しい Visitor の作成:

サンプルコード:

package dp.visitor;

public class Element { }
package dp.visitor;

public class Product { }
package dp.visitor;

public class MyVisitor {
	public void visit(Element element) {
		System.out.println("Element");
	}
	public void visit(Product prodcut) {
		System.out.println("Product");
	}
}
package dp.visitor;

import dp.visitor.*;

public class Client {

	public static void main(String[] args) {

		Object[] objects = new Object[2];
		objects[0] = new Element();
		objects[1] = new Product();


		MyVisitor visitor = new MyVisitor();

		for(int i = 0 ; i < objects.length ; i++) {
			visitor.visit( objects[i] );
		}
	}
}
package dp.visitor;

public aspect VisitorAspect {

	declare parents: MyVisitor implements Visitor;

	private interface Visitor { }

	pointcut visit(Visitor visitor) :
		target(visitor) &&
		call( void Visitor.visit(Object) ) &&
		!this(VisitorAspect);

	void around(Visitor visitor, Element element) : args(element) && visit(visitor) {
		visitor.visit( element );
	}
	void around(Visitor visitor, Product prodcut) : args(prodcut) && visit(visitor) {
		visitor.visit( prodcut );
	}

	public void Visitor.visit(Object obj) {}

	public abstract void Visitor.visit(Element element);
	public abstract void Visitor.visit(Product prodcut);
}
Element
Product
サンプルコード捕捉 - アスペクトの再利用を考慮する:
package dp.visitor;

public abstract aspect VisitorProtocol {

	protected interface Visitor {}

	pointcut visit(Visitor visitor) :
		target(visitor) &&
		call( void Visitor.visit(Object) ) &&
		!this(VisitorProtocol);

	public void Visitor.visit(Object object) { }
}
package dp.visitor;

public aspect VisitorAspect extends VisitorProtocol {

	declare parents: MyVisitor implements Visitor;

	void around(Visitor visitor, Element element) : args(element) && visit(visitor) {
		visitor.visit( element );
	}
	void around(Visitor visitor, Product prodcut) : args(prodcut) && visit(visitor) {
		visitor.visit( prodcut );
	}

	public abstract void Visitor.visit(Element element);
	public abstract void Visitor.visit(Product prodcut);
}

実装:

  1. どこに visit メソッドを定義するのか: サンプルコードでは、各オブジェクトを訪問するためのメソッドを抽象メソッドとして Visitor interface に定義した。これにより、Visitor の interface を実装するクラスでは (サンプルコードでは MyVisitor) それら抽象メソッドを実装することが強制される。抽象メソッドとして宣言することで、プログラマが抽象メソッドの実装し忘れたり、タイプミスによるオーバーライドミスなどを防ぐことができる。

    しかしながら、Visitor interface を実装するすべてのクラスが Visitor interface で定義された visit メソッドを必要としないかもしれない。サンプルコードでいえば、次のような意図をもった Visitor interface を実装するクラスがあるかもしれない: Element クラスを visit することを意図していても、Prodcut クラスを visit することは意図していない; つまり、visit(Eolement) メソッドを実装しても、visit(Product) は実装したくない。上記のサンプルコードの実装では、このような要求にたいして柔軟に対応できてない。つまり、Visitor interface を実装するすべてのクラスが考えられるすべての visit メソッドを実装することが強制される。

    解決方法としては、少々コードは複雑になるが、target pointcut を各 Visitor interface を実装するクラスに使うことと、上記のサンプルコードの少し変更と追加によって対処できる方法がある:

    package dp.visitor;
    
    public class YourVisitor {
    	public void visit(Element element) {
    		System.out.println("Element");
    	}
    }
    
    package dp.visitor;
    
    import dp.visitor.*;
    
    public class Client {
    
    	public static void main(String[] args) {
    
    		Object[] objects = new Object[2];
    		objects[0] = new Element();
    		objects[1] = new Product();
    
    
    		MyVisitor visitor1 = new MyVisitor();
    
    		for(int i = 0 ; i < objects.length ; i++) {
    			visitor1.visit( objects[i] );
    		}
    		YourVisitor visitor2 = new YourVisitor();
    
    		for(int i = 0 ; i < objects.length ; i++) {
    			visitor2.visit( objects[i] );
    		}
    	}
    }
    
    package dp.visitor;
    
    public abstract aspect VisitorProtocol {
    
    	protected interface Visitor {}
    
    	pointcut visit() :
    		call( void Visitor.visit(Object) ) && !this(VisitorProtocol);
    
    	public void Visitor.visit(Object object) { }
    }
    
    package dp.visitor;
    
    public aspect VisitorAspect extends VisitorProtocol {
    
    	declare parents: MyVisitor   implements Visitor;
    	declare parents: YourVisitor implements Visitor;
    
    
    	void around(MyVisitor visitor, Element element) :
    		target(visitor) && args(element) && visit()
    	{
    		visitor.visit( element );
    	}
    	void around(MyVisitor visitor, Product prodcut) :
    		target(visitor) && args(prodcut) && visit()
    	{
    		visitor.visit( prodcut );
    	}
    
    	void around(YourVisitor visitor, Element element):
    		target(visitor) && args(element) && visit()
    	{
    		visitor.visit( element );
    	}
    
    
    	public abstract void MyVisitor.visit(Element element);
    	public abstract void MyVisitor.visit(Product prodcut);
    
    	public abstract void YourVisitor.visit(Element element);
    }
    
    Element
    Product
    Element
    

実装 - その 4 [2]

(1.1 OK)

文献 [2] を元にしての実装です。

public interface Element { }
public class ConcreteElementA implements Element {
	private String getString() {
		return "ConcreteElementA";
	}
}
public class ConcreteElementB implements Element {
	private String getString() {
		return "ConcreteElementB";
	}
}
privileged public aspect Visitor {

	public abstract void Element.operation();

	public void ConcreteElementA.operation() {
		System.out.println("operation: " + getString() );
	}
	public void ConcreteElementB.operation() {
		System.out.println("operation: " + getString() );
	}
}
public class Client {

	public static void main(String[] args) {

		Element[] elements = new Element[2];
		elements[0] = new ConcreteElementA();
		elements[1] = new ConcreteElementB();
	
		for(int i = 0 ; i < elements.length; i++) {
			elements[i].operation();
		}
	}
}
operation: ConcreteElementA
operation: ConcreteElementB

実装 - その 5

(AspectJ 1.1.1 OK)

GoF によると (p.359)

6. カプセル化を破る。 visitor によるアプローチでは、ConcreteElement クラスのインタフェースが、visitor が仕事を行うのに十分、強力であることを仮定している。その結果、このパターンでは要素の内部情報にアクセスする公開オペレーションを提供するように強いられることがしばしばある。したがって、カプセル化に対して妥協を与えることになるかもしれない。
AspectJ では、アスペクトが privileged として宣言されれば、通常のようにクラスの public なメソッドにアクセスできるだけでなく、private なメソッドや private なフィールドにさえアクセスできる権限が得られます。この実装では、Visitor の役割を持ったクラスを、通常のクラスとして定義するのではなく、アスペクトとして定義することにより、カプセル化に妥協することなく、要素の内部情報にアクセスする実装を紹介します。

実装に関してキーとなる考えは二つあります:

前者は、もし、ConcreteVisitor の役割を持ったアスペクトが、Visitor interface として扱われる必要があるのであれば、重要になります。つまり:
MyAspectVsitor my = ...
Vsitor visitor = my;
後者は、通常、アスペクトは普通のクラスのようには new を使ってインスタンス化できないため (インスタンスを得るためには、MyAspectVsitor.aspecOf() などのメソッドを呼び出す必要がある)、もし、ConcreteVisitor の役割を持ったアスペクトのインスタンスを複数必要なのであれば重要になってきます。

サンプルコード (「直接訪問」のテクニックを基にしています):

public interface Element { }
public class ConcreteElementA implements Element {

	private String str;
	
	public ConcreteElementA(String str) {
		this.str = str;
	}
}
public class ConcreteElementB implements Element {

	private int i;
	
	public ConcreteElementB(int i) {
		this.i = i;
	}
}
public interface Visitor { }
public aspect VisitorAspect {

	pointcut visit(Visitor vistor) :
		target(vistor) && call( void Visitor.visit(Element) );

	pointcut elementA(ConcreteElementA elementA) : args(elementA);
	pointcut elementB(ConcreteElementB elementB) : args(elementB);

	void around(Visitor vistor, ConcreteElementA elementA) : visit(vistor) && elementA(elementA) {
		vistor.visit(elementA);
	}
	void around(Visitor vistor, ConcreteElementB elementB) : visit(vistor) && elementB(elementB) {
		vistor.visit(elementB);
	}
	
	public void Visitor.visit(Element element) {}

	public void Visitor.visit(ConcreteElementA elementA) { }
	public void Visitor.visit(ConcreteElementB elementB) { }
}
privileged public aspect MyVisitor implements Visitor {

	public static MyVisitor newInstance() {
		return new MyVisitor();
	}

	public void visit(ConcreteElementA elementA) {
		System.out.println("ConcreteElementA - " + elementA.str);
	}
	
	public void visit(ConcreteElementB elementB) {
		System.out.println("ConcreteElementB - " + elementB.i);
	}
}
public class Client {

	public static void main(String[] args) {

		Element[] elements = new Element[2];
		elements[0] = new ConcreteElementA("aaa");
		elements[1] = new ConcreteElementB(10);


		Visitor visitor = MyVisitor.newInstance();

		for(int i = 0 ; i < elements.length ; i++) {
			visitor.visit( elements[i] );
		}
	}
}
ConcreteElementA - aaa
ConcreteElementB - 10

実装 - その 6

(AspectJ 1.1.1 OK)

GoF によると (p.358)

3. 新しい ConcreteElement クラスを加えることは難しい。 Visitor パターンでは、Element の新しいサブクラスを加えることを難しくする。新しい ConcreteElement クラスを導入することにより、Visitor クラスでは新しい抽象化されたオペレーションを宣言し、各 ConcreteVisitor クラスではそれに対応する実装を行わなければならなくなる。デフォルトの実装を Visitor クラスに与えて、ほとんどの ConcrteteVisitor クラスにこれを継承されることもできるだろう。しかし、これは例外的な場合である。

サンプルコード

以下のようなシチュエーションがあるとします。まず、誰かの作成した、Visitor パターンを適用したライブラリがあるとします。次に、そのライブラリを利用してクライアントアプリケーションを実装したいとします。

このようなシチュエーションでの 1 つの困難な点は、新しい ConcreteElement をクライアントから追加したい、ということです。

これら、ライブラリのソースとアプリケーションのソースが、以下のようなディレクトリ構成にあるとします

[aspectj_dp]
 |
 +- build.xml
 +- lib.jar // dp.lib ライブラリ
 |
 +- [lib-src]
 |   |
 |   +- [dp]
 |       |
 |       +-[lib]
 |          |
 |          +- Element.java
 |          +- ConcreteElementA.java
 |          +- ConcreteElementB.java
 |          +- Visitor.java
 |          +- ConcreteVisitor.java
 |          +- Main.java
 |    
 +- [lib-dest]
 |    |
 |    + ライブラリのクラスファイル
 |
 +- [app-src]
 |   |
 |   +- [dp]
 |       |
 |       +- [app]
 |           |
 |           +- ConcreteElementC
 |           +- VisitorAspect
 |           +- Main.java
 |
 +- [app-dest]
     |
     + アプリケーションのクラスファイル

また、build ファイルは以下のようであるとします:
<project name="aspectj dp" default="lib" >

	<taskdef 
		resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
		<classpath>
			// AspectJ のライブラリの場所は適当に
			<pathelement location="../../lib/aspectjtools.jar"/> 
		</classpath>
	</taskdef>
	
	<target name="lib" >
		<javac srcdir="lib-src" destdir="lib-dest" debug="on" />
		<jar destfile="lib.jar" basedir="lib-dest"/>
	</target>
	
	<target name="app" >
		<iajc
			destdir="app-dest" 
			sourceroots="app-src"
			incremental="true"
			injars="lib.jar"
		>
			<classpath>
				<pathelement location="../../lib/aspectjrt.jar"/>
				<pathelement location="lib.jar"/>
			</classpath>
		</iajc>
	</target>
</project>
ライブラリ (dp.lib パッケージ):
package dp.lib;

public interface Element {
	public void accept(Visitor visitor);
}
package dp.lib;

public class ConcreteElementA implements Element {
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}
package dp.lib;

public class ConcreteElementB implements Element {
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}
package dp.lib;

public interface Visitor {
	public void visit(ConcreteElementA elementA);
	public void visit(ConcreteElementB elementB);
}
package dp.lib;

public class ConcreteVisitor implements Visitor {

	public void visit(ConcreteElementA elementA) {
		System.out.println("ConcreteVisitor - ConcreteElementA");
	}
	
	public void visit(ConcreteElementB elementB) {
		System.out.println("ConcreteVisitor - ConcreteElementB");
	}
}
package dp.lib;

public class Main { // 単に動作を確認するだけ

	public static void main(String[] args) {

		Element[] elements = new Element[2];
		elements[0] = new ConcreteElementA();
		elements[1] = new ConcreteElementB();

		Visitor visitor = new ConcreteVisitor();

		for(int i = 0 ; i < elements.length ; i++) {
			elements[i].accept(visitor);
		}
	}
}
実行結果:
ConcreteVisitor - ConcreteElementA
ConcreteVisitor - ConcreteElementB
アプリケーション (dp.app パッケージ):
package dp.app;

import dp.lib.Element;
import dp.lib.Visitor;

public class ConcreteElementC implements Element {

	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}
package dp.app;

import dp.lib.Element;
import dp.lib.Visitor;
import dp.lib.ConcreteVisitor;

public aspect VisitorAspect {

	public abstract void Visitor.visit(ConcreteElementC elementC);

	public void ConcreteVisitor.visit(ConcreteElementC elementC) {
		System.out.println("ConcreteVisitor - ConcreteElementC");
	}
}
package dp.app;

import dp.lib.Element;
import dp.lib.ConcreteElementA;
import dp.lib.ConcreteElementB;

import dp.lib.Visitor;
import dp.lib.ConcreteVisitor;

public class Main {

	public static void main(String[] args) {

		Element[] elements = new Element[3];
		elements[0] = new ConcreteElementA();
		elements[1] = new ConcreteElementB();
		elements[2] = new ConcreteElementC();

		Visitor visitor = new ConcreteVisitor();

		for(int i = 0 ; i < elements.length ; i++) {
			elements[i].accept(visitor);
		}
	}
}
実行結果:
ConcreteVisitor - ConcreteElementA
ConcreteVisitor - ConcreteElementB
ConcreteVisitor - ConcreteElementC

参考文献とリソース

参考文献: リソース:

更新履歴

todo