Rethinking ObserverProtocol

AspectJデザインパターンの実装というと一番有名なのは Observer パターンだと思う。Hannemann らの提案した再利用可能な Observer パターンの ObserverProtocol が一番脚光を浴びているきがする。


でも、どのくらいこの ObserverProtocol が有効なんだろうか? 欠点はないのだろうか? もしかしたら、みんな気づいているかもしれないけど(元々の論文をちゃんと読み直すと指摘してるかも?)、僕がさっき気づいた(もしかすると昔気づいていたかもしれないけど)、欠点を2つ紹介したいと思う。二つとも問題点の原因は、タイプセーフじゃないことにあると思う。


Hannemann らの ObserverProtocol は以下のような感じ(見やすくするためにコメントとかは削ってます)


public abstract aspect ObserverProtocol {

protected interface Subject { }
protected interface Observer { }

private WeakHashMap perSubjectObservers;


protected List getObservers(Subject subject) {
if (perSubjectObservers == null) {
perSubjectObservers = new WeakHashMap();
}
List observers = (List)perSubjectObservers.get(subject);
if ( observers == null ) {
observers = new LinkedList();
perSubjectObservers.put(subject, observers);
}
return observers;
}

public void addObserver(Subject subject, Observer observer) {
getObservers(subject).add(observer);
}

public void removeObserver(Subject subject, Observer observer) {
getObservers(subject).remove(observer);
}

protected abstract pointcut subjectChange(Subject s);

after(Subject subject): subjectChange(subject) {
Iterator iter = getObservers(subject).iterator();
while ( iter.hasNext() ) {
updateObserver(subject, ((Observer)iter.next()));
}
}
protected abstract void updateObserver(Subject subject, Observer observer);
}

で Subject の役割としての Point は:


public class Point {

private int x;
private int y;
private Color color;


public Point(int x, int y, Color color) {
this.x=x;
this.y=y;
this.color=color;
}

public int getX() {
return x;
}

public int getY() {
return y;
}

public void setX(int x) {
this.x = x;
}

public void setY(int y) {
this.y = y;
}

public Color getColor() {
return color;
}

public void setColor(Color color) {
this.color=color;
}
}

で Observer の役割としての Screen は:


public class Screen {

private String name;

public Screen(String s) {
this.name = s;
}

public void display (String s) {
System.out.println(name + ": " + s);
}
}

Point のカラーの変化を見る ColorObserver はこんな感じ


public aspect ColorObserver extends ObserverProtocol{

declare parents: Point implements Subject;
declare parents: Screen implements Observer;


protected pointcut subjectChange(Subject subject):
call(void Point.setColor(Color)) && target(subject);


protected void updateObserver(Subject subject, Observer observer) {

((Screen)observer).display("Screen updated "+
"(point subject changed color).");
}
}


使用例は:


public class Main {

public static void main(String[] args) {

Point p = new Point(5, 5, Color.BLUE);

Screen s = new Screen("s1");

ColorObserver.aspectOf().addObserver(p, s);

p.setColor(Color.RED);
}
}

実行結果は:


s1: Screen updated (point subject changed color).


なかなかOKに見える。何が問題? たとえば、Observer の役割を持つ新しいクラス(Logger)が導入されたとしたら? この Logger クラスは、Point のカラーの変化に興味がある。


public class Logger {
public void log(String message) {
System.out.println("log: " + message);
}
}


ColorObserver の実装はどう変更すればよい?


一番ストレートに思いつく方法は、declare parents で新たに Logger クラスを宣言すること:


public aspect ColorObserver extends ObserverProtocol{

declare parents: Point implements Subject;
declare parents: Screen implements Observer;

declare parents: Logger implements Observer;

protected pointcut subjectChange(Subject subject):
call(void Point.setColor(Color)) && target(subject);


protected void updateObserver(Subject subject, Observer observer) {

// Screen も Logger もどっちもありえる。どうする?
((Screen)observer).display("Screen updated "+
"(point subject changed color).");

}
}

とすると updateObserver の実装に困る。もちろん、instanceof とかで逃げることはできる。Logger が追加されなかっても、元々、Subject も Observer も場合によっては、キャストが必要なんだから、いまさら、って思うかもしれない。でも、キャストが必要ないようにできるようにはしたい。

これが、問題その1。


次の問題は、さらに追加で、CoordinateObserver が追加されたとする。このときは、Subject の役割は前と同じく Point で Observer は、Logger だとする。実装は以下のような感じ:


public aspect CoordinateObserver extends ObserverProtocol{


declare parents: Point implements Subject;
declare parents: Logger implements Observer;

protected pointcut subjectChange(Subject subject):
(call(void Point.setX(int)) ||
call(void Point.setY(int)) ) && target(subject);

protected void updateObserver(Subject subject, Observer observer) {
( (Logger)observer).log("Screen updated (point subject changed coordinates).");
}
}

問題はその使用例にある。ColorObserver で Screen も Observer を実装しているのが問題になる。


public class Main {

public static void main(String[] args) {

Point p = new Point(5, 5, Color.BLUE);

Screen s = new Screen("s1");
Logger l = new Logger();

CoordinateObserver.aspectOf().addObserver(p, l);
CoordinateObserver.aspectOf().addObserver(p, s); // screen も渡せる!
  // ClassCastException がスロー

p.setX(10);
}

ので、ClassCastException がスローされる可能性がある。


もちろん、使用者側が注意していればいいことだけど、コンパイル時に指摘してもらえるならそれにこしたことはない。これが問題の二つ目。この問題も、うまくコードすれば解決できるかもしれないけど、再利用可能な Observer の利点が薄まるんじゃないかと思う。


参考:

Jan Hannemann and Gregor Kiczales
Design Pattern Implementation in Java and AspectJ
OOPSLA 2002

DL: http://www.cs.ubc.ca/~jan/papers/oopsla/oopsla2002.html
HP: http://www.cs.ubc.ca/~jan/AODPs/

Mira Mezini and Klaus Ostermann
Conquering Aspects with Caesar
AOSD 2003
DL: http://www.st.informatik.tu-darmstadt.de/public/Publications.jsp

ObserverProtocol の欠点を指摘して Caesar 言語モデルの提案。