2011年1月7日金曜日

Flash IDE 上でのMVCについて考えてみた(その2)

GUIコンポーネントは入れ子の構造となり、また各コンポーネントは MVC のすべての役割をもつため、MVC という枠組みだけでは対応できないと考え、前回の内容を変更しました。

今回作成したサンプル
  • TextArea に最初 0 が表示され、マウスでクリックもしくは Enter キーを押すごとに値が 1 増えるアプリです。

シンボル
  • LIBRARY 内のフォルダ名
    • com.objectfanatics.samplecomponent
  • シンボル名
    • Component
      • リンク先のクラス
        • com.objectfanatics.samplecomponent.Component
クラス構成
  • com.objectfanatics.samplecomponent パッケージ
    • com.objectfanatics.samplecomponent.Component
      • Flash IDE 上で作成したシンボルからリンクされるクラス。
      • コンポーネント全体を表すクラス。
  • com.objectfanatics.samplecomponent.model パッケージ
    • com.objectfanatics.samplecomponent.model.Model
      • コンポーネントのモデルのインタフェース。
      • モデルは可能な限り実装を意識しないで設計したいため、あえてインタフェースを作成。
    • com.objectfanatics.samplecomponent.model.ModelImpl
      • コンポーネントのモデルの実装。
    • com.objectfanatics.samplecomponent.model.ModelListener
      • Model の変更内容を監視するためのリスナーインタフェース
  • com.objectfanatics.samplecomponent.view パッケージ
    • com.objectfanatics.samplecomponent.view.ModelListenerImpl
      • Model の変更を監視し、Model の変更内容に応じて View を更新するクラス。
      • ModelListenerインタフェースの実装クラス。
    • com.objectfanatics.samplecomponent.view.ViewInitializer
      • View を初期化するためのクラス。
  • com.objectfanatics.samplecomponent.controller パッケージ
    • com.objectfanatics.samplecomponent.controller.Controller
      • Controller の Model 寄りの処理を行なう。
    • com.objectfanatics.samplecomponent.controller.ViewAdapter
      • Controller の Component 寄りの処理を行なう。
      • Component の内部要素に対してリスナーを登録し、イベントを監視する。
      • Component からのイベントを処理し、必要に応じて Model を更新する。
ソース
package com.objectfanatics.samplecomponent.component {

import com.objectfanatics.samplecomponent.controller.Controller;
import com.objectfanatics.samplecomponent.controller.ViewAdapter;
import com.objectfanatics.samplecomponent.model.Model;
import com.objectfanatics.samplecomponent.model.ModelImpl;
import com.objectfanatics.samplecomponent.view.ModelListenerImpl;
import com.objectfanatics.samplecomponent.view.ViewInitializer;

import flash.display.Sprite;
import flash.text.TextField;

/**
 * FlashIDE 上の LIBRARY にて、フォルダ名が com.objectfanatics.samplecomponent、名前が Component のシンボルに対応するクラスです。
 *
 * このサンプルでは、GUIコンポーネントという単位を扱い、このクラスが1つの GUI コンポーネントを表しています。
 *
 * このコンポーネントの内部には TextField などの GUI コンポーネントが内包(FlashIDE上で定義されたシンボルのインスタンスやActionScriptで動的に作成されたものなど)
 * されており、それはこのコンポーネントそのものの Model, View, Controller のいずれでもないと考えます。
 *
 * そのため、このコンポーネントは Model, View, Controller だけでなく、自身を表す Component とその内部の Sub Components で表されると考えます。
 *
 * View については、Model の内容を常に反映しながら情報を外部に提供するという MVC で言うところの View は、オブジェクトとしては存在しないと考えます。
 * なぜなら、この役割は自身を表す Component もしくはその内部の Sub Components によって達成されるからです。
 *
 * 自身を表す Component とその内部の Sub Components が View に該当するかというと、View としての性質を持っていますが、同時に Model でもあり Controller でも
 * あるため、View と表現するのは妥当ではないと考えます。そのため、自身を表す Component とその内部の Sub Components という表現をしています。
 *
 */
public class Component extends Sprite {

    /**
     * numberText sub component.
     */
    public var numberText:TextField;

    public function Component ():void
    {
        // Model を初期化します
        var model:Model = new ModelImpl ();

        // Model の変更内容を監視し Component に反映させるための ModelListener を初期化します
        model.modelListener = new ModelListenerImpl (this);

        // Controller を初期化します
        var controller:Controller = new Controller (this, model);

        // Component もしくは Sub Components からの入力を監視し Controller に伝える処理を行うクラスを初期化します
        new ViewAdapter (this, controller);

        // View を初期化します
        new ViewInitializer (this, model);
    }
}
}

package com.objectfanatics.samplecomponent.controller {

import com.objectfanatics.samplecomponent.component.Component;
import com.objectfanatics.samplecomponent.model.Model;

/**
 * Controller クラスです.
 *
 * Component の監視処理の記述は ViewAdapter クラスが行っています。
 *
 * Controller 自身が Component や Sub Component の監視を行う場合、複数のコンポーネント上で
 * ボタンの監視やキーが押されることが、Model の値をインクリメントさせるトリガに成り得ます。
 * このような処理を Controller の Component 寄りの処理と言うことにします。
 *
 * しかし、どのボタンが押されてもキーが押されても Controller としては Model の値をインクリメント
 * して欲しいというイベントであると考えられます。このような考えに基づく処理を Controller の
 * Model 寄りの処理と言うことにします。
 *
 * そう考えると、Controller の Model 寄りの処理を Controller が、Component 寄りの処理を
 * ViewAdapter が行っていると言うことができます。
 */
public class Controller {

    private var model:Model;
    private var component:Component;

    public function Controller (component:Component, model:Model):void
    {
        this.component = component;
        this.model = model;
    }

    /**
     * Model の値をインクリメントします.
     */
    public function increment ():void
    {
        model.increment ();
    }
}
}

package com.objectfanatics.samplecomponent.controller {

import com.objectfanatics.samplecomponent.component.Component;

import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;

/**
 * Controller の Component 寄りの処理を受け持つクラスです.
 *
 * Component もしくは Sub Components からの入力を監視し Controller に伝える処理を行います.
 */
public class ViewAdapter {

    private var controller:Controller;

    public function ViewAdapter (component:Component, controller:Controller):void
    {
        // Controller の格納
        this.controller = controller;

        // Component のクリックの検出設定
        component.addEventListener (MouseEvent.CLICK, onClicked);

        // Enter キーの検出設定
        component.addEventListener (KeyboardEvent.KEY_UP, onKeyUp);
    }

    private function onClicked (e:MouseEvent):void
    {
        controller.increment ();
    }

    private function onKeyUp(e:KeyboardEvent):void {
        if (e.keyCode == Keyboard.ENTER) {
            controller.increment ();
        }
    }
}
}

package com.objectfanatics.samplecomponent.model {

/**
 * Model インタフェースです.
 */
public interface Model {

    /**
     * ModelListener をセットします。
     * @param modelListener ModelListener
     */
    function set modelListener (modelListener:ModelListener):void;

    /**
     * 値を返します。
     * @return 値
     */
    function get number ():uint;

    /**
     * 値を1増やします。
     */
    function increment ():void;
}
}

package com.objectfanatics.samplecomponent.model {

/**
 * Model の実装クラスです.
 */
public class ModelImpl implements Model {

    private var _modelListener:ModelListener;

    /**
     * 保持される値
     */
    private var _number:uint = 0;

    /**
     * Model#get number():uint の実装
     */
    public function get number ():uint
    {
        return _number;
    }

    /**
     * Model#set modelListener(modelListener:ModelListener):void の実装
     */
    public function set modelListener (modelListener:ModelListener):void
    {
        this._modelListener = modelListener;
    }

    /**
     * Model#increment():void の実装
     */
    public function increment ():void
    {
        this._modelListener.numberChanged (++_number);
    }
}
}

package com.objectfanatics.samplecomponent.model {

/**
 * Model の変更内容を監視するためのリスナー
 */
public interface ModelListener {

    /**
     * 値が変更された場合に呼び出されます。
     *
     * @param number 変更後の値
     */
    function numberChanged (number:uint);
}
}

package com.objectfanatics.samplecomponent.view {

import com.objectfanatics.samplecomponent.component.Component;
import com.objectfanatics.samplecomponent.model.ModelListener;

/**
 * Model の変更内容を監視し Component に反映させるための ModelListener の実装クラス。
 */
public class ModelListenerImpl implements ModelListener {

    /**
     * 変更対象の Component
     */
    private var component:Component;

    /**
     * コンストラクタ
     * @param view 変更対象の View
     */
    public function ModelListenerImpl (component:Component):void
    {
        this.component = component;
    }

    /**
     * Model の値が変更された時に呼び出され、View 上の表示を変更します。
     * @param currentNumber 現在のの Model の値
     */
    public function numberChanged (currentNumber:uint)
    {
        component.numberText.text = "" + currentNumber;
    }
}
}

package com.objectfanatics.samplecomponent.view {

import com.objectfanatics.samplecomponent.component.Component;
import com.objectfanatics.samplecomponent.model.Model;

/**
 * View を初期化するためのクラスです.
 *
 * ここではこのコンポーネント全体の見た目を初期化するため、View ということばをあえて使用しています。
 */
public class ViewInitializer {

    /**
     * View を初期化します.
     *
     * @param component Component
     * @param model Model
     */
    public function ViewInitializer (component:Component, model:Model):void
    {
        component.numberText.text = "" + model.number;
    }
}
}

今回の所感

今回 Component として扱った箇所を前回は View として扱っていてなんとなく気持ち悪かったのですが、なんとなくすっきりした気がします。

コンポーネントを構成するクラスの数がかなり多くなっていますが、1つのコンポーネントのサブコンポーネントが数十とか数百あり全体のソースが数千行以上に及ぶようなコンポーネントをを作成するのであればこの程度の分割はメリットはあってもデメリットはないと思います。

それほど大きなコンポーネントを作成しない場合であれば、いくつかもしくはすべてを省略してもかまわないと思います。要はケースバイケースということですね。

Twinner で画面上のコンポーネントを動かしたりするような場合、処理はどこに書くのかな? その処理のトリガがGUIからの入力であればトリガの処理は ViewAdapter に記述して、実際の処理は Controller かな。Twinnerなどによる処理や ActionScript を用いてのコンポーネントの処理などが多くデザイン系の比重が大きい場合は、Controller の描画寄りの処理用として別クラスにしたほうがいいかもしれません。しかし、クラス名や責務はなんだろう。Controllerなのか?Viewなのか?

徹夜後の夕方なのでちょっとヘロヘロです(´・ω・`)

アーカイブ

2011年1月1日土曜日

asdocのエラー

asdoc を作成する際に遭遇したエラーのメモ

エラー内容
[Fatal Error] :3:6: エレメント型 "Hoge" は対応する終了タグ "</Hoge>" で終了する必要があります。
適格な形式でないテキストが検出されました。詳細については、C:\hoge\fuga\piyo\validation_errors.log を参照してください。

ログ内容
org.xml.sax.SAXParseException: エレメント型 "Hoge" は対応する終了タグ "</Hoge>" で終了する必要があります。

原因と対応方法
メソッドの /** */ で囲まれているドキュメント内に Vector.<Hoge> のような箇所があったことが原因。括弧をエスケープして対応。