Observer(オブザーバー)パターンは対象オブジェクトの状態が変わるごとに、その情報を必要なインスタンス(Observer)に送る仕組みです。Observerパターンは、出版−購読型モデルとも呼ばれます。ちょうどメールマガジンに登録して、その配信を受けるのと同じです。したがって、配信する側(Generator)と受取る側(Observer)のふたつの役割が必要になります。
受取る側はリスナー(listener)と呼ばれることもあります。そして、配信する側が送るのはイベントです。どこかで聞いたことがありますね。そうです。ActionScript 3.0のイベントリスナーは、Observerパターンの実装なのです。本稿は、イベントリスナーの仕組みを単純にしたサンプルで、Observerパターンをご説明します[*1]。
01 Observerパターンのインターフェイス
ActionScript 3.0のクラス設計で統一の仕様を定めるときは、備えるべきメソッドはインターフェイス(interface)として定義します。まず、Observerパターンの配信側のインターフェイスには、少なくともつぎのような3つのメソッドを備えます。
- オブザーバー(リスナー)の登録: addListener()
- オブザーバー(リスナー)の削除: removeListener()
- オブザーバー(リスナー)への配信: dispatchEvent()
さらに今回のサンプルでは、配信側のクラスは一定の時間間隔でイベントを送ることにします。そこで、その配信開始のメソッドstart()を加えます。配信側のクラスに実装するインターフェイスIIntervalGeneratorは、つぎのスクリプト001のように定義されます。
スクリプト001■配信側のクラスに実装するインターフェイスIIntervalGenerator
  
    | 
// インターフェイス定義ファイル: IIntervalGenerator.as// イベントを配信するクラスに実装
 
 package {  public interface IIntervalGenerator {// 配信側クラスに必要なメソッド
    function addListener(observer:IObserver):void;    function removeListener(observer:IObserver):void;    function dispatchEvent():void;// サンプルとして加えたメソッド
    function start():void;  }} | 
このインターフェイスIIntervalGeneratorを実装するクラスには、もちろん登録したオブザーバー(インターフェイスIObserverを実装)のインスタンスを納めるプロパティが必要です。けれどJavaと異なり、ActionScript 3.0のインターフェイスにはプロパティは宣言できません。
つぎに、受信側のクラスに実装するインターフェイスです。配信側のクラスが実装するメソッドdispatchEvent()は、登録されたすべてのオブザーバーインスタンスを取出して、それぞれのインスタンスメソッドtimer()を呼出すこととします。したがって、受信側のインターフェイスIObserverには、つぎのスクリプト002のようにメソッドtimer()を定義します。
スクリプト002■受信側のクラスに実装するインターフェイスIObserver
  
    | 
// インターフェイス定義ファイル: IObserver.as// イベントを受信するクラスに実装
 
 package {  public interface IObserver {    function timer():void;  }} | 
02 インターフェイスを実装したクラスの定義
それでは、ふたつのインターフェイスをクラスに実装しましょう。まず、イベントを配信するクラスIntervalです(スクリプト003)。オブザーバー(インターフェイスIObserverを実装)のインスタンスはVector型のプロパティ(listeners)に納めることにしました。したがって、インターフェイスを実装する3つのメソッドaddListener()とremoveListener()、およびdispatchEvent()は、それに沿った処理になっています(スクリプト第11〜29行目)[*2]。
スクリプト003■イベントを配信するクラスInterval
  
    | 
// ActionScript 3.0クラス定義ファイル: Interval.as// イベントを配信する
 
 package {  import flash.utils.setInterval;  import flash.utils.clearInterval;  public class Interval implements IIntervalGenerator {    private var listeners:Vector.<IObserver> = new Vector.<IObserver>();    private var interval:uint;    private var id:uint;    public function Interval(myInterval:uint = 100) {      interval = myInterval;    }    public function addListener(listener:IObserver):void {      listeners.push(listener);    }    public function removeListener(listener:IObserver):void {      var nLength:uint = listeners.length;      for (var i:uint = 0; i < nLength; i++) {        if (listeners[i] == listener) {          listeners.splice(i, 1);          break;        }      }    }    public function dispatchEvent():void {      var nLength:uint = listeners.length;      for (var i:uint = 0; i < nLength; i++) {        var listener:IObserver = listeners[i];        listener.timer();      }    }    public function start():void {      clearInterval(id);      id = setInterval(dispatchEvent, interval);    }  }} | 
一定の時間間隔でイベントを送り始めるメソッドstart()は、setInterval()関数に配信のメソッドdispatchEvent()を指定しました。あえてsetInterval()関数を用いたのは、Observerパターンのイベントリスナーを使わないようにしたためです。時間間隔を定めるプロパティintervalは、コンストラクタメソッドInterval()の引数で設定できます(デフォルト値100ミリ秒)。
つぎに、イベントを受信するクラスListenerを定義します(スクリプト004)。インターフェイスIObserverを実装するメソッドtimer()が一定の時間間隔で呼出されます。したがって、このメソッドの中に、イベントが発生したとき処理したい内容を書き加えます。
スクリプト004■イベントを受信するクラスListener
  
    | 
// ActionScript 3.0クラス定義ファイル: Listener.as// イベントを受信する
 
 package {  import flash.display.MovieClip;  public class Listener extends MovieClip implements IObserver {    public var onTimer:Function;    public function Listener() {}    public function timer():void {      if (onTimer) {        onTimer();      }    }  }} | 
もっともこのクラスListenerは、少し変わったつくりになっています。Funciton型のプロパティ(onTimer)が宣言され、メソッドtimer()はそこに関数が入っていたら呼出します(第4〜10行目)[*3]。しかし、クラスListenerの定義には、プロパティへの関数の設定がありません。このクラスはMovieClipシンボルの[クラス](または[基本クラス])に指定することが想定されているのです(図001)。
図001■[シンボルプロパティ]ダイアログボックスで[クラス]を指定する

MovieClipシンボルには、フレームアクションが書けます。プロパティonTimerには、フレームアクションで関数を設定すればよいのです。たとえば、つぎのスクリプト005は、インスタンスを水平にスクロールし、ステージ中央で1回転させます。
スクリプト005■[クラス]にListenerを設定したMovieClipシンボルに記述したフレームアクション
  
    | 
// [クラス]にListenerを設定したMovieClipシンボル// フレームアクション
 
 var nSpeed:Number = 5;var nStageWidth:Number = stage.stageWidth;var nStageCenter:Number = nStageWidth / 2;var myInterval:Interval = new Interval();onTimer = xScroll;myInterval.addListener(this);myInterval.start();function xScroll():void {  x +=  nSpeed;  if (x > nStageWidth) {    x -=  nStageWidth;  } else if (Math.abs(x - nStageCenter) < nSpeed) {    x = nStageCenter;    onTimer = xRotate;  }}function xRotate():void {  rotation +=  nSpeed;  if (Math.abs(rotation) < nSpeed) {    rotation = 0;    onTimer = xScroll;  }} | 
図002■[クラス]にListenerを設定したMovieClipシンボルにフレームアクションでアニメーションさせる
Intervalインスタンスを生成して(スクリプト第4行目)、MovieClipインスタンス自身をaddListener()メソッドで登録すると(スクリプト第6行目)、start()メソッドにより(スクリプト第7行目)決められた時間間隔でListenerクラスのtimer()メソッドが呼出されます。
timer()メソッドはプロパティonTimerに設定された関数を呼出しますので、フレームアクションで指定した関数が実行されることになります。この仕組みは、ActionScript 2.0のリスナー(たとえばMouse.addListener()メソッド)に近い組立てになっています。
ご紹介したサンプルは、処理の流れをごく単純にしました。けれど、ActionScript 3.0の中でイベントリスナーがどれだけ用いられているかを見れば、予め定められたインスタンスを決まったときに動かす仕組みとしてObserverパターンの使い道の広さがおわかりいただけるでしょう。
  
    | [*2] 同じオブザーバーのインスタンスを複数登録する場合について、それをとくに避ける処理は加えていません。 [*3] if条件に直接Function型のプロパティを指定すると、コンパイラ警告#3553が告げられます。 Warning: 3553: Boolean型が必要なところに関数値が使用されています。この関数参照の後に括弧()がない可能性があります。 関数を呼出していない旨の警告ですので、あえて呼出さないこの処理では問題はありません。気になるようでしたら、値をBoolean型に変換してください。 
  
    | 
public function timer():void {  // if (onTimer) {if (Boolean(onTimer)) {
 |  | 
作成者: 野中文雄
  作成日: 2010年11月1日