※このエントリは BEFoundation について書かれたものです。詳しくはこちら。
今週は長文を書く時間が取れそうにないので、BEFoundation にさり気なく存在する、「階層を形成出来る EventDispatcher」について紹介します。
DisplayObject ツリーを使いこなしている皆さんならご存知だと思いますが、以下のように addEventListener の第三引数を true にすると、自分ではなく、子で発生したイベントを全て受け取る (キャプチャする、といいます) ことができるようになります。キャプチャ及びイベントフローについて詳しくは、大重さんのページや、scratchbrainさんのページを参照のこと。
// 子のイベントを受け取る (キャプチャする) sprite.addEventListener(MouseEvent.CLICK, clickCaptureHandler, true);
これはなかなか便利です。たまに AS2 をやるとこの辺りが未熟なため苦労させられます。失って初めて分かる大切さってヤツです。
便利なので、自作のクラス、特に親子関係がハッキリしているモデルクラスでもこれを使いたくなります。例えば、何か Flash や Photoshop 的なお絵描きツールを作っているとします。モデルは、ドキュメント (Document) がレイヤー群 (LayerList) を保持していて、各レイヤー (Layer) がレイヤー内に置かれたシェイプ (Circle, Square, ...) を保持していて、シェイプに位置情報があり…というような感じの構造になっています。簡単に図にすると次の通り。
+Document +- LayerList +- Layer +- Circle +- Square +- Layer +- Square
ここで、各クラスは EventDispatcher を継承しており、何かプロパティが編集される (例えば x,y 座標が動いたとか) と ModelEvent.UPDATE イベントを配信するようになっています。編集されたら編集されたフラグを立てたい (「未保存の編集がありますが終了しますか?」の確認等に使う) ので、全ての UPDATE イベントをキャプチャするために、document にイベントハンドラを仕掛けます。
// 全ての UPDATE イベントをキャプチャ
document.addEventListener(ModelEvent.UPDATE, documentUpdateHandler, true);
private function documentUpdateHandler(e:ModelEvent):void
{
// 編集フラグ立てる
isEdited = true;
}
が、しかし!!これはうまくいきません。なぜか EventDispatcher 単体では DisplayObject ツリーのように階層構造を形成出来ないため、子のイベントをキャプチャする (あとバブリングも) ことが不可能です。
何でこんな仕様になっているだんろう…。と思いつつも、これを打破するために作ったのが org.libspark.events.HierarchicalEventDispatcher です。名前の通り、階層を形成できる EventDispatcher です。使い方は簡単で、普通の EventDispatcher と同じように継承し (委譲でも可)、parentEventDispatcher プロパティに、「親の EventDispatcher」を設定します。
// 先ほどの Document の例
public class Document extends HierarchicalEventDispatcher
{
public function Document()
{
// レイヤーリスト生成
layerList = new LayerList();
// レイヤーリストの親は自分!
layerList.parentEventDispatcher = this;
}
// プロパティなど省略
}
// LayerList も HierarchicalEventDispatcher を継承している…
publci class LayerList extends HierarchicalEventDispatcher
{
// 省略
}
こうすると、Document (親) <- LayerList (子) という親子関係が形成されます。この下にぶら下がるであろうレイヤーやシェイプについても同様にします。こうすると、さきほどのイベントのキャプチャがうまく動くようになります。
一点だけ注意点で、発行するイベントは Event クラスではなく、必ず org.libspark.events.HierarchicalEvent か、このクラスのサブクラスのインスタンスにする必要があります。
// たとえば。Circle クラスに書かれているという想定
public function set x(value:Number):void
{
if (x == value) {
return;
}
setX(value);
dispatchEvent(new HierarchicalEvent('update'));
}
// ※本当はちゃんと継承して専用のイベントクラス作った方が良い
絶対に無きゃ無理!というほどではないかもしれませんが、先ほどの編集フラグのような処理をこのような機構なしに実現しようとするとかなり無駄が多くなると思うので、個人的には重宝しています。余談ですが、モデルクラスを作るために BEFoundation で用意されている org.libspark.model.AbstractModel と org.libspark.model.ArrayModel は HierarchicalEventDispatcher に対応しており、支援機能がついていたりします。それはまた別の機会に。ではまた!
前回、前々回と、BEFoundation (って何?という方は→こちら) 内の再利用可能なクラスについて解説しました。しかし、オブジェクト指向では、実際のコードやクラスとなっていなくても、再利用可能なものがあります。そう、設計、いわゆるデザインパターンというヤツです。ライブラリと違って、デザインパターンは文書化しないとなかなか見えない・残らないものなので、仕事で使って有効だと思ったものなど紹介していきたいと思います。
今回は、データ通信を抽象化するためのパターンです。個人的に Abstract Query パターンと呼んでいますが、名前をわざわざ付けるほどオリジナリティがあるかは微妙です。事実、実体としては Abstract Factory ほぼそのままで、考え方としては、サーバーサイドではお馴染みであろう Data Access Object が近いです。
ここで言うデータ通信とは、次のように URLLoader を使って API を叩いてデータを取ってくる、みたいな処理を言います。
// ユーザーリストを取ってくる
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, loadCompleteHandler);
loader.load(new URLRequest('userlist.php'));
仕事で Flash 開発をやっている方々ならお分かり頂けると思いますが、まずこのデータ通信というのはくせ者です。そもそも (開発が同時進行しているなどの理由で) サーバーサイドが出来ていないこともザラですし、いきなり一部の API がおかしくなってデバッグが困難になることもありますし、通信方法に変更が入る (例えば毎回、初回に取ってきたユーザーIDをリクエストに付けてくれとか) こともありますし、そりゃもう色々なことが起こります。
起こるものはしょうがないので、先回りして、通信を抽象化することでこれらの問題に簡単に対処できるようにして開発効率を上げようというのが、Abstract Query パターンの目的です。
Abstract Query パターンでは、通信が必要な際に、直接 URLLoader などを使いません。次のようにします。
var query:IQuery = queryFactory.makeGetUserlistQuery(); query.onComplete = getUserlistQueryCompleteHandler; query.load();
IQuery というのは、「リクエストを投げてデータを取ってくる」という一連の通信処理を抽象化したインターフェイスで、例えば次のようなものです。
/**
* サーバーへの問い合わせクエリ
*/
public interface IQuery
{
/**
* 完了時に呼び出される関数 (引数 = query:IQuery)
*/
function get onComplete():Function;
function set onComplete(value:Function):void;
/**
* エラー時に呼び出される関数 (引数 = query:IQuery)
*/
function get onError():Function;
function set onError(value:Function):void;
/**
* ロード中?
*/
function get isLoading():Boolean;
/**
* ロードされたデータ
*/
function get data():Object;
/**
* ロードを開始する
*/
function load():void;
/**
* ロードをキャンセルする
*/
function cancel():void;
}
続いて queryFactory というのが出てきますが、これは IQueryFactory インターフェイスを実装したクラスのインスタンスです。IQueryFactory は、次のようになっており、「ユーザー一覧を取ってくる」とか「指定した名前のユーザーを探す」とか、そういう一回のリクエストをこなしてくれる IQuery の実装クラスを生成して返すのが任務です。
/**
* サーバーへの問い合わせのファクトリ
*/
public interface IQueryFactory
{
/**
* ユーザー一覧取得クエリを作成して返す
*/
function makeGetUserlistQuery():IQuery;
/**
* 指定された名前のユーザーのユーザーID取得クエリを作成して返す
*/
function makeGetUserIdByNameQuery(name:String):IQuery;
}
余談ですが、queryFactory のインスタンスは、アプリケーションユニーク (アプリケーション内でひとつ存在すれば十分) なインスタンス群を格納する ApplicationGlobals (こいつは Singleton) みたいなクラスに入れておいて取ってきたりする想定です。
var queryFactory:IQueryFactory = ApplicationGlobals.globals.queryFactory;
IQueryFactory の実装クラスは例えば次のようになります。
/**
* 実際に通信するクエリを作る IQueryFactory の実装
*/
public class NetworkQueryFactory implements IQueryFactory
{
public function makeGetUserlistQuery():IQuery
{
return new NetworkQuery(new URLRequest('userlist.php'));
}
public function makeGetUserIdByNameQuery(name:String):IQuery
{
return new NetworkQuery(new URLRequest('finduser.php?name=' + name));
}
}
NetworkQuery というのは、中で URLLoader を使って、コンストラクタの引数として渡された URLRequest のデータを取ってくる処理を行う IQuery の実装です。
これで準備は整いました。使う時は、まずアプリケーションの先頭で IQueryFactory の実装クラスを生成して ApplicationGlobals に格納しておきます。
ApplicationGlobals.globals.queryFactory = new NetworkQueryFactory();
そして、通信が必要になった段階で、ファクトリにクエリを作ってもらって通信します。
var getUserlistQuery:IQuery = ApplicationGlobals.globals.queryFactory.makeGetUserlistQuery();
getUserlistQuery.onComplete = getUserlistQueryCompleteHandler;
getUserlistQuery.load();
// 通信が成功すると IQuery を引数としてイベントハンドラが呼び出される、という想定
protected function getUserlistQueryCompleteHandler(query:IQuery):void
{
// data で取ってきたデータが取れる、という想定。そしてきっと予め JSON なんかをパースしておいてくれている
var userlist:Array = query.data as Array;
}
これで動く理屈は分かると思います。でも、単に面倒になっただけじゃないか!…ちょっと待って下さい。ここまでは下準備で、ここからが本番です。このように通信の詳細 (どのようにデータが取ってこられるのか) を IQuery に隠蔽したことで、実際どように通信するか、ということを、IQuery のユーザ (↑で示した IQuery を使って通信を行うコードを書く人、大抵の場合あなた) には関係なく変更することができるようになります。また、どの IQuery の実装クラスを選択するか (すなわち「実際どのように通信するか」の決定) を IQueryFactory に任せたことで、このクラスを使う!というのを IQuery のユーザに関係なくごそっと変更出来るようになります。そして柔軟性が増し、開発効率が上がります。
例えば、まだサーバーサイドが全然出来ていない場合。「とっとと作れや!!後から通信処理を入れ込んでいくの大変なんじゃボケ!!!」と叫ぶ必要はもうありません。次のように、「通信したとみせかけて決められた値を返すだけのなんちゃって IQuery」を作ります。
/**
* ローカルテスト用のクエリ (単純に設定されたデータを返すだけ)
*/
public class LocalQuery implements IQuery
{
public function LocalQuery(data:Object = null)
{
setData(data);
}
private var _data:Object;
private var _onComplete:Function;
private var _onError:Function;
private var _isLoading:Boolean;
private var _intervalId:int;
public function get onComplete():Function
{
return _onComplete;
}
public function set onComplete(value:Function):void
{
_onComplete = value;
}
public function get onError():Function
{
return _onError;
}
public function set onError(value:Function):void
{
_onError = value;
}
public function get isLoading():Boolean
{
return _isLoading;
}
protected function setIsLoading(value:Boolean):void
{
_isLoading = value;
}
public function get data():Object
{
return _data;
}
protected function setData(value:Object):void
{
_data = value;
}
protected function get intervalId():int
{
return _intervalId;
}
protected function set intervalId(value:int):void
{
_intervalId = value;
}
public function load():void
{
if (isLoading) {
return;
}
setIsLoading(true);
intervalId = setInterval(internalLoad, 500);
}
protected function internalLoad():void
{
if (!isLoading) {
return;
}
clearInterval(intervalId);
setIsLoading(false);
if (onComplete != null) {
onComplete(this);
}
}
public function cancel():void
{
if (!isLoading) {
return;
}
clearInterval(intervalId);
setIsLoading(false);
}
}
setInterval を使って 500 ミリ秒後に onComplete を呼んでるだけです。data にはコンストラクタで渡された値がセットされます。
そして、次のような LocalQuery だけを使うファクトリを作ります。
/**
* 実際には通信をしないクエリを作る IQueryFactory の実装
*/
public class LocalQueryFactory implements IQueryFactory
{
public function makeGetUserlistQuery():IQuery
{
return new LocalQuery(['userA', 'userB', 'userC']);
}
public function makeGetUserIdByNameQuery(name:String):IQuery
{
return new LocalQuery('1234');
}
}
そして ApplicationGlobals には LocalQueryFactory を生成して渡すようにすると…
ApplicationGlobals.globals.queryFactory = new LocalQueryFactory();
お分かり頂けるでしょうか。IQuery を使う側 (再度掲載しますが↓のコードです) は全く変更無しに、サクッとデバッグ用のローカルで完結するテスト通信だけで開発を進めることができます。サーバーサイドが出来てきたら、new LocalQueryFactory() を new NetworkQueryFactory() に書き換えるだけで、すぐ実際の通信に切り替えてテストを行うことができます。
var getUserlistQuery:IQuery = ApplicationGlobals.globals.queryFactory.makeGetUserlistQuery();
getUserlistQuery.onComplete = getUserlistQueryCompleteHandler;
getUserlistQuery.load();
// 通信が成功すると IQuery を引数としてイベントハンドラが呼び出される、という想定
protected function getUserlistQueryCompleteHandler(query:IQuery):void
{
// data で取ってきたデータが取れる、という想定。そしてきっと予め JSON なんかをパースしておいてくれている
var userlist:Array = query.data as Array;
}
「IQuery を使う側の変更が必要ない」というのは非常に重要です。サーバーサイド通信が出来ていない段階でも、サーバーサイド通信を前提としたコードを書くことができ、そしてそれを将来に渡って変更無く使えるということです。余計な後戻りが無くなります。ネットワーク動作・ローカル動作の切替や、テストのためにこの辺をぐちゃぐちゃ弄り回してると、バグが混入する確率も高まるのは言うまでもないでしょう。
さて、次の例として、サーバー側の API が突然バグった場合。例えばさっきのユーザー一覧取得 API がなんかおかしくなっちゃったとしましょう。でも、他はサーバー通信しながらテストしないといけない。「何やっとんじゃ動くものコミットしろって言ってんだろ!!クライアントの開発止まるだろうが!!!」と叫ぶ必要はもうありません。そんな場合は NetworkQueryFactory を派生させて、ユーザー一覧取得だけ LocalQuery にしてしまいましょう。
/**
* バグ対策用 IQueryFactory の実装
*/
public class VSBugNetworkQueryFactory extends NetworkQueryFactory
{
override public function makeGetUserlistQuery():IQuery
{
return new LocalQuery(['userA', 'userB', 'userC']);
}
}
で、
ApplicationGlobals.globals.queryFactory = new VSBugNetworkQueryFactory();
することで、一部だけしれっとローカル動作にしてやり過ごすことができます。バグ再現のために、ここだけこういう値が欲しい!という場合にも有用です (特にランキング等時間経過で変動してしまう値)。
最後の例として、通信方法に変更が入った場合。例えばさっきの各種 API を呼び出す際に、「FlashVars で渡したユーザーIDをつけてくれ」と言われたとしましょう。「ふざけんな!!通信処理してるところリストアップするだけでも半日掛かるわ!!!」と叫ぶ必要はもうありません。NetworkQueryFactory をちゃちゃっと修正するだけです。
/**
* 実際に通信するクエリを作る IQueryFactory の実装
*/
public class NetworkQueryFactory implements IQueryFactory
{
public function NetworkQueryFactory(userId:uint)
{
_userId = userId;
}
private var _userId:uint;
public function makeGetUserlistQuery():IQuery
{
return new NetworkQuery(new URLRequest('userlist.php?id=' + _userId));
}
public function makeGetUserIdByNameQuery(name:String):IQuery
{
return new NetworkQuery(new URLRequest('finduser.php?id=' + _userId + '&name=' + name));
}
}
で、
ApplicationGlobals.globals.queryFactory = new NetworkQueryFactory(loaderInfo.parameters['userId']);
とすれば後の一切の変更が不要なのは明白でしょう。このほかにも、例えば「このリクエストを5回まではキャッシュして欲しい」などと言われても、このクラスで全て吸収すれば何とでもなります。
このように、通信処理を抽象化しておくと様々なメリットがもたらされ、終始にこやかにクライアントの皆様とお仕事ができるようになります。基本的に僕の仕事では必須のパターンです。ちなみに、実際には、IQuery の代わりに Command を使っても Thread を使っても構いません。Abstract Factory の使いどころが良く分からないという嘆きを良く見掛けますが、このように「ごそっと関連クラスを入れ替える」目的で使うパターンです。ではまた!
※このエントリは BEFoundation について書かれたものです。詳しくはこちら。
今週はあまり長文を書く時間が取れそうにないので、前回の「オブジェクトの基本操作」で予告した永続化について書いてみたいと思います。
永続化とは、オブジェクトを、後で全く同じ状態で復元可能なように、何らかの形式で直列化して保存しておく (そして直列化復元して元に戻す) ことを言います。シリアライズ・デシリアライズって言った方が馴染みがあるかもしれないですね。
ActionScript 3.0 では AMF (ByteArray の writeObject メソッドを使う) や JSON (as3corelib にエンコーダ・デコーダが存在する) で永続化を行うのが一般的なように思います。ただし、これらだと以下のようなデメリットが存在します。(正直このデメリットが致命的になるようなプロジェクトはかなり少ないと思われますが、なぜか僕はここ最近そんなプロジェクトばかりです…)
- プライベートなフィールドを保存出来ない
- オブジェクトのバージョン管理が難しい
- AMF の場合、引数無しのコンストラクタを持つカスタムクラスしか扱えない
- AMF の場合、エンコード結果がバイナリになってしまう
- JSON の場合、カスタムクラスを扱えない
- JSON の場合、同じ参照でも復元すると別オブジェクトになる
- JSON の場合、参照に循環が含まれているとエンコード出来ない
などなど。というわけで作ったのが org.libspark.common.serialize.JSONSerializer クラスです。この人は org.libspark.common.serializer.ISerializable インターフェイスを実装したオブジェクトをいい感じに JSON 形式で直列化してくれます。使い方は次のような感じ。
// シリアライザ生成 var serializer:JSONSerializer = new JSONSerializer(); // シリアライズしてもらう (obj は ISerialzable 実装クラスのインスタンス) serializer.serialize(obj); // JSON 文字列が完成する trace(serializer.serializedString);
※ JSON といっても、形式が JSON というだけで色々細工してあるのであの見慣れた感じとはちょっと遠いものが出来上がります。
復元の方は org.libspark.common.serialize.JSONDeserializer がやってくれます。次のような感じ。
// デシリアライザ生成 var deserializer:JSONDeserializer = new JSONDeserializer(); // シリアライズした JSON 文字列をセット deserializer.sourceString = serializedString; // デシリアライズしてもらう deserializer.deserialize(); // オブジェクトもどってきたお obj = deserializer.deserializedObject;
前回紹介した Any クラスは ISerializable を実装しており、かつ、例の store / restore メソッドをオーバーライドしていれば OK になってるので、簡単に賢い永続化が出来るようになります。プライベートなフィールドは store でちゃんと書き出してやれば全然問題なしです。バージョン管理については、store でバージョンも保存しておいて、restore で取り出す時にバージョンを見て分岐したりすればとりあえず対応出来そうかなーと思ってます。引数ありコンストラクタについては、デシリアライザがインスタンスを生成する際は必ず org.libspark.common.reflect.InstanceFactory を使うようになってるので、(まだこの辺の設計ちゃんと考えてないんですけど) ここでごにょごにょすれば OK かなぁーと思ってます (ちなみに clone とかも同じルール)。オブジェクトの参照構造は完全に保ったまま保存と復元をしてくれるので、JSON のように循環があるとダメとか復元したら別々の参照になってしまったとかいうこともありません。てな感じで、大体上のデメリットは克服出来ているかなと。
余談: ISerializable は実質 IStorable の実装を必要とするので IStorable に統一すればいいんじゃないかと思ったりするんですが IStorable の store / restore は色々な所で使われるのでシリアライズ時だけに何か処理を挟みたいという場合に備えて serialize / deserialize は別メソッドになっていた方がいいのかなぁ、と思って別のインターフェイスになってますがどうなんだろう。
というわけで、そんな感じですがいかがでしょう?上で挙げたようなデメリットに困ったことある人や、AIR で本格的なアプリ作っててモデルクラスの永続化で悩んでるとか言う人は使ってみると幸せになれるかもしれませんよ。
ちなみに、参照構造とか復元とかどうでもいいから、単純に JSON とか XML に値を書き出す/から値をセットする、みたいなのは恐らく永続化ではなく、変換 (エンコードとかデコード?) という別の概念になりそうなのでそれはそれでまた別に作った方がいいんじゃねーでぃすかと思ってます。ではまた!
※このエントリは BEFoundation について書かれたものです。詳しくはこちら。
早速ですが、今回はオブジェクトの基本操作についてです。ここでいうオブジェクトの基本操作とは、以下のものを指します。
- 比較 - 二つのオブジェクトが等価か比較する
- 複製 - あるオブジェクトと等価な新しいオブジェクトを作成する
- コピー - あるオブジェクトと等価になるように値を設定する
大体イマドキな言語だったら言語レベルでこの辺のサポートがあったりするんですが、AS3 ではほとんどありません。しかしながら、仕事をしてると、一案件で数回はこういうことがしたくなります。特にモデルクラス (アプリケーション固有のデータを表現するクラス) に対してそういう操作が多いでしょう。
そこで、BEFoundation ではこの辺何も考えずに出来ちゃう仕組みを作りました。作るにあたって、「オブジェクト指向入門」の著者である、バートランド・メイヤー先生 (オブジェクト指向界の高木浩光先生のような存在なんではないかと思ってる) が作った、オブジェクト指向のお手本のような言語 Eiffel を参考にしてます。
まず、これらの能力を有することを示すインターフェイスを作りました。
- org.libspark.common.ability.IComparable
- org.libspark.common.ability.IClonable
- org.libspark.common.ability.ICopyable
それぞれ、比較可能か、複製可能か、コピー可能か、ですね。最も基本的な所っぽいので、common パッケージ、更に、能力を示すものなので ability パッケージにしてみました。各メソッド deep がついているものとついていないものがありますが、違いはフィールドの内容を再帰的に見に行くかどうかです。例として isEqual と isDeepEqual を見てみます。次のようなオブジェクトがあったとしましょう。
public class Hoge
{
public var moja:Hoge;
public var value:String;
}
var a:Hoge = new Hoge();
var b:Hoge = new Hoge();
a.value = "A";
b.value = "B";
a.moja = b;
var a2:Hoge = new Hoge();
var b2:Hoge = new Hoge();
a2.value = "A";
b2.value = "B";
a2.moja = b2;
ここで a.isEqual(a2) をすると、フィールドを再帰的に見に行きませんので、a.moja と a2.moja を比較し、a.moja != a2.moja であるので false が返ります。ところが、a.isDeepEqual(a2) だと、フィールドを再帰的に見に行くので、a.moja != a2.moja であっても、その中身である b と b2 の内容は等しいので true が返ります。
他のメソッドも同様で、var a3:Hoge = a.clone だと再帰的には複製されないので a3.moja == a.moja (すなわち両方とも b と等しい) ですが、var a4:Hoge = a.deepClone だと再帰的に複製されるので、a4.moja は a.moja とは別物になります。
さて、ではこれらのメソッドをどうやって使えるようにするのか?というところですが、org.libspark.common.Any という、IComparable, IClonable, ICopyable を全て実装したクラスがあるのでこれを継承し、以下のように store メソッドと restore メソッドをオーバーライドします。(ちなみに Any という名前の由来は Eiffel で AS3 の Object にあたる同名のクラスです)
public class Hoge extends Any
{
public var moja:Hoge;
public var value:String; // コード行数減らすために public にしてるだけ
override public function store(to:IWritableStore):void
{
super.store(to);
to.writeStorable('moja', moja);
to.writeString('value', value);
}
override public function restore(from:IReadableStore):void
{
super.restore(from);
moja = from.readStorable('moja', null) as Hoge;
value = from.readString('value', null);
}
}
store には org.libspark.common.store.IWritableStore が渡ってくるので、フィールド名をキーにして、フィールドの内容を書き出し、restore には org.libspark.common.store.IReadableStore が渡ってくるので、同じキーを使ってフィールドの内容を書き戻すという感じです。これをやっておくだけで、後は Any 君がいい感じに色々やってくれて isEqual, isDeepEqual, clone, deepClone, copy, deepCopy が使えるようになります。モデルクラスをこれで作っておくだけで、格段に使い勝手がよくなりますよ。(オーバーライドして色々書くのめんどくせーと思うかもしれませんがこれだけで上記メソッド使えるようになると思ったら安いと思いません?)
また余談ですが、今度詳しく書こうと思ってるコレクションフレームワークの org.libspark.colleciton.array.ArrayedArray クラスには useEqualityComparsion プロパティがあって、これを true にすると indexOf するときの比較などを全て isEqual を使ってやるようになってくれるので便利です。あと ArrayedArray 自身も Any を継承しているので deepClone で完全な複製が作れたりして便利です。
ちなみに、比較・複製・コピー動作をカスタマイズしたい時はどうするの?という点ですが、比較に関しては isEqual を直接オーバーライドします。
// ID さえ等しければ等価と見なす例
override public function isEqual(other:IComparable):Boolean
{
var otherUser:User = other as User;
if (otherUser == null) {
return false;
}
if (otherUser.id != id) {
return false;
}
return true;
}
複製とコピーは protected な copyFrom メソッドをオーバーライドします。clone は新しいインスタンスの生成 + コピーなので、コピー動作に集約されます。super.copyFrom は呼び出しても呼び出さなくても OK です。
// コピーする時に配列は複製しないと困る、とか
override protected function copyFrom(other:Any):void
{
super.copyFrom(other);
var otherAlbum:Album = other as Album;
photos = otherAlbum.photos.clone as ArrayedArray;
}
先ほどの store / restore メソッドは org.libspark.common.store.IStorable というインターフェイスで定義されていて、このインターフェイスを実装したクラスの比較やコピーや複製をサポートするクラスが org.libspark.common.support パッケージにおりまして、Any は内部的にこれを使っています。もし自力で IComparable, IClonable, ICopyable を実装する際は、IStorable も実装してこれらのサポートクラスを使えば良いかな、という想定です。
長々と書いてきましたが、こんな感じでオブジェクトの基本操作を実現出来る機構を作ってみたんですがいかがでしょう。あと実は永続化も出来るんですがそれはまた今度書きます。ではまた〜。
タイトルは若干釣りですが。
しばらく前に、以前エントリで紹介した、「オブジェクト指向入門」という本を読み終わりました。トータルで1000ページ以上ある辞書のような殺人的な本で何度か死にかけましたが、iPad を血で染めながらなんとか読み切ったのです。結果としては、かなり勉強になり、目から鱗もこぼれまくりでした。正直オブジェクト指向を全然理解できてなかったんじゃないかと思えるほどです。色々と思うところありましたが、読み終わって一番感じたのは、再利用性足りてるぅ? (CV: 田中理恵) ということでした。
「オブジェクト指向入門」から少し引用させてもらうと、オブジェクト指向とは、拡張性と再利用性 (他にもいくつかの要因がありますが) を高いレベルで達成するための手法です。拡張性とは、「仕様の変更に対するソフトウェア製品の適用のしやすさ」、再利用性とは、「多数多様なアプリケーションの構築に使うことのできる、ソフトウェア要素の能力」を言います。日々案件をこなしている皆さんなら分かると思いますが、ソフトウェア (Flash も同じです) は、他のどのような分野の製品よりも「変わりやすい」ものであることを感じていると思います。例えば、仕様変更とか、仕様変更とか、仕様変更とか、etc…。同時に、その変化に適応させていく難しさも知っていることでしょう。結果、毎度大量のコードをゼロから作ったり、途中で作り直したり、コピペだらけのコードになったりする現実があります。あーもー!もっとサクッと既存のものを組み合わせて、ちょこっと修正して、スバラシイもんが作れる素敵な方法はないのか!!と思うでしょう。僕も思ってました。しかし、その手法こそオブジェクト指向だったのです。灯台下暗しとはこのことです。お前、単に使いこなせてなかっただけなんじゃね?と気付かされました。
なんかだんだん宗教の勧誘みたいな口上になってきてしまいましたが、実際のところ、もっときちんとオブジェクト指向を使いこなして、拡張性や再利用性を高めていかないと、この先乗り切れないな、という危機感を覚えたのです。というわけで、二ヶ月ぐらい前から、「二度と同じコードは書かない」というスローガンの元、このあたり真剣に意識して仕事をしてました。結果、以下のライブラリが生まれました。
BEFoundation
色んな所で使える基礎ライブラリ。今のところカバーしてるのは以下の要素。
- オブジェクトの基本操作
- 永続化サポート
- コレクションフレームワーク
- バインディング機構
- 階層イベントディスパッチャ
- 抽象モデル
- ユーティリティ
BEComponents
カスタマイズすること前提な汎用コンポーネント群。見た目と動作が完全分離された抽象コンポーネントを提供する。自分コンポーネントを作る時に、見た目以外の面倒な部分が全て出来上がってる、って言った方がイメージし易いかも。
BEGameEngine
2Dゲームを作る時のひな形。BeInteractive! 的なゲームエンジンの設計を実現するためのもの。
というわけで、「BEなんちゃら」という名前で BeInteractive! 基本ライブラリ群を作っていくことにしました。たぶん BetweenAS3 もそのうち BETween か BETweenAS3 になります。とりあえずコミットはしましたが、今現在は全然安定はしておらず、日々案件で使いながら育てていってる段階です。とはいえ、この辺りの技術情報は広くシェアした方が世の中幸せになる気がするので、開発日誌的な感じで、気が向いた時にブログに残していこうと思います。ただし、途中でうっかり色々変更になる可能性もあるのでその辺りはご了承下さい。ちなみに、このクラスってなんなん?とか、どう使う前提なの?とかそういう質問には出来るだけお答えしようと思います。そこから何か生まれるかもしれないので。(ただししばらくは自分の仕事で使うこと優先ですが…)
そんなこんなで、詳しいことはまた随時ブログに書いていく所存です。どうぞ生暖かく見守ってやって下さい。
あっという間に今年も半分が終わろうとしていますが、皆様如何がお過ごしでしょうか。
最近、仕事でエディタやツール的なものを作ることが多いのですが、いざ作ってみて思うのはイマドキ Cmd+Z が利かない (=アンドゥ/取り消しが出来ない) のはどーなのよ。ってことです。実際、世の FLASHer が Cmd+Enter の前に Cmd+S を押してしまうように、無意識に Cmd+Z を押してる場面も少なくないです。一個前にやっていた仕事ではアンドゥをつけなかったのを微妙に後悔していたりします (後から組み込むのは結構大変…)。
というわけで、今やってる仕事ではアンドゥを付けようと思って、さー UndoManager を書くぞと書き始めたら、補完に flashx.undo.UndoManager っていう、何か既に出来る子っぽいクラスが居るので、3秒で自分で作ったクラスは消しました。オブジェクト指向の奥義、再利用ってヤツです。
調べてみると、flashx.undo.UndoManager は、TextLayoutFramework に含まれているクラスのようです。Flex SDK 4 であれば、"frameworks/projects/textLayout/src/flashx/undo/UndoManager.as" に元のファイルを見つけることができます。たぶん、新しいテキストエンジンで色々やるために自前で実装する必要があったんでしょうね。
アンドゥ/リドゥを実現するためには、行った「操作」を覚えておいて、その操作に対する「復元」と「再実行」ができなければなりません。言い換えると、「復元」と「再実行」が出来る「操作」を表現するクラスが必要です。このようなクラスがあれば、操作を行う度に、このクラスのインスタンスを生成して溜め込んでおくことで、必要な時に「復元」を呼び出して、操作を取り消すことができます。所謂 Command パターンですね。
flashx.undo パッケージにも、このような Command クラスを実装するための、flashx.undo.IOperation というインターフェイスが存在します。定義されているメソッドはふたつで、perfomUndo() と perfomRedo() です。それぞれ、これが呼び出された時に「復元」と「再実行」を行えってことです。例えば何かドット絵エディタ的なものを作っているとして、「BitmapData のあるピクセルに色をセットする」という操作に対する IOperation インターフェイスの実装クラスは次のようなものになるでしょう。ちなみに、折角「操作」をクラス化するんですから、操作の「実行」もこのクラスに任せたいので、IOperation クラスを継承して execute() というメソッドを定義した IExecutableOperation を用意して、それを実装しています。
public interface IExecutableOperation extends IOperation
{
function execute():void;
}
public class SetPixelOperation implements IExecutableOperation
{
public function SetPixelOperation(bitmapData:BitmapData, x:uint, y:uint, color:uint)
{
_bitmapData = bitmapData;
_x = x;
_y = y;
_color = color;
}
private var _bitmapData:BitmapData;
private var _x:uint;
private var _y:uint;
private var _color:uint;
private var _oldColor:uint;
// 操作の呼び出し
public function execute():void
{
// 元の色を覚えておく
_oldColor = _bitmapData.getPixel32(_x, _y);
// 色をセット
_bitmapData.setPixel32(_x, _y, _color);
}
// 操作の取り消し
public function performUndo():void
{
// 元の色に戻す
_bitmapData.setPixel32(_x, _y, _oldColor);
}
// 操作のやり直し
public function performRedo():void
{
// 色をセットする
_bitmapData.setPixel32(_x, _y, _color);
}
}
このようにすることで、ある操作に対する「実行」と「復元」と「再実行」の責任をひとつのクラス内にまとめることができました。このクラスを使用して、例えば次のようなコードを書くことで、取り消しが可能な操作を実現することができます。
// クリックされた時のハンドラ
private function clickHandler(e:MouseEvent):void
{
// カーソル位置の色を選択されている色に変更する操作を生成
var operation:IExecutableOperation = new SetPixelOperation(_bitmapData, stage.mouseX, stage.mouseY, _selectedColor);
// 操作を実行
operation.execute();
// 取り消し可能な操作として UndoManager に登録
_undoManager.pushUndo(operation);
}
_undoManager は、もちろん flashx.undo.UndoManager クラスのインスタンスです。こうしておくと、例えば Cmd+Z が押された際に _undoManager の undo() メソッドを呼び出せば、直前の操作を取り消すことが出来ます。勿論、更に undo() メソッドを呼び出せば、それ以前の操作もどんどん取り消されていきます。
ただし、ここで一つ問題があります。普通、undo() をしたら redo() を出来ることが期待されますが、この UndoManager は自動的には redo() をできるようにはしてくれません。この部分は自分で書く必要があるようです。なので、最終的に以下のような OperationManager というクラスを作りました。このようなクラスを一つ作っておくと便利でしょう。
public class OperationManager
{
private static var _sharedOperationManager:OperationManager;
/**
* 共有されている OperationManager クラスのインスタンスを返します.
*/
public static function get sharedOperationManager():OperationManager
{
if (_sharedOperationManager == null) {
_sharedOperationManager = new OperationManager();
}
return _sharedOperationManager;
}
/**
* 新しい OperationManager クラスのインスタンスを生成します.
*/
public function OperationManager()
{
_undoManager = new UndoManager();
}
// 内部で使用する UndoManager
private var _undoManager:UndoManager;
/**
* アンドゥが可能であれば true, そうでなければ false を返します.
*/
public function get canUndo():Boolean
{
return _undoManager.canUndo();
}
/**
* リドゥが可能であれば true, そうでなければ false を返します.
*/
public function get canRedo():Boolean
{
return _undoManager.canRedo();
}
/**
* アンドゥを実行して直前の操作を取り消します.
*/
public function undo():void
{
// アンドゥできる状態でなければ何もしない
if (!canUndo) {
return;
}
// アンドゥ対象の操作を取得
var undoOperation:IOperation = _undoManager.peekUndo();
// アンドゥを実行
_undoManager.undo();
// リドゥできるように登録
_undoManager.pushRedo(undoOperation);
}
/**
* アンドゥを実行して直前に取り消した操作を再実行します.
*/
public function redo():void
{
// リドゥできる状態でなければ何もしない
if (!canRedo) {
return;
}
// リドゥ対象の操作を取得
var redoOperation:IOperation = _undoManager.peekRedo();
// リドゥを実行
_undoManager.redo();
// アンドゥできるように登録
_undoManager.pushUndo(redoOperation);
}
/**
* 指定された操作を実行し、取り消し可能なよう登録します.
*
* @param operation 実行する操作
*/
public function execute(operation:IExecutableOperation):void
{
// 操作を実行
operation.execute();
// アンドゥできるように登録
_undoManager.pushUndo(operation);
// 新しい操作を実行した際はリドゥはもういらない
_undoManager.clearRedo();
}
}
このクラスを使用する場合、先ほどのコードは次のようになります。
// クリックされた時のハンドラ
private function clickHandler(e:MouseEvent):void
{
// カーソル位置の色を選択されている色に変更する操作を生成
var operation:IExecutableOperation = new SetPixelOperation(_bitmapData, stage.mouseX, stage.mouseY, _selectedColor);
// 操作を実行
OperationManager.sharedOperationManager.execute(operation);
}
自分で operation の execute() メソッドを呼び出す代わりに、OperationManager に渡して実行をしてもらいます。このあと、OperationManager の undo() を呼び出せば、操作が取り消されます。続けて redo() を呼び出せば、取り消した操作が再実行されます。何度か undo() を呼び出して、redo() を呼び出さずに execute() で新しい操作を実行すると、undo() した分の redo() はクリアされて再実行できなくなります (大抵のアプリケーションを観察するとこのような実装になっているのが分かるでしょう)。めでたく、いい感じにアンドゥ/リドゥが実装出来ました。
ちなみに、AIR でメニュー項目と関連づける場合は、次のようにすると、アンドゥ/リドゥ可能かによってメニューの有効/無効も切り替えることができます。
// アンドゥメニュー
var undoMenu:NativeMenuItem = new NativeMenuItem('Undo');
undoMenu.label = '元に戻す';
undoMenu.keyEquivalent = 'z'; // Cmd+Z or Ctrl+Z
undoMenu.addEventListener(Event.DISPLAYING, willDisplayUndoMenuHandler);
undoMenu.addEventListener(Event.SELECT, undoMenuSelectHandler);
// リドゥメニュー
var redoMenu:NativeMenuItem = new NativeMenuItem('Redo');
redoMenu.label = 'やり直し';
redoMenu.keyEquivalent = 'Z'; // Cmd+Shift+Z or Ctrol+Shift+Z
redoMenu.addEventListener(Event.DISPLAYING, willDisplayRedoMenuHandler);
redoMenu.addEventListener(Event.SELECT, redoMenuSelectHandler);
// メニューが表示される直前に呼ばれる
private function willDisplayUndoMenuHandler(e:Event):void
{
var undoMenu:NativeMenuItem = e.target as NativeMenuItem;
undoMenu.enabled = OperationManager.sharedOperationManager.canUndo;
}
// メニューが表示される直前に呼ばれる
private function willDisplayRedoMenuHandler(e:Event):void
{
var redoMenu:NativeMenuItem = e.target as NativeMenuItem;
redoMenu.enabled = OperationManager.sharedOperationManager.canRedo;
}
// メニューが選択されると呼ばれる
private function undoMenuSelectHandler(e:Event):void
{
OperationManager.sharedOperationManager.undo();
}
// メニューが選択されると呼ばれる
private function redoMenuSelectHandler(e:Event):void
{
OperationManager.sharedOperationManager.redo();
}
それではまた!
先月や先々月は鼻から DisplayObject がパーティクルの如く吹き出そうなぐらいバリバリ実装をしていた反動か、今月は設計やデザインに食指が動く今日この頃です。というか、現実問題として実装前フェーズの案件が沢山あるってだけなんですけどね。
そんで色々考える訳なのですが、ふと、「オブジェクト指向そのもの」についてきちんと勉強したことが無い気がしてきました。これはいけません。なんかバイブル的なものってあるんでしょうか。
で、インターネッツを漂流していたらオージス総研さんの「オブジェクトの広場」にいい感じのまとめをいくつか発見しました。
- 技術書籍紹介 [オブジェクト指向を学ぶための入門書ガイド]
- これだけは読んでおきたい!オブジェクト指向関係の文献ガイド
- オブジェクト本分野別ランキング [デザインパターン]
- オブジェクトの広場編集員が贈るオススメ書籍 - 今、この本がアツい! -
で、気になったのが以下。
オブジェクト指向入門 第二版
こいつがラスボス (いきなり) っぽい。入門書にしてバイブルなんですと。 (レビュー)
Booch法: オブジェクト指向分析と設計 第2版
スリーアミーゴ (とか初めて聞いた…GoF みたいな) のお一人である Booch さんの本らしい。 (レビュー)
ソフトウェア アーキテクチャソフトウェア開発のためのパターン体系
POSAってそういえば聞き覚えある。GoF のデザパタ本よりもソフトウェア全体の設計について言及してるのかな。 (レビュー1, レビュー2, レビュー3)
デザインパターンプログラミング
なんか「メタパターン」というのについて書かれたものらしい。 (レビュー)
アンチパターン
アンチパターンもそういえば読んだことない。べからず集ですな。 (レビュー)
ケント・ベックの Smalltalk ベストプラクティス・パターン — シンプル・デザインへの宝石集
ケントベックと言えばXPとかアジャイルとかテスト駆動開発とかとかですな。コーディング作法が色々、みたい。 (レビュー)
実装パターン
ケントベックの Wikipedia 見てたらついでに見つけた本。上の本が1996年に書かれたものなのに対してこっちは2007年。進化版っぽい? (レビュー)
うーん、いっぱい。最後の方は「オブジェクト指向そのもの」と関係なくなってきてるし。ただ、iPad を二台予約したワタクシは最近読書欲が高まってる (全部 iPad にぶっ込みたい) のでコツコツと吸収していきたいですね。これを読んでる FLASHer の方々は全く興味無いと思われますが、そもそもここまで読み進める前にブラウザや RSS リーダーを閉じているかもしれませんが、なんかしら得るものがあったら随時フィードバックしていきたいと思っております。
ちなみに既に持ってる本は↓
オブジェクト指向における再利用のためのデザインパターン
言わずもがな。
増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編
そうめんをよく使う人にもオススメ。
J2EEパターン 第二版
なぜ持っているのか謎。でも一通り読んだ記憶はある。
リファクタリング - プログラムの体質改善テクニック
リファクタリングのバイブル。
テスト駆動開発入門
リファクタリングにも欠かせない TDD のバイブル (かは定かではないけどケントベック先生著なので鉄板)。
どれも数年前に読んだっきりなので、これらの本も (iPad で) 改めて読み直したいところ。
残念ながら(?)、いきなり Cocoa の話です。が、Cocoa のビルトインクラスやフレームワークはなかなか面白いデザインが多いです。設計した人の、プログラミングも beautiful じゃなきゃ boom! みたいなこだわりを随所に感じます。今日は、なるほどと思った部分をひとつ紹介します。
Cocoa の文字列クラスである NSString のメソッドの一つに、「stringByTrimmingCharactersInSet:」というものがあります。これは、文字列の先頭や最後にある空白を削除した文字列などを作るためのメソッドです。ただし、NSCharacterSet クラスのインスタンスを引数として取るのが面白い所です。同じようなメソッドは他の言語でもよく見掛けます (例えば Java の String クラスの trim メソッド) が、引数を取るものはあまり見掛けません。
NSCharacterSet は、文字列のセット (集合) を表現するためのクラスです。例えば、空白文字のセットや、英数字のセットなどです。stringByTrimmingCharactersInSet: メソッドはこのセットを引数にとり、セットに含まれる文字列をトリミングしてくれるのです。NSCharacterSet には「whitespaceCharacterSet」(空白)、「whitespaceAndNewlineCharacterSet」(空白と改行)、「alphanumericCharacterSet」(アルファベットと数字)といったセットが予め静的メンバとして用意されており、柔軟なトリミングが行えます。更に、NSCharacterSet のサブクラスを作成することで、任意の文字列のセットを独自に作ることができます。NSCharacterSet のサブクラスでは、その文字がセットに含まれるかどうかを判定して返す「characterIsMember:」メソッドだけをオーバーライドすれば OK です。ここから想像すると、トリミングメソッドの中では各文字に対して characterIsMember: メソッドを呼び出して判定を行っているんでしょうね。
Java の trim メソッドでは決めうちの空白文字しか削除出来ないのに対して、Cocoa では、削除する文字の判定を別オブジェクト化することで、柔軟性が高まっているのが分かりますね。このように動作を変えたい部分を別のクラスに切り出して、組み合わせて使うのはオブジェクト指向においては重要な考え方ではないかと思っています。Strategy パターンっぽいですね。動作を変えたい分だけ、「stringByTrimingWhitespaceCharacters」とか「stringByTrimingAlphanumCharacters」とかメソッド増やしていくのは美しくないです。というか、他の文字セットに対応しようとしたらそれどーすんの?って感じです。
NSCharacterSet は NSString の他のメソッド、例えば「componentsSeparatedByCharactersInSet:」などでも有効に活用されています。応用範囲が広いですね。このようにクラス化しておくことで、色々な場面で再利用出来そうです。単なるデータ (文字列) ではなく、クラスなので、複数の NSCharacterSet をまとめてひとつの NSCharacterSet に見せる CompositeCharacterSet なんてのも簡単に作れそうで良さげです。
それではまた!
みなさまこんにちは。すっかりお知らせブログになってしまってる BeInteractive! ですがもう少し技術的な話を復活させようってことでこの連載をはじめてみることにしました。連載といっても不定期なんですけどね!
一昔前は beinteractive といえばバイナリの人とか SWF ファイルを直接弄くり回す人みたいな謎のイメージがありましたが、最近はおぶじぇくと指向とか、せっけいとか、らいぶらりを作るとか、そういう話が趣味なので、要するにコーディングにまつわる普段思ってることとか好きな話をつらつら書いていこうかなと思ってます。
記事は「ArtisticCoding」カテゴリに入る予定です。どうぞよろしく!



