3.0「Model-View-Controller」(モデル・ビュー・コントローラ)は、ユーザーにデータをインタラクティブに示すアプリケーションについて、3つの要素で構成する設計の手法です。3つの構成要素の頭文字をとってMVCモデルとか、デザインパターンのひとつとしてMVCパターンと呼ばれることもあります[*1]。データを扱うModelとその表示であるViewをはっきりと分け、ユーザーインタラクションはControllerが担います。それぞれの構成要素やその間でデザインパターンが用いられることも多く、デザインパターンよりももう少し大きなくくりで考える設計というべきでしょう(注[*1]参照)。Controller」をご参照ください。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から知らされます。データとその処理を扱う。値が変わると、結果はViewに表される。他の構成要素を直接参照はしない。値が変わったことは、Observerパターンなどによって伝える。Modelの必要なデータを、適切なかたちでユーザーに見せる。ユーザインターフェースに操作が加えられると、それをControllerに伝えることによってModelのデータや自身の表示が変わる。そのために、ModelとControllerへの参照をもつ。ユーザーの操作に対応して処理を行う。その操作は、Viewから知らされることもある。Modelを参照するので、データを変える必要があれば、Modelに伝える。その結果に応じて、Viewの表示も変わる。本稿は、時計をお題にして、Model-View-Controllerの例をご紹介します。ユーザーインタラクションのないModelとViewだけのデジタル時計をつくります。ひとつのViewとして、アナログ時計を定義します。つぎに、Controllerを加えた例です。Modelを拡張して、ストップウォッチを定義します。Viewにはユーザーインターフェイスとなる開始・停止・リセットのボタンが加わります。ControllerはViewからユーザー操作を受取って、Modelに伝えます。時計のModel時計のModelは、ひたすらデータとしての時を刻みます。時刻が変わったことは、ObserverパターンによりViewに知らせます。時計のModelとなるクラスは、時刻のデータを管理し、受渡す必要があるでしょう。そこでまず、時分秒のプロパティが備わった時刻データ用の簡単なクラスTimeを定義します(スクリプト001)。myTime;}}}ひとつ説明しておきたいのは、clone()メソッドです(スクリプト001第11〜14行目)。Timeインスタンスのやり取りで、そのまま参照を渡してしまうと、相手に書替えられてしまう恐れがあります。そのようなときこのメソッドは、プロパティが同じ新たなインスタンスを返します。つぎに、時刻のデータを扱うModelとなるクラスClockModelの定義です(後掲スクリプト002)。刻んだ時は、Observerパターンによって知らせます。そのために、EventDispatcherクラスを継承しています。プロパティやメソッドには、あとで拡張することを考えて加えてあるものもあります。アクセス制御の属性がprotectedのメソッドは、サブクラスから呼出したり、オーバーライド(再定義)するつもりです。差し当たって時計で必要な定義だけを抜出すと、つぎのとおりです。Event(Event.CHANGE));}}}コンストラクタメソッド(スクリプト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オブジェクトを得て返します。Event(Event.CHANGE));}}}今は必要のないsetアクセサメソッドtimeは、あとで使えるように定義しました(スクリプト002第18〜21行目)。渡されたTimeオブジェクトの複製をclone()メソッドで受取って、プロパティ(_time)に設定します。Modelのデータが変わりますので、update()メソッドの呼出しによりイベントをリスナーに配信します。ClockModelインスタンスに登録されたEvent.CHANGEのイベントリスナーは、(1)1,000ミリ秒間隔(スクリプト002第11行目)および(2)setアクセサメソッドtimeにTimeインスタンスが設定されたとき(第20行目)にイベントの配信を受けます。デジタル時計のViewまずはデジタル時計のViewをつくり、そのあとアナログ時計のViewを加えます。それらがともに用いるプロパティとメソッドを、基本クラスClockViewとして定義しておくことにします(スクリプト003)。このクラスのプロパティとメソッドは、Viewが基本的に備えるべきものでもあります。時刻をタイムラインに表示できるように、Spriteクラスを継承させます。{}}}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)に設定します。return_str;}}}メソッドsetTime()は、引数に渡されたTimeインスタンスから時分秒の値を取出し、「時:分:秒」というかたちの文字列にしてTextFieldインスタンス(プロパティclock)に設定します(スクリプト004第24〜29行目)。それぞれの数値は、メソッドgetTwoDigits()の引数に渡すことで数字ふた桁の文字列に変えています(第30〜36行目)。ClockModelインスタンスは、1,000ミリ秒間隔でEvent.CHANGEのリスナーにイベントを配信しました(スクリプト002)。したがって、このDigitalClockViewインスタンスは、1,000ミリ秒ごとにTextFieldインスタンス(プロパティclock)の時刻を書替えることになります。アナログ時計のViewつぎに、アナログ時計のViewを加えます(スクリプト005)。コンストラクタメソッド(第9〜14行目)とupdate()メソッド(第15〜18行目)は、デジタル時計のDigitalClockViewと同じです。createClock()メソッド(スクリプト005第19〜26行目)は、createHand()メソッド(第32〜38行目)の呼出しによりつくられた時分秒の針のSpriteインスタンスを、自身の表示リストに加えます。メソッドsetTime()は、引数に渡されたTimeインスタンスから時分秒の値を取出し、各針の角度を時刻に合わせます(第27〜31行目)。hand;}}}では、デジタル時計と併せてアナログ時計も表示してみましょう。ストップウォッチのModelいよいよControllerが加わったサンプルをつくります。ユーザーインタラクションに応じて、Model-View-Controllerが連携して処理を行います(図004)。ユーザーの操作をインターフェイスのViewが受取ります。つぎに、Viewがその操作内容をControllerに伝えます。Controllerは操作の情報にもとづいて、Modelにデータの書替えを求めます。Modelはデータを変えると、それをViewに知らせます。図004■ユーザーの操作に応じたModel-View-Controllerの処理の流れもっとも、時計ではユーザーインタラクションの余地がほとんどありません。そこで、ストップウォッチに変えましょう。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インスタンスがつくられます。myTime;}}}ストップウォッチは、ボタンひとつのインターフェイスで操作できます。ボタンをクリックするたびに、計測の開始・停止・リセットの機能が働けばよいからです。その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行目)。reset()メソッドはスーパークラスのsetアクセサメソッドtimeに時間を設定する(スクリプト007第49行目)のに対して、stop()メソッドはプロパティ_timeにTimeオブジェクトを代入しています(第43行目)。これは、前者がリセットした時間で表示(View)を改めるように、イベントを配信するためです(ClockModelクラスのsetアクセサメソッドtimeは、スクリプト002第20行目でupdate()メソッドを呼出します)。ボタンのViewとControllerユーザーインターフェイスとなるボタンのViewとしてクラスToggleButtonViewを定義します。ボタンはクリックするたびに、ストップウォッチの計測開始、停止、リセットと機能を切替えます(図005)。ViewはただユーザーのクリックをControllerに伝えるだけです。すると、Controllerはその知らせにもとづき、モードの切替えをModelに求めます。Modelはモードを切替え、必要なデータを改めたうえで、それをViewに知らせます。そこで、Viewは新たなデータをModelから得て、表示も改めます(前掲図004「ユーザーの操作に応じたModel-View-Controllerの処理の流れ」参照)。図005■ストップウォッチのstart/stop/resetボタンクラス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行目)。{}}}クラスToggleButtonViewは、ユーザークリックをControllerに伝え、Modelからモードが変わったことを知らされます。ステージに表示するのはボタンのインスタンスがひとつです(スクリプト009)。ボタンにはコンポーネントを使いますので、Flashムービー(FLA)ファイルの[ライブラリ]には予めButtonコンポーネントを納めておきます(図006)。図006■[コンポーネント]パネルからButtonコンポーネントを[ライブラリ]に納めるまず、コンストラクタメソッド(スクリプト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行目)。{controller.update();}}}ToggleButtonViewクラスがひとつのボタンしかもちませんので、ControllerのクラスClockControllerもいたって簡素です(スクリプト010)。コンストラクタメソッドは、引数に受取ったStopWatchModelインスタンスの参照をプロパティ(clockData)にもちます(第5〜7行目)。ボタンのクリックでToggleButtonViewクラスから呼ばれるコールバックメソッドupdate()は、StopWatchModelインスタンス(clockData)のメソッドchangeState()の呼出しにより、モード変更を求めます(スクリプト010第8〜10行目)。{clockData.changeState();}}}これでストップウォッチのModel-View-Controllerのクラスが揃いました。つぎのフレームアクションはタイムラインにストップウォッチとボタンを表示します(スクリプト011)。ボタンをクリックすると、ストップウォッチの開始・停止・リセットを操作できます(図007)。toggleButton.width;図007■ストップウォッチをボタンで開始・停止・リセットできる時計という簡単なお題で、Model-View-Controllerの仕組みをご紹介しました。Modelはデータの管理と処理、変更に専念し、そのデータがどのように使われ、表示されるかに意を用いません。イベントリスナーに対して、必要なデータの更新を知らせるのみです。そのため、Viewは好きな使い途に合わせて、表現が自由に選べます。他方で、Modelのデータを参照するだけで、変更は直接行いません。ユーザー操作はControllerに伝え、Modelのデータが更新されたのを受けて表示に反映します。Controllerは、ユーザー操作をViewから知らされます。その内容に応じてModelにアクセスし、データの変更や処理を求めます。このように3つの構成要素の役割を分けることで、使い回しのしやすいスクリプティングができるのです。デザインパターン』)。