サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash非公式テクニカルノート

Model-View-Controller (MVC)

ID: FN1012001 Product: Flash CS5 and above Platform: All Version: 10 and above/ActionScript 3.0

Model-View-Controller」(モデル・ビュー・コントローラ)は、ユーザーにデータをインタラクティブに示すアプリケーションについて、3つの要素で構成する設計の手法です。3つの構成要素の頭文字をとってMVCモデルとか、デザインパターンのひとつとしてMVCパターンと呼ばれることもあります[*1]

データを扱うModelとその表示であるViewをはっきりと分け、ユーザーインタラクションはControllerが担います。それぞれの構成要素やその間でデザインパターンが用いられることも多く、デザインパターンよりももう少し大きなくくりで考える設計というべきでしょう(注[*1]参照)。

[*1] 詳しくは、Wikipedia「Model View Controller」をご参照ください。


01 Model-View-Controllerとは
Model-View-Controller各構成要素の間の役割の分け方には、バリエーションがあります。中には、ViewとControllerをひとつにまとめてしまう例もみられるようです。大切なのは、互いの構成要素をできるだけ意識せずに設計でき、また同じ仕様の他の要素と入替えやすくするということです。その方向で考えれば、細かな違いはあまり気にしなくてもよいでしょう。

Model-View-Controllerのひとつの典型的な役割は、つぎのとおりです(図001・表001)。

Modelはデータをもち、その処理を必要に応じて行います。データが変わると、それをViewに知らせます。ただし、Model自身は他の構成要素について知りません。通知は、Observer(オブザーバー)パターンなどを用いて、相手の具体的な中身を問うことなく行われます。

ViewはまずModelのデータを反映して、ユーザーに表示します。また、グラフィカルユーザインターフェース(GUI)にユーザーが操作を加えたときは、Controllerに伝えます。そのために、ModelとControllerへの参照をもちます。

Controllerは、ユーザーの操作を必要に応じてModelに伝えます。そのため、Modelを参照します。ユーザインターフェースの操作は、Viewから知らされます。

図001■Model-View-Controllerの互いの関係
図001

表001■Model-View-Controllerの役割
Model データとその処理を扱う。値が変わると、結果はViewに表される。しかし、他の構成要素を直接参照はしない。値が変わったことは、Observerパターンなどによって伝える。
View Modelの必要なデータを、適切なかたちでユーザーに見せる。ユーザインターフェースに操作が加えられると、それをControllerに伝えることによってModelのデータや自身の表示が変わる。そのために、ModelとControllerへの参照をもつ。
Controller ユーザーの操作に対応して処理を行う。その操作は、Viewから知らされることもある。Modelを参照するので、データを変える必要があれば、Modelに伝える。その結果に応じて、Viewの表示も変わる。

本稿は、時計をお題にして、Model-View-Controllerの例をご紹介します。まず、ユーザーインタラクションのないModelとViewだけのデジタル時計をつくります。そして、もうひとつのViewとして、アナログ時計を定義します。

つぎに、Controllerを加えた例です。Modelを拡張して、ストップウォッチを定義します。Viewにはユーザーインターフェイスとなる開始・停止・リセットのボタンが加わります。ControllerはViewからユーザー操作を受取って、Modelに伝えます。


02 時計のModel
時計のModelは、ひたすらデータとしての時を刻みます。そして、時刻が変わったことは、ObserverパターンによりViewに知らせます。時計のModelとなるクラスは、時刻のデータを管理し、受渡す必要があるでしょう。そこでまず、時分秒のプロパティが備わった時刻データ用の簡単なクラスTimeを定義します(スクリプト001)。

スクリプト001■時刻の値を保持するクラスTime
    // クラス定義ファイル: Time.as
    // 時刻の値を保持する
  1. package {
  2.   public class Time {
  3.     public var hours:uint;
  4.     public var minutes:uint;
  5.     public var seconds:uint;
  6.     public function Time(nHours:uint, nMinutes:uint, nSeconds:uint) {
  7.       hours = nHours;
  8.       minutes = nMinutes;
  9.       seconds = nSeconds;
  10.     }
  11.     public function clone():Time {
  12.       var myTime:Time = new Time(hours, minutes, seconds);
  13.       return myTime;
  14.     }
  15.   }
  16. }

ひとつ説明しておきたいのは、clone()メソッドです(スクリプト001第11〜14行目)。Timeインスタンスのやり取りで、そのまま参照を渡してしまうと、相手に書替えられてしまう恐れがあります。そのようなときこのメソッドは、プロパティが同じ新たなインスタンスを返します。

つぎに、時刻のデータを扱うModelとなるクラスClockModelの定義です(後掲スクリプト002)。刻んだ時は、Observerパターンによって知らせます。そのために、EventDispatcherクラスを継承しています。プロパティやメソッドには、あとで拡張することを考えて加えてあるものもあります。とくにアクセス制御の属性がprotectedのメソッドは、サブクラスから呼出したり、オーバーライド(再定義)するつもりです。差し当たって時計で必要な定義だけを抜出すと、つぎのとおりです。

  1. package {
  2.   import flash.events.EventDispatcher;
  3.   import flash.events.Event;
  4.   import flash.events.TimerEvent;
  5.   import flash.utils.Timer;
  6.   public class ClockModel extends EventDispatcher {
  1.     protected var _timer:Timer;
  2.     public function ClockModel() {
  3.       _timer = new Timer(1000);
  4.       _timer.addEventListener(TimerEvent.TIMER, update);
  5.       initialize();
  6.     }
  7.     public function get time():Time {
  8.       var myTime:Time = getTime();
  9.       return myTime;
  10.     }
  1.     protected function initialize():void {
  2.       start();
  3.     }
  4.     public function start():void {
  5.       _timer.start();
  6.     }
  7.     protected function getTime():Time {
  8.       var myDate:Date = new Date();
  9.       var myTime:Time = new Time(myDate.hours, myDate.minutes, myDate.seconds);
  10.       return myTime;
  11.     }
  12.     private function update(eventObject:TimerEvent = null):void {
  13.       dispatchEvent(new Event(Event.CHANGE));
  14.     }
  15.   }
  16. }

コンストラクタメソッド(スクリプト002第9〜13行目)は、Timerインスタンスを1,000ミリ秒の指定で生成し(プロパティ_timer)、メソッドupdate()をイベントリスナーに設定しています。そして、initialize()メソッド(第22〜24行目)の呼出しを経て、メソッドstart()からTimer.start()メソッドを呼出しています(第25〜27行目)。

リスナーメソッドupdate()からは、定数Event.CHANGEをイベントとしてEventDispatcher.dispatchEvent()メソッドが呼出されます(スクリプト002第33〜35行目)。時計のViewは、このEvent.CHANGEのイベントリスナーを定め、getアクセサメソッドtime(第14〜17行目)により時刻のデータを得ます。getアクセサメソッドtimeは、getTime()メソッド(第28〜32行目)でその時刻がプロパティに納められたTimeオブジェクトを得て返します。

スクリプト002■時刻のテータを扱うModelとなるクラスClockModel
    // クラス定義ファイル: ClockModel.as
    // 時刻のテータを扱うModel
  1. package {
  2.   import flash.events.EventDispatcher;
  3.   import flash.events.Event;
  4.   import flash.events.TimerEvent;
  5.   import flash.utils.Timer;
  6.   public class ClockModel extends EventDispatcher {
  7.     protected var _time:Time = new Time(0, 0, 0);
  8.     protected var _timer:Timer;
  9.     public function ClockModel() {
  10.       _timer = new Timer(1000);
  11.       _timer.addEventListener(TimerEvent.TIMER, update);
  12.       initialize();
  13.     }
  14.     public function get time():Time {
  15.       var myTime:Time = getTime();
  16.       return myTime;
  17.     }
  18.     public function set time(myTime:Time):void {
  19.       _time = myTime.clone();
  20.       update();
  21.     }
  22.     protected function initialize():void {
  23.       start();
  24.     }
  25.     public function start():void {
  26.       _timer.start();
  27.     }
  28.     protected function getTime():Time {
  29.       var myDate:Date = new Date();
  30.       var myTime:Time = new Time(myDate.hours, myDate.minutes, myDate.seconds);
  31.       return myTime;
  32.     }
  33.     private function update(eventObject:TimerEvent = null):void {
  34.       dispatchEvent(new Event(Event.CHANGE));
  35.     }
  36.   }
  37. }

今は必要のないsetアクセサメソッドtimeは、あとで使えるように定義しました(スクリプト002第18〜21行目)。渡されたTimeオブジェクトの複製をclone()メソッドで受取って、プロパティ(_time)に設定します。そして、Modelのデータが変わりますので、update()メソッドの呼出しによりイベントをリスナーに配信します。

つまり、ClockModelインスタンスに登録されたEvent.CHANGEのイベントリスナーは、(1)1,000ミリ秒間隔(スクリプト002第11行目)および(2)setアクセサメソッドtimeにTimeインスタンスが設定されたとき(第20行目)にイベントの配信を受けます。


03 デジタル時計のView
まずはデジタル時計のViewをつくり、そのあとアナログ時計のViewを加えます。けれど、それらがともに用いるプロパティとメソッドを、基本クラスClockViewとして定義しておくことにします(スクリプト003)。このクラスのプロパティとメソッドは、Viewが基本的に備えるべきものでもあります。時刻をタイムラインに表示できるように、Spriteクラスを継承させます。

スクリプト003■時計のViewの基本を定めるクラスClockView
    // ActionScript 3.0クラス定義ファイル: ClockView.as
    // 時計のViewの基本を定める
  1. package {
  2.   import flash.display.Sprite;
  3.   import flash.events.Event;
  4.   public class ClockView extends Sprite {
  5.     protected var clockData:ClockModel;
  6.     public function ClockView(myClock:ClockModel) {
  7.       clockData = myClock;
  8.     }
  9.     protected function update(eventObject:Event):void {}
  10.   }
  11. }

01「Model-View-Controllerとは」で述べたとおり、Viewは「ModelとControllerへの参照をもちます」。それらは、コンストラクタメソッド(スクリプト003第5〜8行目)の引数として受取ります。ただ、Controllerをまだ定義していませんので、それはあとで加えることにします。渡されたModelのClockModelインスタンスは、プロパティ(clockData)に設定します。メソッドupdate()は、Modelに登録するイベントリスナーです。けれど、具体的な処理はサブクラスでオーバーライドすることとして、このクラスでは空にしました(第9行目)。

そして、ClockViewを継承したデジタル時計のクラスDigitalClockViewが以下のスクリプト004のように定義されます。コンストラクタメソッド(スクリプト004第7〜12行目)は、ModelのClockModelインスタンスを引数に受取り、superステートメントによりスーパークラスのコンストラクタに渡されます。そして、ClockModelインスタンス(継承したプロパティclockData)のEvent.CHANGEイベントに、リスナーとして後述update()メソッドを登録します。さらに、後述createClock()メソッドでTextFieldインスタンスをタイムラインに置いたうえで、update()メソッドの呼出しにより現在時刻を表示します。

メソッドcreateClock()は、TextFieldインスタンスを生成して、タイムラインに表示します(スクリプト004第17〜23行目)。インスタンスは、選択(TextField.selectableプロパティ)が不可(false)で幅の設定(TextField.autoSizeプロパティ)を自動(定数TextFieldAutoSize.LEFT)としました。そして、インスタンスをプロパティ(clock)に納めたうえで、DisplayObjectContainer.addChild()メソッドにより自身の表示リストに加えます。

リスナーメソッドupdate()は、スーパークラスClockViewのメソッドをoverride属性キーワードによりオーバーライドしています(スクリプト004第13〜16行目)。プロパティclockDataのClockModelインスタンスからgetアクセサメソッドtimeによりTimeインスタンスを得て、後掲setTime()メソッドの引数に渡すことにより、時刻のデータTextFieldインスタンス(プロパティclock)に設定します。

スクリプト004■デジタル時計のViewを表すクラスDigitalClockView
    // ActionScript 3.0クラス定義ファイル: DigitalClockView.as
    // デジタル時計の表示
  1. package {
  2.   import flash.text.TextField;
  3.   import flash.text.TextFieldAutoSize;
  4.   import flash.events.Event;
  5.   public class DigitalClockView extends ClockView {
  6.     var clock:TextField;
  7.     public function DigitalClockView(myClock:ClockModel) {
  8.       super(myClock);
  9.       clockData.addEventListener(Event.CHANGE, update);
  10.       createClock();
  11.       update(null);
  12.     }
  13.     override protected function update(eventObject:Event):void {
  14.       var myTime:Time = clockData.time;
  15.       setTime(myTime);
  16.     }
  17.     private function createClock():void {
  18.       var _txt:TextField = new TextField();
  19.       _txt.selectable = false;
  20.       _txt.autoSize = TextFieldAutoSize.LEFT;
  21.       clock = _txt;
  22.       addChild(clock);
  23.     }
  24.     private function setTime(myTime:Time):void {
  25.       var hour_str:String = getTwoDigits(myTime.hours);
  26.       var minute_str:String = getTwoDigits(myTime.minutes);
  27.       var seconds_str:String = getTwoDigits(myTime.seconds);
  28.       clock.text = hour_str + ":" + minute_str + ":" + seconds_str;
  29.     }
  30.     private static function getTwoDigits(n:uint):String {
  31.       var return_str:String = String(n);
  32.       if (n < 10) {
  33.         return_str = "0" + return_str;
  34.       }
  35.       return return_str;
  36.     }
  37.   }
  38. }

メソッドsetTime()は、引数に渡されたTimeインスタンスから時分秒の値を取出し、「時:分:秒」というかたちの文字列にしてTextFieldインスタンス(プロパティclock)に設定します(スクリプト004第24〜29行目)。それぞれの数値は、メソッドgetTwoDigits()の引数に渡すことで数字ふた桁の文字列に変えています(第30〜36行目)。

さて、ClockModelインスタンスは、1,000ミリ秒間隔でEvent.CHANGEのリスナーにイベントを配信しました(スクリプト002)。したがって、このDigitalClockViewインスタンスは、1,000ミリ秒ごとにTextFieldインスタンス(プロパティclock)の時刻を書替えることになります。

上記スクリプト001から004までのActionScript(AS)ファイルと同じ場所に保存したFlashムービー(FLA)ファイルのメインタイムラインにつぎのフレームアクションを書けば、ステージに時刻が表れて毎秒数字が変わります(図002)

// フレームアクション
var model:ClockModel = new ClockModel();
var digitalView:ClockView = new DigitalClockView(model);
addChild(digitalView);

図002■フレームアクションでステージにデジタル時計を表示する
図002上図
図002下図


04 アナログ時計のView
つぎに、アナログ時計のViewを加えます(スクリプト005)。コンストラクタメソッド(第9〜14行目)とupdate()メソッド(第15〜18行目)は、デジタル時計のDigitalClockViewと同じです。

createClock()メソッド(スクリプト005第19〜26行目)は、createHand()メソッド(第32〜38行目)の呼出しによりつくられた時分秒の針のSpriteインスタンスを、自身の表示リストに加えます。そして、メソッドsetTime()は、引数に渡されたTimeインスタンスから時分秒の値を取出し、各針の角度を時刻に合わせます(第27〜31行目)。

スクリプト005■アナログ時計のViewを表すクラスAnalogClockView
    // ActionScript 3.0クラス定義ファイル: AnalogClockView.as
    // アナログ時計の表示
  1. package {
  2.   import flash.display.Sprite;
  3.   import flash.events.Event;
  4.   import flash.display.Graphics;
  5.   public class AnalogClockView extends ClockView {
  6.     var hour:Sprite;
  7.     var minute:Sprite;
  8.     var second:Sprite;
  9.     public function AnalogClockView(myClock:ClockModel) {
  10.       super(myClock);
  11.       clockData.addEventListener(Event.CHANGE, update);
  12.       createClock();
  13.       update(null);
  14.     }
  15.     override protected function update(eventObject:Event):void {
  16.       var myTime:Time = clockData.time;
  17.       setTime(myTime);
  18.     }
  19.     private function createClock():void {
  20.       hour = createHand(5, 50);
  21.       minute = createHand(2, 80);
  22.       second = createHand(0, 85);
  23.       addChild(hour);
  24.       addChild(minute);
  25.       addChild(second);
  26.     }
  27.     private function setTime(myTime:Time):void {
  28.       hour.rotation = myTime.hours * 30 + myTime.minutes / 2;
  29.       minute.rotation = myTime.minutes * 6;
  30.       second.rotation = myTime.seconds * 6;
  31.     }
  32.     private static function createHand(thickness:Number, length:Number):Sprite {
  33.       var hand:Sprite = new Sprite();
  34.       var myGraphics:Graphics = hand.graphics;
  35.       myGraphics.lineStyle(thickness, 0x0);
  36.       myGraphics.lineTo(0, -length);
  37.       return hand;
  38.     }
  39.   }
  40. }

では、デジタル時計と併せてアナログ時計も表示してみましょう。上記スクリプト001から005までのActionScript(AS)ファイルと同じ場所に保存したFlashムービー(FLA)ファイルのメインタイムラインに、つぎのフレームアクション(スクリプト006)を書けば、ステージ左上角に時刻が表れて、中央にアナログ時計の針が示されます(図003)

スクリプト006■デジタルとアナログのふたつの時計を表示するフレームアクション
    // フレームアクション
    // メインタイムライン
  1. var model:ClockModel = new ClockModel();
  2. var digitalView:ClockView = new DigitalClockView(model);
  3. var analogView:ClockView = new AnalogClockView(model);
  4. addChild(digitalView);
  5. addChild(analogView);
  6. analogView.x = stage.stageWidth / 2;
  7. analogView.y = stage.stageHeight / 2;

図003■フレームアクションでステージにデジタルとアナログのふたつの時計を表示する
図003上図
図003下図


05 ストップウォッチのModel
いよいよControllerが加わったサンプルをつくります。ユーザーインタラクションに応じて、Model-View-Controllerが連携して処理を行います(図004)。まず、ユーザーの操作をインターフェイスのViewが受取ります。つぎに、Viewがその操作内容をControllerに伝えます。そして、Controllerは操作の情報にもとづいて、Modelにデータの書替えを求めます。Modelはデータを変えると、それをViewに知らせます。

図004■ユーザーの操作に応じたModel-View-Controllerの処理の流れ
図004

もっとも、時計ではユーザーインタラクションの余地がほとんどありません。そこで、ストップウォッチに変えましょう。ただし、ModelのクラスClockModelに直接手は加えません。ClockModelクラスのサブクラスStopWatchModelを新たに定義します。そのStopWatchModelクラスに、追加や修正を加えることにします(スクリプト007)。

コンストラクタメソッドは空ですので(スクリプト007第8行目)、スーパークラスClockModelのコンストラクタメソッドによりTimerインスタンスにイベントリスナーが登録され、メソッドinitialize()が呼出されます。メソッドinitialize()は、オーバーライドしています(スクリプト007第9〜11行目)。ストップウォッチが、直ちに時を刻む訳にはいかないからです。替わりに、時刻を0:0:0にリセットするreset()メソッドが呼出されます。

メソッドreset()は、stop()メソッドを呼出し、スーパークラスのsetアクセサメソッドtimeに0:0:0の時間を設定します(スクリプト007第47〜50行目)。stop()メソッド(第42〜46行目)は、まず計測時間をスーパークラスのプロパティ(_time)に納めます[*2]。つぎに、計測開始ミリ秒が納められるプロパティ(startTime)に、停止中の意味となる-1を代入します。そして、スーパークラスのTimerインスタンス(プロパティ_timer)に対して、Timer.stop()メソッドを呼出します。

メソッドstart()はオーバーライドしました(スクリプト007第38〜41行目)。時間を計り始めますので、その開始ミリ秒をプロパティ(startTime)に取っておく必要があるからです。そのうえで、スーパークラスClockModelのstart()メソッドを呼出します。

オーバーライドしたgetTime()メソッドは、計測時間をTimeインスタンスで返します(スクリプト007第51〜61行目)。プロパティ(_time)に納められた直近の計測開始時間を取出し、計測中の(startTimeプロパティの値が-1でない)場合は経過ミリ秒数を加えて、戻り値のTimeインスタンスがつくられます。

スクリプト007■ストップウォッチのテータを扱うModelとなるクラスStopWatchModel
    // クラス定義ファイル: StopWatchModel.as
    // ストップウォッチのテータを扱うModel
  1. package {
  2.   import flash.events.Event;
  3.   import flash.utils.getTimer;
  4.   public class StopWatchModel extends ClockModel {
  5.     private var startTime:int = -1;
  6.     private var _states:Vector.<String> = new <String>["start", "stop", "reset"];
  7.     private var _state:uint = 0;
  8.     public function StopWatchModel() {}
  9.     override protected function initialize():void {
  10.       reset();
  11.     }
  12.     internal function get states():Vector.<String > {
  13.       var myStates:Vector.<String > = new Vector.<String>();
  14.       var nLength:uint = _states.length;
  15.       for (var i:uint = 0; i < nLength; i++) {
  16.         myStates[i] = _states[i];
  17.       }
  18.       return myStates;
  19.     }
  20.     internal function get state():uint {
  21.       return _state;
  22.     }
  23.     internal function changeState():void {
  24.       switch (_state) {
  25.         case 0 :
  26.           start();
  27.           break;
  28.         case 1 :
  29.           stop();
  30.           break;
  31.         case 2 :
  32.           reset();
  33.           break;
  34.       }
  35.       _state = (++_state) % 3;
  36.       dispatchEvent(new Event(Event.SELECT));
  37.     }
  38.     override public function start():void {
  39.       startTime = getTimer();
  40.       super.start();
  41.     }
  42.     public function stop():void {
  43.       _time = getTime();
  44.       startTime = -1;
  45.       _timer.stop();
  46.     }
  47.     public function reset():void {
  48.       stop();
  49.       time = new Time(0, 0, 0);
  50.     }
  51.     override protected function getTime():Time {
  52.       var myDate:Date = new Date();
  53.       myDate.hours = _time.hours;
  54.       myDate.minutes = _time.minutes;
  55.       myDate.seconds = _time.seconds;
  56.       if (startTime > -1) {
  57.         myDate.milliseconds = getTimer() - startTime;
  58.       }
  59.       var myTime:Time = new Time(myDate.hours, myDate.minutes, myDate.seconds);
  60.       return myTime;
  61.     }
  62.   }
  63. }

ストップウォッチは、ボタンひとつのインターフェイスで操作できます。ボタンをクリックするたびに、計測の開始・停止・リセットの機能が働けばよいからです。その3つのいずれの役割(モード)かは、整数型のプロパティ(_state)にそれぞれ0〜2の整数でもたせます(スクリプト007第7行目)。外からは読取り専用にするため、getアクセサメソッド(state)のみ設けました(第20〜22行目)。

現在の機能を果たし、つぎのモードに切替えるのがメソッドchangeState()です(スクリプト第23〜34行目)。プロパティ(_state)の値(0〜2)に応じて、start()、stop()、reset()の各メソッドを呼出します。そして、つぎのモードに切替わったたことを、Event.SELECTイベントで伝えます。

なお、Viewに現在のモードを表示するための文字列("start"、"stop"、"reset")も配列のプロパティ(_states)で用意しました(スクリプト007第6行目)。配列インデックスは、3つのモードを示すプロパティ(_state)の値に対応します。getアクセサメソッド(states)で、複製した配列が得られます(第12〜19行目)。

[*2] reset()メソッドはスーパークラスのsetアクセサメソッドtimeに時間を設定する(スクリプト007第49行目)のに対して、stop()メソッドはプロパティ_timeにTimeオブジェクトを代入しています(第43行目)。これは、前者がリセットした時間で表示(View)を改めるように、イベントを配信するためです(ClockModelクラスのsetアクセサメソッドtimeは、スクリプト002第20行目でupdate()メソッドを呼出します)。


06 ボタンのViewとController
ユーザーインターフェイスとなるボタンのViewとしてクラスToggleButtonViewを定義します。ボタンはクリックするたびに、ストップウォッチの計測開始、停止、リセットと機能を切替えます(図005)。もっとも、ViewはただユーザーのクリックをControllerに伝えるだけです。すると、Controllerはその知らせにもとづき、モードの切替えをModelに求めます。Modelはモードを切替え、必要なデータを改めたうえで、それをViewに知らせます。そこで、Viewは新たなデータをModelから得て、表示も改めます(前掲図004「ユーザーの操作に応じたModel-View-Controllerの処理の流れ」参照)。

図005■ストップウォッチのstart/stop/resetボタン
図005

クラスToggleButtonViewも、他のViewと同じく、クラスClockViewを継承します(後掲スクリプト009)。つまり、ClockModelインスタンスへの参照をもち、イベントリスナーとすべきメソッドupdate()を定義(オーバーライド)します(前掲スクリプト003)。ただし、登録するイベントはStopWatchModelクラスがモード変更したときのEvent.SELECTです(前掲スクリプト007第36行目)。

前述のとおりクラスToggleButtonViewは、ユーザーのクリックをControllerに伝えます。そのために、Controllerの参照をもつ必要が生じます(前掲図001参照)。しかし、Viewの基本を定めるクラスClockView(スクリプト003)は、コンストラクタメソッドの引数にModelしか定めていません。そこで、コンストラクタの第2引数にControllerを加えます(スクリプト008)。ただし、時計のViewを表すクラスDigitalClockViewやAnalogClockViewは、コンストラクタの引数にControllerが要りません。そこで、引数のデフォルト値をnullとしました(第6行目)。

スクリプト008■時計のViewの基本を定めるクラスClockViewのコンストラクタにControllerの引数追加
    // ActionScript 3.0クラス定義ファイル: ClockView.as
    // 時計のViewの基本を定める
  1. package {
  2.   import flash.display.Sprite;
  3.   import flash.events.Event;
  4.   public class ClockView extends Sprite {
  5.     protected var clockData:ClockModel;
  6.     public function ClockView(myClock:ClockModel, myController:ClockController = null) {
  7.       clockData = myClock;
  8.     }
  9.     protected function update(eventObject:Event):void {}
  10.   }
  11. }

クラスToggleButtonViewは、ユーザークリックをControllerに伝え、Modelからモードが変わったことを知らされます。ステージに表示するのはボタンのインスタンスがひとつです(スクリプト009)。ボタンにはコンポーネントを使いますので、Flashムービー(FLA)ファイルの[ライブラリ]には予めButtonコンポーネントを納めておきます(図006)。

図006■[コンポーネント]パネルからButtonコンポーネントを[ライブラリ]に納める
図006左図 図006右図

まず、コンストラクタメソッド(スクリプト009第9〜18行目)が、引数にStopWatchModelとClockControllerのインスタンスを受取ります。StopWatchModelインスタンスはスーパークラスClockViewに渡し、ClockControllerインスタンスをプロパティ(controller)にもちます。つぎに、StopWatchModelインスタンスのモードが変わったときのイベントEvent.SELECTに、リスナーメソッドupdate()を登録します。

そして、Buttonインスタンスを表示リストに加えたうえで、MouseEvent.CLICKイベントにリスナーメソッドonClickを登録しています。最後に、update()メソッドで表示を更新します。なお、StopWatchModelクラスのgetアクセサメソッドstatesからは、ボタンに表示するラベルが納められたVectorオブジェクトを得て、同名のプロパティに納めました。

StopWatchModelインスタンスのモードが変わると、オーバーライドしたリスナーメソッドupdate()が呼出されます(スクリプト009第19〜21行目)。すると、StopWatchModelインスタンスのgetアクセサメソッドstateで現在のモードを整数で受取り、文字列のVectorオブジェクト(states)から対応するラベルを取出して、Button.labelプロパティに設定して表示します。

ボタンがクリックされたときのリスナーメソッドonClick()は、ClockControllerインスタンス(controller)のコールバックメソッドupdate()を呼出します(スクリプト009第22〜24行目)。

スクリプト009■ストップウォッチのボタンを表すViewとなるクラスToggleButtonView
    // クラス定義ファイル: ToggleButtonView.as
    // ストップウォッチのボタンを表すView
  1. package {
  2.   import flash.events.Event;
  3.   import fl.controls.Button;
  4.   import flash.events.MouseEvent;
  5.   public class ToggleButtonView extends ClockView {
  6.     private var controller:ClockController;
  7.     private var toggleButton:Button;
  8.     private var states:Vector.<String > ;
  9.     public function ToggleButtonView(myClock:StopWatchModel, myController:ClockController) {
  10.       super(myClock);
  11.       clockData.addEventListener(Event.SELECT, update);
  12.       controller = myController;
  13.       states = myClock.states;
  14.       toggleButton = new Button();
  15.       addChild(toggleButton);
  16.       toggleButton.addEventListener(MouseEvent.CLICK, onClick);
  17.       update(null);
  18.     }
  19.     override protected function update(eventObject:Event):void {
  20.       toggleButton.label = states[StopWatchModel(clockData).state];
  21.     }
  22.     private function onClick(eventObject:MouseEvent) {
  23.       controller.update();
  24.     }
  25.   }
  26. }

ToggleButtonViewクラスがひとつのボタンしかもちませんので、ControllerのクラスClockControllerもいたって簡素です(スクリプト010)。コンストラクタメソッドは、引数に受取ったStopWatchModelインスタンスの参照をプロパティ(clockData)にもちます(第5〜7行目)。

そして、ボタンのクリックでToggleButtonViewクラスから呼ばれるコールバックメソッドupdate()は、StopWatchModelインスタンス(clockData)のメソッドchangeState()の呼出しにより、モード変更を求めます(スクリプト010第8〜10行目)。

スクリプト010■ストップウォッチのボタン操作をModelに伝えるControllerのクラスClockController
    // クラス定義ファイル: ClockController.as
    // ストップウォッチのボタン操作をModelに伝えるController
  1. package {
  2.   import flash.events.Event;
  3.   public class ClockController {
  4.     protected var clockData:StopWatchModel;
  5.     public function ClockController(myClock:StopWatchModel) {
  6.       clockData = myClock;
  7.     }
  8.     internal function update():void {
  9.       clockData.changeState();
  10.     }
  11.   }
  12. }

これでストップウォッチのModel-View-Controllerのクラスが揃いました。つぎのフレームアクションはタイムラインにストップウォッチとボタンを表示します(スクリプト011)。ボタンをクリックすると、ストップウォッチの開始・停止・リセットを操作できます(図007)。

スクリプト011■ストップウォッチとボタンを表示するフレームアクション
    // フレームアクション
    // メインタイムライン
  1. var model:StopWatchModel = new StopWatchModel();
  2. var controller:ClockController = new ClockController(model);
  3. var digitalView:ClockView = new DigitalClockView(model);
  4. var analogView:ClockView = new AnalogClockView(model);
  5. var toggleButton:ToggleButtonView = new ToggleButtonView(model, controller);
  6. addChild(digitalView);
  7. addChild(analogView);
  8. addChild(toggleButton);
  9. analogView.x = stage.stageWidth / 2;
  10. analogView.y = stage.stageHeight / 2;
  11. toggleButton.x = stage.stageWidth - toggleButton.width;

図007■ストップウォッチをボタンで開始・停止・リセットできる
図007上図
図007左下図 図007右下図

時計という簡単なお題で、Model-View-Controllerの仕組みをご紹介しました。Modelはデータの管理と処理、変更に専念し、そのデータがどのように使われ、表示されるかに意を用いません。イベントリスナーに対して、必要なデータの更新を知らせるのみです。

そのため、Viewは好きな使い途に合わせて、表現が自由に選べます。他方で、Modelのデータを参照するだけで、変更は直接行いません。ユーザー操作はControllerに伝え、Modelのデータが更新されたのを受けて表示に反映します。

Controllerは、ユーザー操作をViewから知らされます。その内容に応じてModelにアクセスし、データの変更や処理を求めます。このように3つの構成要素の役割を分けることで、使い回しのしやすいスクリプティングができるのです。

[参考文献] William B. Sanders/Chandima Cumaranatunge『ActionScript 3.0 Design Patterns』、Joey Lott/Danny Patterson『Advanced ActionScript 3 with Design Patterns』(邦訳書: 中尾真二(監訳)『ActionScript 3.0 : デザインパターン』)。


作成者: 野中文雄
作成日: 2010年12月6日


Copyright © 2001-2010 Fumio Nonaka.  All rights reserved.