2011-12-30
Robotlegsのドキュメント
RobotlegsというFlex/Flash用のMVCSフレームワークの英語ドキュメントを翻訳したものを掲載します。
https://github.com/robotlegs/robotlegs-framework/wiki/Best-Practices
掲載にあたりRobotlegsプロジェクトからの了解を頂いております。
また、この翻訳はversion 1.1.2のものとなります。現在version2の開発が進んでおり、来年早々にはドキュメントの方も新たになる予定とのことでした。
●Robotlegsとは
RobotlegsはFlash, Flex, AIRアプリケーションの開発で使える、ピュアAS3のマイクロアーキテクチャ(フレームワーク)です。Robotlegsはアプリケーションの各層を結びつけ、相互にコミュニケーションを行うことに焦点を当てています。Robotlegs は開発における一般的な問題を解決することで開発効率を上げることに努めています。Robotlegsはフレームワークに拘束することに関心を持っていないため、他のフレームワークに移植することも容易です。
このフレームワークはModel-View-Controllerのメタデザインパターンを基礎としたデフォルトの実装クラスを提供します。この実装はアプリケーションの構造と設計のための強力な手法を提供します。移植しやすく、各クラスに対して最小限の干渉しか行わないようになっています。MVCSの実装クラスを継承することで、多くの便利なメソッドやプロパティが使えるようになります。
RobotlegsのMVCS実装クラスは必ず使わなければならないものではなく、必要に応じて取捨選択することができます。Robotlegsは適切な実装クラスを含み、すぐに使い始めることができます。
●依存性の注入(Dependency Injection)
Robotlegsは依存性の注入というデザインパターンによって構成されています。簡単にいうと、依存性の注入とはインスタンス変数やプロパティにオブジェクトを供給することです。クラスのコンストラクタに変数を渡したりクラスのプロパティに値をセットするということが、依存性を注入するということです。もしAS3のコードを厳格な手続き型で書いていなければ、依存性の注入を使っている可能性が高いでしょう。Robotlegsはメタデータを使い自動的に依存性の注入を行います。これは開発者にとって扱いやすく、アプリケーションの間を関連づけるためのコード量を削減し、各クラスに必要な依存性を注入します。この依存性を全くの手動で行うことは可能ですが、フレームワークがこの責務を負うことによりエラーを減らし、より素早いコーディングを可能にします。
●Injectorを使う
Robotlegsは依存性の注入を行うためのアダプタメカニズムを提供しています。デフォルトでは、SwiftSuspenderインジェクション・リフレクションライブラリを使用して実現しています。追加的なアダプタとしてSmartyPants-IoCとSpring ActionScriptが使えます。各々特徴がありますが、Robotlegsに最適化されていることから、デフォルトのSwiftSuspenderを使用することをお勧めします。
・SwiftSuspenderアダプタの依存性の注入の書式
SwiftSuspenderは以下の3つの方法をサポートしています。
・プロパティ(フィールド)への注入
・コンストラクタへの注入
このドキュメントのために、プロパティへの注入がRobotlegsでどのように行われているのかについて考察してみます。クラスのプロパティに注入する方法には2通りあります。[Inject]タグにname属性をつける方法とつけない方法です。
[Inject] public var myDependency:Depedency; //name属性を使わない注入 [Inject(name="myNamedDependency")] public var myNamedDependency:NamedDepedency; //name属性を使った注入
依存性の注入はMediatorMapやCommandMap、そしてInjectorを直接操作することで行うことができます。MediatorMapとCommandMapもまたInjectorを利用していますが、それぞれ追加的な処理も行っています。名前からも推測できますが、MediatorMapはMediatorに必要なオブジェクトをマッピングするために使用し、CommandMapはCommandに必要なオブジェクトをマッピングするために使用します。またそれ以外にも依存性の注入が必要な場合は、Injectorクラスを使って対応づけすることができます。
・Injectorを使った依存性のマッピング
依存性の注入のためのアダプタクラスはInjectorのためのインターフェイスを実装してるため、アダプタの種類に関わらず、インターフェイスは一貫したAPIを提供します。このドキュメントでSwiftSuspenderに焦点を当てますが、他のアダプタでも同じことが言えます。
Injectorはアプリケーションで依存性の注入を行うために馬車馬のように働きます。それは、フレームワークで使用する各オブジェクトだけでなく、必要なオブジェクトを注入することができます。RemoteObjectやHTTPServiceやファクトリクラスなどどのようなクラスも例外ではありません。
以下がInjectorで提供されているの4つのマッピングメソッドです。
・MAPVALUE
mapValueメソッドは特定のクラスを特定のクラスのインスタンスと結びつけます。
//設定が必要な箇所に記述します var myClassInstance:MyClass = new MyClass(); injector.mapValue(MyClass, myClassInstance); //依存性の注入が必要な箇所です [Inject] public var myClassInstance:MyClass
MyClassのインスタンスが生成され、[Inject]タグがついているMyClass型のプロパティにインスタンスを注入します。
mapValue(whenAskedFor:Class, instantiateClass:Class, named:String = null)
MyClassインスタンスが生成され、[Inject]タグがついているMyClass型のプロパティにインスタンスを注入します。手動で生成したクラスのインスタンスをmapValueメソッドでマッピングしたため、インスタンスは自動的に依存性を注入されないということは注目に値します。Injectorを使って依存性を注入する必要があります。
injector.injectInto(myClassInstance);
これは注入できるプロパティに即時にインスタンスを注入します。
・MAPCLASS
mapClassメソッドは依存性を注入する必要のあるプロパティにインスタンスを注入します。インスタンスは注入の度に新たに生成されます。
//設定が必要な箇所に記述します injector.mapClass(MyClass, MyClass); //依存性の注入が必要が1つめの箇所です [Inject] public var myClassInstance:MyClass //依存性の注入が必要が2つめの箇所です [Inject] public var myClassInstance:MyClass
上記の例では依存性の注入の度に新たなインスタンスを生成します。
mapClass(whenAskedFor:Class, instantiateClass:Class, named:String = ""):*;
Injectorはマッピングされたオブジェクトをインスタンス化するメソッドを提供します。
injector.mapClass(MyClass, MyClass);
var myClassInstance:MyClass = injector.instantiate(MyClass);
これにより指定したポイントすべてにオブジェクトが注入されます。
・MAPSINGLETON
mapSingletonメソッドは不必要なインスタンスを生成せずに、常に単一のインスタンスを注入します。 この単一のインスタンスはフレームワークによって管理され、インスタンス化されるクラスにシングルトンであることを強要しません。
//設定が必要な箇所に記述します injector.mapSingleton(MyClass); //依存性の注入が必要な1つめの箇所です [Inject] public var myClassInstance:MyClass //依存性の注入が必要な2つめの箇所です [Inject] public var myClassInstance:MyClass
上記の例では、同一のインスタンスが注入されます。最初の要求があるまでオブジェクトはインスタンス化されません。
・MAPSINGLETONOF
mapSingletonOf(whenAskedFor:Class, useSingletonOf:Class, named:String = null)
mapSingletonOfメソッドはmapSingletonメソッドに機能的に似ています。インターフェイスをマッピングするときに便利です。
//設定が必要な箇所に記述します injector.mapSingletonOf(IMyClass, MyClass); //MyClass implements IMyClass //依存性の注入が必要が1つめの箇所です [Inject] public var myClassInstance:IMyClass //依存性の注入が必要が2つめの箇所です [Inject] public var myClassInstance:IMyClass
この注入メソッドはテストしやすいクラスを生成し、多態性を実現します。この例はサービスの例の節で解説します。
・MediatorMapクラスを使った注入マッピング
MediatorMapクラスはIMediatorMapを実装していて、メディエータとビューを関連づけるための2つのメソッドを持っています。
mapView(viewClassOrName:*, mediatorClass:Class, injectViewAs:Class = null, autoCreate:Boolean = true, autoRemove:Boolean = true):void
mapViewメソッドの第1引数には、MyAwesomeWidgetのような単純なクラス名、もしくはMyAwesomeWidgetのような完全修飾名を渡します。第2引数にはビューと共有イベントディスパッチャを仲介するメディエータのクラス名を渡します。[NEED TO PUT IN injectAsView]。最後の2つの引数にはメディエータの生成・破棄時に自動管理を行うかどうかの真偽値を渡します。
//設定が必要な箇所に記述します mediatorMap.mapView(MyAwesomeWidget, MyAwesomeWidgetMediator); //contextViewの適切な箇所に記述します var myAwesomeWidget:MyAwesomeWidget = new MyAwesomeWidget(); this.addChild(myAwesomeWidget); //the ADDED_TO_STAGE event is dispatched, which triggers the view component to be mediated
この方法はメディエータを自動生成するときに便利です。メディエータを手動生成する他、更に深い内容についてはメディエータの節で解説します。
・CommandMapクラスを使った注入マッピング
CommandMapクラスはICommandMapを実装していて、コマンドとコマンドを呼び出すためのフレームワークイベントを関連づけるためのメソッドを持っています。
mapEvent(eventType:String, commandClass:Class, eventClass:Class = null, oneshot:Boolean = false)
commandMapメソッドの第1引数にはコマンドを実行するイベントタイプを、第2引数には実行されるコマンドを渡します。これ以降はオプションで、第3引数にはイベントクラスの型を、第4引数にはコマンドを1回実行した後にマッピングを解除するかどうかの真偽値を渡します。
第3引数のイベントクラスの型は、Flashプラットフォームのイベントを型安全にします。これは型が異なりイベント名が同一の場合に起こるイベントの衝突を防ぎます。
//設定が必要な箇所に記述します commandMap.mapEvent(MyAppDataEvent.DATA_WAS_RECEIVED, MyCoolCommand, MyAppDataEvent); //フレームワークのアクターにイベントを通知します //これにより対応したコマンドを実行します dispatch(new MyAppDataEvent(MyAppDataEvent.DATA_WAS_RECEIVED, someTypedPayload))
Robotlegsの心臓部はコンテキストです。コンテキストは各層が相互にコミュニケーションをするためのメカニズムを提供します。アプリケーションは一つのコンテキストだけしか使えないということではないのですが、多くの場合一つで十分です。フラッシュプラットフォームのモジュラーアプリケーションを作るために、必要に応じてコンテキストを複数にすることができます。コンテキストは初期化、終了、中央イベントバスによるコミュニケーションの3つの機能を持っています。
package org.robotlegs.examples.bootstrap { import flash.display.DisplayObjectContainer; import org.robotlegs.base.ContextEvent; import org.robotlegs.core.IContext; import org.robotlegs.mvcs.Context; public class ExampleContext extends Context implements IContext { public function UnionChatContext(contextView:DisplayObjectContainer) { super(contextView); } override public function startup():void { //このコンテキストでは単一のコマンドとContextEvent.STARTUPを関連づけています //StartupCommandは更に他のコマンドやメディエータやサービス、モデルとも関連づけることができます commandMap.mapEvent( ContextEvent.STARTUP, StartupCommand, ContextEvent, true ); //StartupCommandを実行しアプリケーションを起動します dispatchEvent(new ContextEvent(ContextEvent.STARTUP)); } } }
●MVCSのリファレンス実装
Robotlegsはリファレンス実装により実装されています。この実装はModel-View-Comtroller(MVC)で知られる古典的なメタデザインパターンを参考に、4つ目のパターンであるサービスを追加しています。これらの層はこのドキュメント中ではコアアクター、もしくは単にアクターとも呼ばれます。
MVCSはアプリケーションのアーキテクチャの概観を提供します。十分に評価されたデザインパターンを組み合わせて実装されているので、RobotlegsのMVCS実装は一貫した方法で用いることができます。それらのアーキテクチャにより一般的な障害が取り除かれ、より重要な設計に力を注ぐことができます。
・Organization
・Decoupling
MVCSはアプリケーションを固有の機能を持った複数の層に自然な形で分離する機能を提供します。ビュー層はユーザとのインタラクションを扱います。モデル層は外部ソースから取得したデータやユーザによって生成されたデータを扱います。コントローラ層は各層の複雑なインタラクションをカプセル化するメカニズムを提供します。最後に、サービス層はリモートサービスAPIやファイルシステムなどのアプリケーションの外部とやり取りするための独立したメカニズムを提供します。
・ORGANIZATION
この分離を通して自然に組織化を行うことができます。すべてのプロジェクトではあるレベルの組織化を要求します。すべてのクラスをルートパッケージに入れるのは、小さなプロジェクトでも現実的ではありません。プロジェクトがありふれた大きさでない場合は、クラスファイルの構造の組織化を始める必要があります。プロジェクトに新たなメンバーが加わるときはより一層必要になります。RobotlegsのMVCSは4つの層に適切に分割され、組織化された構造を提供します。
・DECOUPLING
RobotlegsのMVCSはアプリケーションを4つの層に分離することを推奨します。それぞれの層は他の層から独立していますので、テストのしやすい独立したクラスやコンポーネントを作ることが容易です。テストが容易になることに加え、他のプロジェクトに移植し易くなります。例えば、他のリモートAPIと通信をするサービスクラスは多くのアプリケーションで再利用できます。分割をすることで、ソースにほとんど手を加えることなく他のプロジェクトに移植をすることができます。
このデフォルト実装はベストプラクティスの例を提供することを意味します。Robotlegsはこのやり方に開発者を拘束するつもりは決してありません。自由に作ってよいのです。もし、何かを強要することがあれば我々に教えてください。我々は常に新しい方法に関心を持っていて、Robotlgsのリポジトリに代わりの実装を追加することに関心を持っています。
RobotlegsのMVCSは一つもしくは複数のコンテキストの周りに配置されます。コンテキストは中央イベントバスを提供し、フレームワークの起動と終了を制御します。コンテキストはスコープを定義します。フレームワークのアクターはコンテキストに属し、他のコンテキストのスコープにあるアクターとコミュニケーションを行います。一つのアプリケーションで複数のコンテキストを持つことは可能で、これは外部モジュールをアプリケーションに組み込むような場合に有効です。アクターがあるコンテキストの中にいる間は、自身の属するコンテキスト内だけでコミュニケーションを行うことができます。モジュラーアプリケーションではコンテキスト間のコミュニケーションも可能です。
モジュラープログラミングはこのドキュメントではカバーしません。単一のコンテキストについて記述します。
・コントローラとコマンド
コントローラ層はコマンドクラスによって表されます。コマンドは状態を持たず、単一の処理を行うため、その生存期間は短いです。コマンドは各層とコミュニケーションするために最適で、他のコマンドやビューコンポーネントからメディエータ経由で受け取ったイベントを送ることができます。コマンドはビジネスロジックをカプセル化するのに最適な場所です。
・ビューとメディエータ
ビュー層はメディエータクラスによって表されます。メディエータを継承したクラスはビューコンポーネントとフレームワークの仲介役として使われます。メディエータはフレームワークのイベントやビューコンポーネントのイベントをイベントリスナに追加したり、ビューコンポーネントからのイベントをフレームワークに送出する役割を担います。これは開発者にアプリケーション固有のロジックをメディエータに記述することを許すことにもなりますが、うまく利用すればビューコンポーネントを特定のアプリケーションに結びつけることをさけることができ、コンポーネントの再利用を促進することができます。
・モデルとサービスとアクター
MVCSアーキテクチャではサービス層とモデル層は概念的に似ています。なぜなら共にActorクラスを継承しているからです。Actorクラスを継承したクラスはアプリケーションアーキテクチャの中で多くの機能を提供します。MVCSのコンテキストの中で、データを管理したり外部のソースとのやり取りを行うモデルやサービスを定義するためにActorを拡張します。このドキュメントではモデルとサービスのところでそれぞれ説明します。
明確にするために、このドキュメントでは4つの層に属するクラスを「フレームワークアクター」もしくは「アクター」と呼びます。MVCSのActorクラスはモデルとサービスクラスで使用されるだけなので、この呼び方と混同することはないと思います。
・モデル
モデルクラスはモデル層をカプセル化しデータのAPIを提供します。モデルはデータの操作が行われたときにイベントを送出します。モデルは一般的に移植性が高いエンティティです。
・サービス
サービスクラスは外部と通信を行うための機能を提供します。Webサービスやファイルアクセスやその他の外部環境とのやり取りを行うにはサービスクラスが適切です。サービスクラスは外部からのイベントを送出します。サービスクラスは移植性が高く、外部サービスをうまくカプセル化してくれます。
・フレームワークイベント
RobotlegsはフレームワークのアクターとFlashのネイティブなイベントを使ってコミュニケーションを行います。Flashの既存のイベントも使えますが、一般的にはカスタムイベントが使われます。RobotlegsはFlashの表示リストのイベント機構に依存していないため、イベントのバブリングをサポートしません。カスタムイベントを用いることで、強い型付けをされたデータをイベントにセットし、アクター間で受け渡しをすることができます。
イベントはフレームワークのアクター(メディエータ、サービス、モデル、コマンド)から送出されます。メディエータはフレームワークイベントを受け取る唯一のアクターです。コマンドはフレームワークイベントにより実行されます。イベントはメディエータに通知をするだけでなく、コマンドを実行します。
モデルとサービスクラスはイベントに反応すべきではありません。もし反応したら、アプリケーション固有のロジックと緊密になり可搬性と再利用性が低下します。
●コマンド
コマンドは生存期間の短い、状態を持たないオブジェクトです。生成され実行された後にすぐ廃棄されます。コマンドはフレームワークイベントによって実行され、決して他のアクターからインスタンス化され実行されることはありません。
・コマンドの役割
コマンドはコンテキストのCommandMapを通じて登録されます。CommandMapはコンテキストとコマンドで使用できます。コマンドはイベントの種類とセットで登録され、イベントに反応して実行されます。また、オプションでイベントの型とコマンドが実行された後にコマンドをコンテキストから削除するかの真偽値を設定できます。
・コマンドの実行
コマンドはメディエータやサービス、モデル、他のコマンドによって送出されたフレームワークイベントにより実行されます。コマンドがイベントの持つプロパティやデータにアクセスできるように、コマンドを実行したイベントをコマンド自身に注入するのが一般的です。
public class MyCommand extends Command { [Inject] public var event:MyCustomEvent; [Inject] public var model:MyModel; override public function execute():void { model.updateData( event.myCustomEventPayload ) } }
フレームワークイベントに応じて対応したコマンドがインスタンス化されるとき、コマンドクラスにある[Inject]メタデータタグをつけたプロパティに依存性が注入されます。更にコマンドを実行したイベントのインスタンスも注入されます。すべての依存性が注入された後に、execute()メソッドが自動的に呼び出され、コマンドの処理が実行されます。直接execute()メソッドを呼び出す必要はなく、決して行ってはいけません。これはフレームワークの仕事です。
・コマンドの連鎖
コマンドを連鎖させることもできます。
public class MyChainedCommand extends Command { [Inject] public var event:MyCustomEvent; [Inject] public var model:MyModel; override public function execute():void { model.updateData( event.myCustomEventPayload ) //UPDATED_WITH_NEW_STUFFイベントはコマンドを実行し、 //必要な場合に限りメディエータからビューの更新の通知を受け取ります。 if(event.responseNeeded) dispatch( new MyCustomEvent( MyCustomEvent.UPDATED_WITH_NEW_STUFF, model.getCalculatedResponse() ) ) } }
この方法を用いることで、多くのコマンドを必要に応じて組み合わせて連鎖させることができます。上記の例では状態を表すステートメント(event.responseNeeded)を使っています。状態を表すステートメントがない場合はコマンドは連鎖しません。これによりフレキシブルな振る舞いを記述できます。
・アプリケーションの各層の分離
コマンドは各層のアクターを分離するのにとても便利なメカニズムです。コマンドはメディエータ、モデル、サービスからは決してインスタンス化されないので、それらのクラスと結合することはありません。
・コマンドの役割
・メディエータ、モデル、サービス、他のコマンドをコンテキストで関連づける
・メディエータや他のコマンドに通知するためのイベントを送出する
・モデルやサービス、メディエータの依存性が注入されているので直接操作を行う
コマンドが直接メディエータとコミュニケーションを行うことは推奨されない。できないことはないが、双方の結合度を強めることになる。メディエータはモデルやサービスと異なりシステムイベントを受け取ることができるので、単純にコマンド送出されたイベントをメディエータが受け取るようにした方がよい。
・メディエータ
メディエータの役割はビューでのユーザのインタラクションを仲介することです。メディエータはアプリケーションやそのサブコンポーネントなど様々な粒度でその役割を果たします。
・メディエータの役割
FlashやFlexやAIRではユーザインターフェイスに関して実質的な制限はありません。それらはDataGrid、Button、Labelなどのすばらしいコンポーネントを提供しています。また、標準コンポーネントからカスタムコンポーネントや複合コンポーネントも作ったり、新たに一から作ることもできます。
ビューコンポーネントはUIを構成するコンポーネントです。ビューコンポーネントは状態や操作を扱い易いようにカプセル化されています。メディエータのためにビューコンポーネントはイベントやメソッド、プロパティを提供しています。ビューコンポーネントの振る舞いとフレームワークとの仲介役がメディエータの役割です。これはコンポーネントやサブコンポーネントのイベントを受け取ったり、メソッドやプロパティにアクセスすることも含みます。
メディエータはビューコンポーネントのイベントを受け取ったり直接APIにアクセスします。メディエータはフレームワークの他のアクターからのイベントに応じて適切なビューコンポーネントを更新します。メディエータはビューコンポーネントからのイベントに応じてフレームワークにイベントを送出します。
・メディエータのマッピング
mediatorMapメソッドで取り扱い可能なクラスであればどのクラスでもメディエータと関連づけることができます。これはメディエータやコンテキスト、コマンドクラスを含みます。
以下はメディエータのマッピングの例です。
mediatorMap.mapView( viewClassOrName, mediatorClass, injectViewAs, autoCreate, autoRemove );
・ビューコンポーネントとメディエータを自動的に関連づける
ビューコンポーネントとメディエータを関連づけるときに、メディエータのインスタンスを自動的に生成するようにできます。autoCreateのオプションをtrueにすると、コンテキストはビューコンポーネントがstageに追加されたとき(ADDED_TO_STAGEイベントの送出)に、メディエータとビューコンポーネントを自動的に関連づけます。メディエータはその後自身の役割を開始します。
・ビューコンポーネントとメディエータを手動で関連づける
ビューコンポーネントとメディエータを自動的に関連づけることができない、もしくは不適切な場合は、手動で関連づけを行います。
mediatorMap.createMediator(viewComponent);
上記の例では、この処理以前にmapView()メソッドによってビューコンポーネントとメディエータが関連づけられていると推測できます。
・メインアプリケーション(contextView)とメディエータを関連づける
メインアプリケーションとメディエータを関連づけるのが一般的なやり方です。メディエータとの関連づけが働かなかったり、既にステージに追加されていて適切なイベント(ADDED_TO_STAGE)を送出しないというのは特別なケースです。一般的に、この関連づけはContextクラスのstartup()で行われます。
override public function startup():void { mediatorMap.mapView(MediateApplicationExample, AppMediator); mediatorMap.createMediator(contextView); }
メインアプリケーションは十分に関連づけられ、フレームワークイベントを送出したり、受け取ったりすることができるようになりました。
ContextクラスにあるcontextView内でビューコンポーネントがstageに追加されたとき、デフォルトではmapViewメソッドでの設定に従ってビューコンポーネントとメディエータを関連づけます。
基本的なメディエータでは、viewComponentプロパティに関連したビューコンポーネントが注入されます。viewComponentプロパティはObject型です。たいていの場合、強く型付けされたオブジェクトの方がいいので、そうするために以下のように新たにプロパティを作り、そこにビューコンポーネントを注入するようにします。
public class GalleryLabelMediator extends Mediator implements IMediator { [Inject] public var myCustomComponent:MyCustomComponent; /** * onRegisterメソッドでビューコンポーネントからのイベントリスナーを設定するのがいいでしょう */ override public function onRegister():void { //コンテキストにフレームワークイベントのリスナーを設定します eventMap.mapListener( eventDispatcher, MyCustomEvent.DO_STUFF, handleDoStuff ); //ビューコンポーネントからのイベントのリスナーを設定します eventMap.mapListener( myCustomComponent, MyCustomEvent.DID_SOME_STUFF, handleDidSomeStuff) } protected function handleDoStuff(event:MyCustomEvent):void { //イベントにセットされた強力に型付けされたデータをビューコンポーネントにセットします //このデータによってビューコンポーネントの状態を変えます myCustomComponent.aProperty = event.payload } protected function handleDidSomeStuff(event:MyCustomEvent):void { //ビューコンポーネントからのイベントをフレームワークイベントとして送出します dispatch(event) } }
こうするとでキャストすることなしに、ビューコンポーネントのpublicなプロパティやメソッドにアクセスできるようになります。
・メディエータにイベントリスナーを追加する
イベントリスナーはメディエータの耳や目になります。フレームワーク内のコミュニケーションはFlashのイベントを使うため、関心のあるイベントに応じるためにイベントリスナーを登録します。フレームワークのイベントに加え、ビューコンポーネントからのイベントリスナーも登録します。
通常はonRegisterメソッドでイベントリスナーの登録を行います。メディエータのライフサイクルのこのフェーズでは、メディエータはフレームワークに登録され、依存性の注入が終わっています。onRegisterはMediatorを継承したクラスからオーバーライドされる必要があります。イベントリスナーやイベントハンドラも追加されるでしょう。
メディエータはEventMapクラスのmapListener()メソッドを持つクラスです。このメソッドはメディエータに追加されたイベントリスナーを登録したり、メディエータがフレームワークから削除されたときにイベントリスナーを削除したりします。イベントリスナーが登録されたまま残っているとFlash Playerのガーベッジコレクションでオブジェクトが適格にならず、メモリリークが発生する可能性があります。Flashの通常のやり方でイベントリスナーを追加することもできますが、手動で削除しなければならないことを忘れてはいけません。
・フレームワークイベントを受け取る
フレームワークのすべてのアクターはeventDispatcherプロパティを持っています。eventDispatcherはイベントを送出したり受け取ったりするメカニズムです。
eventMap.mapListener(eventDispatcher, SomeEvent.IT_IS_IMPORTANT, handleFrameworkEvent)
こうすることでメディエータはフレームワークからのSomeEvent.IT_IS_IMPORTANTを受け取り、handleFrameworkEventに制御を渡すことができます。
・フレームワークイベントを送出する
同じくらい重要なメディエータの役割は、他のアクターが関心を持つイベントをフレームワークに送出することです。それらのイベントは一般的にユーザのインタラクションによってもたらされたものです。キータイプを少なくするイベント送出を行うための便利なメソッドがあります。
dispatch(new SomeEvent(SomeEvent.YOU_WILL_WANT_THIS, myViewComponent.someData))
このイベントによって他のメディエータに通知したり、コマンドを実行したりすることができます。イベントを送出したメディエータはこのイベントを受け取る他のアクターがどのような反応をするかについて、全く関心を持っていません。単に何かが起きたという通知を行っているだけです。そして他のメディエータは送出されたイベントのリスナーを設定し、イベントに応じて適切に反応します。
・ビューコンポーネントのイベントを受け取る
メディエータはフレームワークとの仲介を行うために、ビューコンポーネントからのイベントを受け取ります。これはテキストフィールドやボタン、複雑に入り組んだコンポーネントであるかもしれません。ビューコンポーネントからのイベントはメディエータの中にあるイベントハンドラに渡されます。イベントリスナーの登録には、フレームワークイベントの場合と同様に好ましい書き方があります。
eventMap.mapListener(myMediatedViewComponent, SomeEvent.USER_DID_SOMETHING, handleUserDidSomethingEvent)
ビューコンポーネントからイベントが発生したときのメディエータの反応
・イベントが持つデータチェックする
・ビューコンポーネントの状態をチェックする
・ビューコンポーネントからの要求に応じて振る舞う
・他のアクターに何かが起きたことを知らせるためにイベントを送出する
もしビューコンポーネントからのイベントを再送する必要がある場合は、Mediatorクラスから継承したdispatchメソッドを使います。
eventMap.mapListener(myMediatedViewComponent, SomeEvent.USER_DID_SOMETHING, dispatch)
・メディエータを通じてモデルやサービスにアクセスする
疎結合にするために、メディエータはサービスやモデルのイベントリスナーを登録できます。イベントリスナーを登録することで、メディエータはそのイベントがどこから発生したかについて知る必要がなくなります。イベントは強く型付けされたデータを保持しています。複数のメディエータが一つのイベントを待ち受け、ビューコンポーネントの状態を受け取ったデータによって調整することができます。
ビューコンポーネントからメディエータを通じてサービスに直接アクセスすることは、ビューコンポーネントとサービスの密結合というリスクのない利便性を提供します。サービスはデータを保持せず、外部サービスへ接続し、結果を受け取るだけのAPIを提供します。このAPIに直接アクセスできるということは、同じことを行うだけのコマンドクラスを減らすことができます。もしサービスのAPI多くのメディエータから同じように繰り返し用いられるのであれば、このコマンドクラスにこの振る舞いを記述し、メディエータへのサービスクラスの依存性の注入を減らすことで利便性を高めることができます。
メディエータに依存性を注入する場合は、サービスやモデルが実装しているインターフェイスを通じて行うことを推奨します。この例は後述するExample Serviceを参照してください。
・他のメディエータにアクセスする
サービスやモデルのように、メディエータに他のメディエータの依存性を注入することもできますが、あまりお勧めしません。フレームワークイベントによるコミュニケーションにより疎結合にすることができるからです。
・モデル
モデルクラスはアプリケーションのデータアクセスを管理するために使用します。モデルはフレームワークの他のアクターがデータにアクセスし操作し変更するためのAPIを提供します。このデータはStringやArray、ArrayCollectionなどのネイティブの型だけでなくドメイン固有のオブジェクトやコレクションも含みます。
モデルはUserModelのような単純なモデルを意味し、またある時はUserProxyのようなプロクシを意味する。Robotlegsではこれらの命名規約は同じ目的で使われます。それはアプリケーションにデータを供給します。命名規約とは関係なく、モデルはActorの規定クラスを継承します。Actorはフレームワークの依存性だけでなく、便利なヘルパーメソッドも提供します。このドキュメントではそれらのクラスをモデルと呼びます。
・モデルの役割
モデルはアプリケーションのデータを提供するためのAPIを持っています。モデルはいわばデータの門番です。他のアクターはデータを取得するために、モデルにデータを問い合わせます。データはモデルを通して更新されるので、モデルは他のアクターにデータの更新を通知し、他のアクターはデータの変更に合わせて自身の状態を順応させます。
更に、モデルのデータへのアクセスをコントロールするために、モデルはいつものようにデータが適切な状態にあるようにコントロールします。これはデータの計算処理や、ドメイン固有のロジックを含みます。このモデルの役割は非常に重要です。モデルはアプリケーションの移植性を高めるための層です。ドメインロジックをモデルに置くことで、ビューやコントローラに重複したロジックを置く必要がなくなります。
例えば、モデルが買い物かごデータの消費税計算を行うとしましょう。コマンドがこのメソッドにアクセスしたら計算結果がメディエータに通知されます。メディエータはその値をもとにビューを更新するだけで済みます。例えばデスクトップ用アプリケーションとFlashモバイルアプリケーションの同様の計算処理が必要な場合は、メディエータやビューでその計算を行うと2つのアプリケーションで重複した記述をすることになりますので、そのような役割はモデルで行った方が効率がいいです。
・モデルを関連づける
モデルをフレームワークのアクターに注入するためのメソッドがインジェクタにあります。更にこれらのメソッドは実質的にすべてのクラスをインジェクションすることができます。
既にインスタンス化されているモデルをシングルトンとして扱う場合は以下のように記述します。
injector.mapValue(MyModelClass, myModelClassInstance)
モデルの注入毎にインスタンスを生成する場合は以下のように記述します。
injector.mapClass(MyModelClass, MyModelClass)
更に注入にインターフェイスとそれを実装したクラスを使用することもできます。
injector.mapClass(IMyModelClass, MyModelClass)
シングルトンインスタンスを注入する場合は以下のように記述します。
injector.mapSingleton(MyModelClass, MyModelClass)
上記のシングルトンに注目してください。シングルトンとして注入されるクラスは、実装としてはシングルトンではなく通常のクラスで、インジェクタによってシングルトンとして管理されます。これはデータを扱う上で重要なことがらです。
・モデルからのイベントの送出
モデルはフレームワークイベントを送出するための便利なメソッドを持っています。
dispatch( new ImportantDataEvent(ImportantDataEvent.IMPORTANT_DATA_UPDATED))
以下の理由によりイベントは送出されます。
・データが初期化され、他のアクターから利用できるようになった
・データが追加された
・データが削除された
・データが更新された
・データに関連のある何らかの状態が変更された
上記で送出されたイベントはメディエータで以下のようにリスナーを設定します。
override public function onRegister():void { eventMap.mapListener(eventDispatcher, ImportantDataEvent.IMPORTANT_DATA_UPDATED, handleImportantDataEvent, ImportantDataEvent); }
もしくは以下のようにコンテキストで関連づけられたコマンドを呼び出します。
override public function startup():void { commandMap.mapEvent(ImportantDataEvent.IMPORTANT_DATA_UPDATED, SomeCommand, ImportantDataEvent); }
・モデルにフレームワークイベントのリスナーを設定する
技術的には可能ですが推奨しません。コマンドでリスナーを設定してください。
●サービス
サービスはアプリケーションの外部のリソースにアクセスするために用いられます。
・localConnectionを通じた他のFlashアプリケーション
サービスは外部のエンティティとの通信をカプセル化し、通信の結果を管理します。
サービスとモデルはとても似ていることに気づくかもしれない。実際のところクラス名以外は同一です。なぜ2つのクラスに分けたかと言うと、2つのクラスはアプリケーション内では異なる役割を持っているからです。実装クラスは決して似ていません。2つに分割しなかったら、モデルから外部サービスとの通信を行うことになるでしょう。これはモデルが外部にアクセスしたり、結果をパースしたり、データを管理したり、データへのAPIを提供したり、サービスへのAPIを提供したりと、複数の責務を持つことになります。分割により問題を緩和することができます。
・サービスの役割
サービスは外部サービスへのAPIを提供します。サービスは一般的に状態を持ちません。サービスは外部サービスから取得したデータを自身に保持せず、他の適切なアクターで管理するようフレームワークイベントを送出します。
・サービスの関連づけ
サービスをフレームワークのアクターに注入するためのメソッドがインジェクタにあります。更にこれらのメソッドは実質的にすべてのクラスをインジェクションすることができます。
既にインスタンス化されているサービスをシングルトンとして扱う場合は以下のように記述します。
injector.mapValue(MyServiceClass, myServiceClassInstance)
サービスの注入毎にインスタンスを生成する場合は以下のように記述します。
injector.mapClass(MyServiceClass, MyServiceClass)
更に注入にインターフェイスとそれを実装したクラスを使用することもできます。
injector.mapClass(IMyServiceClass, MyServiceClass)
シングルトンインスタンスを注入する場合は以下のように記述します。
injector.mapSingleton(MyServiceClass, MyServiceClass)
上記のシングルトンに注目してください。シングルトンとして注入されるクラスは、実装としてはシングルトンではなく通常のクラスで、インジェクタによってシングルトンとして管理されます。これはデータを扱う上で重要なことがらです。
・サービスにフレームワークイベントのリスナーを設定する
技術的には可能ですが推奨しません。コマンドでリスナーを設定してください。
・サービスからのイベントの送出
サービスはフレームワークイベントを送出するための便利なメソッドを持っています。
dispatch( new ImportantServiceEvent(ImportantServiceEvent.IMPORTANT_SERVICE_EVENT))
・サービスの例
以下は画像ギャラリーデモアプリケーションのFlickrサービスクラスです。FlickrAPIのAS3ライブラリはFlickrに接続するための低レベルAPIです。この例ではFlickrのAPIを抽象化して扱い易くしています。
package org.robotlegs.demos.imagegallery.remote.services { import com.adobe.webapis.flickr.FlickrService; import com.adobe.webapis.flickr.Photo; import com.adobe.webapis.flickr.events.FlickrResultEvent; import com.adobe.webapis.flickr.methodgroups.Photos; import com.adobe.webapis.flickr.methodgroups.helpers.PhotoSearchParams; import org.robotlegs.demos.imagegallery.events.GalleryEvent; import org.robotlegs.demos.imagegallery.models.vo.Gallery; import org.robotlegs.demos.imagegallery.models.vo.GalleryImage; import org.robotlegs.mvcs.Actor; /** * このクラスはADOBEによって提供されているFlickrAPIを利用してデータを取得しています * 最初に関心のある画像を取得します * また、検索キーワードを指定して取得することもできます */ public class FlickrImageService extends Actor implements IGalleryImageService { private var service:FlickrService; private var photos:Photos; protected static const FLICKR_API_KEY:String = "516ab798392cb79523691e6dd79005c2"; protected static const FLICKR_SECRET:String = "8f7e19a3ae7a25c9"; public function FlickrImageService() { this.service = new FlickrService(FLICKR_API_KEY); } public function get searchAvailable():Boolean { return true; } public function loadGallery():void { service.addEventListener(FlickrResultEvent.INTERESTINGNESS_GET_LIST, handleSearchResult); service.interestingness.getList(null,"",20) } public function search(searchTerm:String):void { if(!this.photos) this.photos = new Photos(this.service); service.addEventListener(FlickrResultEvent.PHOTOS_SEARCH, handleSearchResult); var p:PhotoSearchParams = new PhotoSearchParams() p.text = searchTerm; p.per_page = 20; p.content_type = 1; p.media = "photo" p.sort = "date-posted-desc"; this.photos.searchWithParamHelper(p); } protected function handleSearchResult(event:FlickrResultEvent):void { this.processFlickrPhotoResults(event.data.photos.photos); } protected function processFlickrPhotoResults(results:Array):void { var gallery:Gallery = new Gallery(); for each(var flickrPhoto:Photo in results) { var photo:GalleryImage = new GalleryImage() var baseURL:String = 'http://farm' + flickrPhoto.farmId + '.static.flickr.com/' + flickrPhoto.server + '/' + flickrPhoto.id + '_' + flickrPhoto.secret; photo.thumbURL = baseURL + '_s.jpg'; photo.URL = baseURL + '.jpg'; gallery.photos.addItem( photo ); } dispatch(new GalleryEvent(GalleryEvent.GALLERY_LOADED, gallery)); } } }
FlickrImageServiceは画像ギャラリーサービスに接続するためのシンプルなインターフェイスを提供します。アプリケーションはギャラリーをロードし、検索することができます。このサービスのインターフェイスはIGalleryImageServiceで定義されています。
package org.robotlegs.demos.imagegallery.remote.services { public interface IGalleryImageService { function loadGallery():void; function search(searchTerm:String):void; function get searchAvailable():Boolean; } }
・インターフェイスの実装
インターフェイスを実装したサービスクラスを作成することで、テスト用に実装クラスを切り替えることができます。つまりFlickrImageServiceはXMLImageServiceに簡単に置き換えることができます。
package org.robotlegs.demos.imagegallery.remote.services { import mx.rpc.AsyncToken; import mx.rpc.Responder; import mx.rpc.http.HTTPService; import org.robotlegs.demos.imagegallery.events.GalleryEvent; import org.robotlegs.demos.imagegallery.models.vo.Gallery; import org.robotlegs.demos.imagegallery.models.vo.GalleryImage; import org.robotlegs.mvcs.Actor; public class XMLImageService extends Actor implements IGalleryImageService { protected static const BASE_URL:String = "assets/gallery/"; public function XMLImageService() { super(); } public function get searchAvailable():Boolean { return false; } public function loadGallery():void { var service:HTTPService = new HTTPService(); var responder:Responder = new Responder(handleServiceResult, handleServiceFault); var token:AsyncToken; service.resultFormat = "e4x"; service.url = BASE_URL+"gallery.xml"; token = service.send(); token.addResponder(responder); } public function search(searchTerm:String):void { trace("search is not available"); } protected function handleServiceResult(event:Object):void { var gallery:Gallery = new Gallery(); for each(var image:XML in event.result.image) { var photo:GalleryImage = new GalleryImage() photo.thumbURL = BASE_URL + "images/" + image.@name + '_s.jpg'; photo.URL = BASE_URL + "images/" + image.@name + '.jpg'; gallery.photos.addItem( photo ); } dispatchEvent(new GalleryEvent(GalleryEvent.GALLERY_LOADED, gallery)); } protected function handleServiceFault(event:Object):void { trace(event); } } }
XMLImageServiceクラスはFlickrImageServiceクラスと同じメソッドを提供しています。IGalleryServiceを実装しているクラスなら何でも置き換えることができます。サービスは同じフレームワークイベントを送出し、それは他のアクターからは見分けがつきません。この例では検索機能は実装されていませんが、それも簡単に実装することができます。
サービスクラスはインターフェイスを実装することを推奨します。サービスを注入されたアクターはインターフェイスのAPIを呼び出すので実装クラスには依存しません。
injector.mapSingletonOf(IGalleryImageService, FlickrImageService); [Inject] public var galleryService:IGalleryImageService
以下のようにXMLImageServiceに置き換えることも簡単です。
injector.mapSingletonOf(IGalleryImageService, XMLImageService);
このやり方を使うとフレキシブルでテストし易くなります。
・サービスでのデータの解析
上記の例ではサービスクラスはアプリケーション固有の機能ではありません。Flickrサービスは強く型付けされたPhotoオブジェクトを返し、XMLサービスはXMLを返します。双方のデータがたはアプリケーションで利用する上で最適化されていません。アプリケーションで利用できる形にすることが望ましいです。
この操作を行うのはサービスかモデルが適切です。サービスは返却されたデータが最初に返ってくる場所なので、ここ比較的よいと思います。既存のサービスが直接行うよりも何らかのファクトリクラスにその作業を任せるのもいいかもしれません。
データをアプリケーションで扱い易い形式に変換したら、フレームワークイベントにそのデータをセットして、他のアクターに通知します。
・サービスイベント
カスタムイベントがなければ、サービスは他のアクターに通知することができません。イベントにより強力に型付けされたデータを他のアクターに通知することができます。