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 クラスを宣言すること:
とすると updateObserver の実装に困る。もちろん、instanceof とかで逃げることはできる。Logger が追加されなかっても、元々、Subject も Observer も場合によっては、キャストが必要なんだから、いまさら、って思うかもしれない。でも、キャストが必要ないようにできるようにはしたい。
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).");
}
}
これが、問題その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 を実装しているのが問題になる。
ので、ClassCastException がスローされる可能性がある。
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);
}
もちろん、使用者側が注意していればいいことだけど、コンパイル時に指摘してもらえるならそれにこしたことはない。これが問題の二つ目。この問題も、うまくコードすれば解決できるかもしれないけど、再利用可能な Observer の利点が薄まるんじゃないかと思う。
参考:
Jan Hannemann and Gregor Kiczales
Design Pattern Implementation in Java and AspectJ
OOPSLA 2002DL: 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 言語モデルの提案。