asato <asato@ncfreak.com>
最終更新日 : 2003/11/2 (2002/9/26 より)
Visitor パターン
Visitor パターンの問題点の 1 つには、カプセル化を破る、ということが挙げられます。あるオブジェクト構造上の要素で実行されるオペレーションを表現する。Visitor パターンにより、オペレーションを加えるオブジェクトのクラスに変更を加えずに、新しいオペレーションを定義することができるようになる。
AspectJ を使った Visitor に関するテクニックの種類
ダブルディスパッチ不要
結果:
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
疑問:
プログラマがコードの追加を行う必要のある部分は以下のものである:
これら 3 つの追加はすべて単純なコピー&ペーストで行うことができる。
GoF のパターンでは、ConcreteElement を 1 つ追加するだけで以下のような行動が要求される:
直接訪問
意図: ダブルディスパッチを使うことなく 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実装:
public class ConcreteElementC 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); pointcut elementC(ConcreteElementC elementC) : args(elementC); 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); } void around(Visitor vistor, ConcreteElementC elementC) : visit(vistor) && elementC(elementC) { vistor.visit(elementC); } public void Visitor.visit(Element element) {} public void Visitor.visit(ConcreteElementA elementA) { } public void Visitor.visit(ConcreteElementB elementB) { } public void Visitor.visit(ConcreteElementC elementC) { } }
public class Client { 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 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"); } public void visit(ConcreteElementC elementC) { System.out.println("ConcreteElementC"); } } }実行結果は以下のようになる:
ConcreteElementA ConcreteElementB ConcreteElementC
無差別訪問
適用可能性:
適用可能性 (詳細):
package dp.visitor; import java.util.List; import java.util.Vector; public class ObjectStructure { private List objects = new Vector(); public void add(Object obj) { objects.add(obj); } public List getObjectList() { return objects; } }
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; import dp.visitor.*; import java.util.Iterator; public class Client { public static void main(String[] args) { ObjectStructure structure = new ObjectStructure(); structure.add( new Element() ); structure.add( new Product() ); MyVisitor visitor = new MyVisitor(); Iterator itr = structure.getObjectList().iterator(); while( itr.hasNext() ) { visitor.visit( itr.next() ); } } }
Element Product
しかしながら、実行時例外として不正な visit を検出したり、あるいは、visit メソッドを何もしないメソッドとして実装することによる選択肢もある。
変化に対する耐性: 次のようなコードの実装上の変化がありうる:
サンプルコード:
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); }
実装:
しかしながら、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]
文献 [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
GoF によると (p.359)
AspectJ では、アスペクトが privileged として宣言されれば、通常のようにクラスの public なメソッドにアクセスできるだけでなく、private なメソッドや private なフィールドにさえアクセスできる権限が得られます。この実装では、Visitor の役割を持ったクラスを、通常のクラスとして定義するのではなく、アスペクトとして定義することにより、カプセル化に妥協することなく、要素の内部情報にアクセスする実装を紹介します。6. カプセル化を破る。 visitor によるアプローチでは、ConcreteElement クラスのインタフェースが、visitor が仕事を行うのに十分、強力であることを仮定している。その結果、このパターンでは要素の内部情報にアクセスする公開オペレーションを提供するように強いられることがしばしばある。したがって、カプセル化に対して妥協を与えることになるかもしれない。
実装に関してキーとなる考えは二つあります:
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
GoF によると (p.358)
3. 新しい ConcreteElement クラスを加えることは難しい。 Visitor パターンでは、Element の新しいサブクラスを加えることを難しくする。新しい ConcreteElement クラスを導入することにより、Visitor クラスでは新しい抽象化されたオペレーションを宣言し、各 ConcreteVisitor クラスではそれに対応する実装を行わなければならなくなる。デフォルトの実装を Visitor クラスに与えて、ほとんどの ConcrteteVisitor クラスにこれを継承されることもできるだろう。しかし、これは例外的な場合である。
サンプルコード
このようなシチュエーションでの 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