Flyweight
一言でいうと
Flyweight (フライウェイト、 フライ級) は、 構造に関するデザインパターンの一つで、 複数のオブジェクト間で共通する部分を各自で持つ代わりに共有することによって、 利用可能な RAM により多くのオブジェクトを収められるようにします。
問題
長い仕事の後の娯楽として、 簡単なビデオゲームを作ることにしました。 プレーヤーは地図を動き回ってお互い撃ち合います。 とてもリアルに見えるパーティクル・システムを実装し、 このゲームのセールスポイントの一つとすることにしました。 大量の弾丸、 ミサイル、 爆発の破片が、 マップ上を飛び回り、 プレーヤーがスリルを味わいます。
プログラミング完成後、 最後のコミットをプッシュしてゲームをビルドし、 テストドライブしてもらうために友達に送りました。 自分のマシン上では完璧に動いていたゲームですが、 友達は長時間プレーすることができませんでした。 彼のコンピューター上では、 ゲームは開始後数分でクラッシュが続きました。 デバッグ用ログを数時間かけて調べた結果、 RAM の容量不足が原因でゲームがクラッシュしたことがわかりました。 友達のマシンは、 自分のコンピューターよりもかなり非力なものであることが判明しました。 彼のマシン上で短時間で問題が起きたのは、 このためです。
実際の問題は、 パーティクル・システムに関連していました。 弾丸、 ミサイル、 破片などのパーティクルは、 それぞれ大量のデータを含む個別のオブジェクトによって表現されていました。 ある時点で、 プレーヤーの画面上の殺戮がクライマックスに達した時、 新しく作成されたパーティクルが残りの RAM に収まりきらず、 そのためプログラムはクラッシュしました。
解決策
Particle
クラスをよく調べてみると、 color と sprite フィールドが他のフィールドよりもかなり大量にメモリーを消費していることにお気づきかもしれません。 さらに悪いことには、 この二つのフィールドには、 すべてのパーティクルにわたってほぼ同じデータが格納されています。 たとえば、 すべての弾丸は同じ色とスプライトを持っています。
座標、 移動ベクトル、 速度などのパーティクルの他の状態は、 それぞれのパーティクルに固有です。 結局のところ、 これらのフィールドの値は時間とともに変化します。 このデータは、 パーティクルが存在の常に変化する状況を表しているわけですが、 それぞれのパーティクルの色とスプライトは一定の値を保ちます。
このようなオブジェクト内の不変のデータは、 通常内因的状態 (intrinsic state) と呼ばれます。 これはオブジェクトの中に存在し、 他のオブジェクトはこれを読むだけで、 変更することはできません。 オブジェクトの残りの状態は、 しばしば他のオブジェクトによって 「外側から」 変更され、 外因的状態 (extrinsic state) と呼ばれます。
Flyweight パターンに従うと、 外因的状態をオブジェクト内部に持つことはやめるべきです。 代わりに、 この状態は、 依存する特定のメソッドに渡します。 内因的状態だけをオブジェクトに保管し、 違った状況で再利用します。 内因的状態は、 外因的態に比べて種類が少ないので、 結果、 このオブジェクトは少い個数ですみます。
さて、 ゲームの話に戻りましょう。 パーティクル・クラスから外因的状態を抽出したところ、 ゲームのすべてのパーティクルを表現するためには、 弾丸、 ミサイル、 破片のたった 3 個のオブジェクトで十分だということがわかったとします。 もうお気づきかもしれませんが、 内因的状態を格納するオブジェクトは、 フライウェイトと呼ばれます。
外因的状態の記憶場所
外因的な状態はどこへ移動すればいいでしょうか? どこかのクラスにそれを保管する必要がありますよね? ほとんどの場合、 パターン適用前にオブジェクトが集約されているコンテナのオブジェクトに移動します。
我々の例では、 主要項目である Game
オブジェクトの particles
フィールドにすべてのパーティクルを格納します。 このクラスに外因的状態を移動するため、 個々のパーティクルの座標、 ベクトル、 速度を格納するための配列フィールドをいくつか作成する必要があります。 しかし、 それだけではありません。 それぞれのパーティクルを表す特定のフライウェイトへの参照を格納するために、 別の配列が必要となります。 同じインデックスを使用してパーティクルのすべてのデータにアクセスできるように、 これらの配列は同期されていなければなりません。
よりエレガントな解決策は、 別にコンテキスト (状況管理) クラスを作り、 そこにフライウェイト・オブジェクトへの参照と共に外因的状態を格納することです。 このやり方では、 コンテナ・クラス内には配列が一つだけあればすみます。
えっ、 ちょっと待って! 最初と同じくらい多数のコンテキスト・オブジェクトを持つ必要があるんじゃない? 技術的にはそうです。 しかし実の所、 これらのオブジェクトは前よりずっと小さいのです。 最もメモリを消費するフィールドは、 わずか数個しかないフライウェイト・オブジェクトに移動されました。 1000 個の小さなコンテクスト・オブジェクトが 1000 個のデータのコピーを保管する代わりに、 重い 「フライウェイト」 のオブジェクト 1 個を再利用できます。
フライウェイトと不変性
同じフライウェイト・オブジェクトを違ったコンテキストから使うので、 その状態は変更できないようにする必要があります。 フライウェイトは、 コンストラクターのパラメーターを介して一度だけ状態の初期化を行います。 よそのオブジェクトに setter や公開フィールドを見せないようにします。
フライウェイト・ファクトリー
様々なフライウェイト・オブジェクトへのアクセスをより便利にするために、 既存のフライウェイト・オブジェクトのプールを管理するファクトリー・メソッドを作成することもできます。 このメソッドは、 クライアントから所望のフライウェイトの内因的状態を受け取り、 この状態に一致する既存のフライウェイト・オブジェクトを探し、 見つかったらそれを返します。 見つからなかった場合は、 新しいフライウェイト・オブジェクトを作成し、 プールに追加します。
このメソッドをどこに置くかについては、 いくつか選択肢があります。 一番わかりやすい場所は、 フライウェイトのコンテナです。 あるいは、 新しいファクトリー・クラスを作成することもできます。 または、 ファクトリー・メソッドを静的 (static) にし、 それを実際のフライウェイト・クラスの中に置くこともできます。
構造
-
Flyweight パターンは最適化にすぎません。 適用前、 プログラムに、 メモリ内に大量の同じようなオブジェクトが同時に存在することに起因する RAM 消費問題があることを確認してください。 この問題が別の方法では解決できないことを確認してください。
-
フライウェイト (Flyweight) クラスは、 元のオブジェクトの状態のうち、 複数のオブジェクト間で共有できる部分を含んでいます。 同じフライウェイトオブジェクトを多くの異なるコンテキストで使用することができます。 フライウェイト内部に格納されている状態は内因的、 フライウェイトのメソッドに渡される状態は外因的と呼ばれます。
-
コンテキスト (Context) クラスには、 すべての元のオブジェクトで一意の、 外因的状態が含まれています。 コンテキストは、 フライウェイト・オブジェクトのいずれかと組になることによって、 元のオブジェクトの状態を完全に表現します。
-
通常、 元のオブジェクトの振る舞いは、 フライウェイト・クラスに残ります。 この場合、 フライウェイトのメソッドを呼び出す際に、 メソッドのパラメーターに外因的状態に関する適切な情報を渡す必要があります。 あるいは、 この振る舞いはコンテキスト・クラスに移動することもできます。 この場合、 リンクされたフライウェイトは、 単なるデータ・オブジェクトとして使用することになります。
-
クライアント (Client) は、 フライウェイトの外因的状態を算出するか保存します。 クライアントの視点からすると、 フライウェイトは、 コンテキスト・データをメソッドのパラメーターに渡すことで実行時に設定可能なテンプレート・オブジェクトです。
-
フライウェイト・ファクトリー (Flyweight Factory) は、 既存のフライウェイトのプールを管理します。 ファクトリーがあるため、 クライアントはフライウェイトを直接作成しません。 代わりに、 ファクトリーを呼び出し、 望むフライウェイトの内因的状態を指定する情報を渡します。 ファクトリーは、 以前に作成されたフライウェイトを見渡して、 検索条件に一致する既存のものを返すか、 何も見つからない場合は新しいものを作成します。
擬似コード
この例では、 Flyweight パターンを適用して、 何百万ものツリー・オブジェクトをキャンバスに描画する際のメモリー使用量を削減します。
パターンを適用して、 主要データである Tree
クラスに繰り返し現れる内因的状態を抽出し、 フライウェイト・クラスの TreeType
に移動します。
ここでは、 同じデータを複数のオブジェクトに格納する代わりに、 ほんの数個のフライウェイト・オブジェクトに格納し、 コンテキストとして機能する、 適切な Tree
オブジェクトにリンクします。 クライアント・コードはフライウェイト・ファクトリーを使用して新しいツリー・オブジェクトを作成します。 このファクトリーは、 正しいオブジェクトを探し、 必要に応じて再利用する複雑な仕組みを隠蔽します。
適応性
Flyweight パターンは、 使用可能な RAM にほとんど収まらりきらない膨大な数のオブジェクトをプログラムがサポートする必要がある場合にのみ使用します。
このパターンが有効かどうかは、 パターンの使用方法と使用場所に大きく依存します。 以下の場合に最も有効です:
- アプリケーションが、 膨大な数の類似したオブジェクトを生成する必要がある。
- これが対象機器上の利用可能な RAM をすべて枯渇する。
- オブジェクトには重複した状態が含まれており、 それを複数オブジェクト間で抽出し共有することが可能。
実装方法
-
フライウェイトになるクラスのフィールドを二つの部分に分割します。
- 内因的状態: 多くのオブジェクト間で繰り返し現れる不変のデータを含むフィールド
- 外因的状態: オブジェクトに固有のコンテキスト・データを含むフィールド
-
内因的状態を表すフィールドはクラス内に残し変更不可とします。 コンストラクターの初期値を取る必要があります。
-
外因的状態のフィールドを使用するメソッドを一つずつ調べます。 メソッドに新しいパラメーターを導入し、 フィールドの代わりに使用します。
-
必要に応じて、 フライウェイトのプールを管理するファクトリー・クラスを作成します。 それは、 新しいフライウェイトを作成する前に、 既存のものがないことを確認します。 ファクトリーができたところで、 クライアントは、 ファクトリーを通してのみフライウェイトを要求しなければなりません。 どのようなフライウェイトが欲しいかを知らせるために、 ファクトリーに内因的状態を渡します。
-
フライウェイト・オブジェクトのメソッドを呼び出せるようにするため、 クライアントは外因的状態 (コンテキスト) の値を保管するか計算する必要があります。 これを容易にするため、 外因的状態とフライウェイト参照フィールドを別のコンテキスト・クラスに移動することもできます。
長所と短所
- プログラムに類似オブジェクトが山のようにある状況下で、 RAM を大量に節約可能。
- フライウェイトのメソッドを呼び出すたびにコンテキスト・データの一部を再計算する必要がある場合、 RAM 使用量減少と引き換えに CPU 使用率増加の可能性。
- コードが大幅に煩雑化。 新しいチーム・メンバーは、 あるエンティティーの状態がどうして別けられているのか、 といつも疑問に思うことになる。
他のパターンとの関係
-
RAM を節約するために、 Composite ツリーの共有リーフ・ノードを Flyweights として実装できます。
-
Flyweight は多くの小さなオブジェクトを作る方法についてですが、 Facade はサブシステム全体を表す単一のオブジェクトを作る方法に関してです。
-
Flyweight で、 共有状態の全部を一つのフライウェイト・オブジェクトに何らかの方法で削減できた場合、 それは Singleton に似たものになります。 しかし、 この二つのパターンには、 根本的な違いが二箇所あります。
- Singleton のインスタンスは一つだけですが、 Flyweight クラスは、 異なる内因的状態を持つ複数のインスタンスがある可能性があります。
- Singleton オブジェクトは変更可能かもしれませんが、 Flyweight のオブジェクトは不変です。