📑

デザインパターンを3つの教材で学習|Flyweightパターン

2022/08/01に公開

学習ソース

デザインパターンを複数の視点で学ぶため、以下の書籍とUdemyで学習している。パターン毎に上から順に進めている。

Flyweightパターン(フライウェイトパターン)とは?

flyweightとは、ボクシングのフライ級(軽量級)の意味。
一つのインスタンスを再利用することで、省リソース化(軽く)するパターン。
主目的はメモリ消費を抑えることだが、例えばファイルを読み込む必要があるというように、インスタンス生成に処理時間がかかったりネットワーク等でインスタンスの転送が大量に発生してしまう場合には、結果的に処理時間を短縮する効果もある。

注意点は、インスタンスを再利用するため、インスタンスの状態が変更されると全てに変更が波及する点。そのため、「intrinsicな情報(場所や状況に依存せず共有できる)/extrinsicな情報(場所や状況に依存するため共有できない)」に分けておく必要がある。Game Programming Patternsのように言い換えると、intrinsicな情報は「状況非依存 (context-free)」、extrinsicな情報は「インスタンス固有」のもの。

Flyweightパターンが使える場面

以下のような機能を実装したいときに使えると理解した。ゲーム開発ではよく使われる手法らしい。

  • 3Dゲームにおける木々のポリゴン。数千本の木があり、そのそれぞれが数千のポリゴンを持つため、メモリ消費量が大きく、更には1フレーム間でGPUに送ることもできない。木はほぼ同じように見えるため、樹皮や葉のテクスチャや幹・枝・葉などの形状を定義するポリゴンのメッシュを共通モデルとして抜き出し、それ以外の森の中での位置や向き、色などをそれぞれの木のインスタンスに持たせることで、大部分が共通化される。
  • ゲームにおける地面の移動コストやテクスチャ、ボートで渡ることのできる水系地形かどうかを示すフラグ(ドラクエだと毒沼やマグマ等のテクスチャや受けるダメージ、三国志だと草地や湿地等のテクスチャや移動コスト)。
  • Webページの背景画像。背景画像は小さな画像の集まりだったりするが、背景で表示される回数分ネットワーク越しにやり取りされるわけではなく、画像を1回取得してその画像を並べて表示する。
  • ファイルやデータベースから読み取った値を大量のインスタンスで使おうとしている場合。

Flyweightパターンを使わない場合の例

Game Programming Patternsより。ゲーム世界の地形タイルの配置をWorldクラスで保持する例。

enum Terrain
{
	TERRAIN_GRASS,
	TERRAIN_HILL,
	TERRAIN_RIVER,
}

class World
{
private:
	Terrain tiles_[WIDTH][HEIGHT];
};

int World::getMovementCost(int x, int y)
{
	switch (tiles_[x][y])
	{
		case TERRAIN_GRASS: return 1;
		case TERRAIN_HILL: return 3;
		case TERRAIN_RIVER: return 2;
	}
}

bool World::isWater(int x, int y)
{
	switch (tiles_[x][y])
	{
		case TERRAIN_GRASS: return false;
		case TERRAIN_HILL: return false;
		case TERRAIN_RIVER: return true;
	}
}

これで動作はするが、移動コストと水系地形のフラグは地形に関するデータなのにコードに埋め込んでしまっている。そこで、Terrainクラスを作る。

class Terrain
{
public:
	Terrain(int moveCost, bool isWater, Texture texture)
	: moveCost_(moveCost),
	  isWater_(isWater),
	  texture_(texture)
	{}
	int getMoveCost() const { return moveCost_; }
	bool isWater() const { return isWater_; }
	const Texture& getTexture() const { return texture_; }
private:
	int moveCost_;
	bool isWater_;
	Texture texture_;
};

これだけだと、各地形タイルはそれぞれでTerrainインスタンスを持つ必要がある。

Flyweightパターンを使った例(ポインター+静的割当)

Game Programming Patternsより。

class World
{
	Terrain* tiles_[WIDTH][HEIGHT];
};

これで、同じ地形の各タイルは、同一の地形インスタンスを指すようにできる。
地形のインスタンスは複数の場所で使用されるため動的に割り当てることにすると、その存続期間の管理がやや複雑になる。そのため、動的な割り当ては行わずにWorldクラスの中に直接保存するようにする。

class World
{
public:
	World()
	: grassTerrain_(1, false, GRASS_TEXTURE),
	  hillTerrain_(3, false, HILL_TEXTURE),
	  riverTerrain(2, true, RIVER_TEXTURE)
	{}
private:
	Terrain grassTerrain_;
	Terrain hillTerrain_;
	Terrain riverTerrain_;
};
tiles_[x][y] = &hillTerrain_;
tiles_[x+1][y+1] = &grassTerrain_;

のようにすれば、各タイルはTerrainオブジェクトへのポインタとなるため、共通化される。

C++のようにポインタが使えて、今回のように動的な割り当てを行わない場合は、上記方法で良いが、一般的にはFactoryパターンを使った方が良い。

Flyweightパターンを使った例(Factoryを使う方法)

一般的には、こちらの方法を使う方が多いと思われる。
Udemyより。

class User:
  def __init__(self, name='', age=''):
    self.__name = name
    self.__age = age

  @property
  def name(self):
    return self.__name

  @name.setter
  def name(self, name):
    self.__name = name

  @property
  def age(self):
    return self.__age

  @age.setter
  def age(self, age):
    self.__age = age

  def __str__(self):
    return f'name: {self.name}, age: {self.age}'

# FlyWeightFactory
class UserFactory:
    __instances = {}

    @classmethod
    def get_instance(cls, id):
      if id not in cls.__instances:
        user = User()
        cls.__instances[id] = user
        return user
      return cls.__instances.get(id)

user1 = UserFactory.get_instance(1)
user1.age = 10
user1.name = "hanako"
user2 = UserFactory.get_instance(2)
user3 = UserFactory.get_instance(1)

print(id(user1))
print(id(user2))
print(id(user3))

print(user1)
print(user2)
print(user3)

実行結果は、下記通りuser1とuser3が共通化されていることがわかる。user1の変更がuser3にも波及している。実際には、上記例のようにsetterで値を設定できるようにするのは望ましくはないと思うが、簡易的な例として。

4309405600
4309405456
4309405600
name: hanako, age: 10
name: , age: 
name: hanako, age: 10

このように、Factoryでインスタンスを生成するようにし、Factoryクラス内で生成済みのインスタンスを持つようにしておけば、既に生成済みのインスタンスが来たら確保済みのインスタンスを返し、未生成のものが来たら新たに作成して返すことができる。

上記例では、instances_ dictionaryに保存していっているが、poolという言葉が使われることも多そう(Java言語で学ぶデザインパターン入門ではpool)。

poolに関連して、オブジェクトプールパターン(GoFではない)がある。Game Programming Patternsによると、火花のようなパーティクルを表示する場合は、パーティクルを何百、何千と生成・破棄する必要があるが、そうするとメモリフラグメンテーションを起こす場合があるらしい。ゲーム機などはPCに比べるとメモリが少ない。ヒープ領域に複数のオブジェクトを割り当てて削除するといったことを繰り返すと、車の縦列駐車のようにヒープ領域は使っている部分同士の間が空いた状態となってしまう。つまり、大きなデータをヒープに配置したいとき、メモリ自体は余っているのにそのデータはヒープに配置できなくなってしまう。そのような場合にオブジェクトプールクラスを作る。各オブジェクトは「使用中」かどうかの問合せに対して現在有効なオブジェクトであるかどうかを返す。プールを初期化する際にはオブジェクトの集合全体を生成し(通常は単一の連続したメモリ領域)、すべてを「未使用」状態に初期化する。新しいオブジェクトが欲しいときは、プールに依頼する。プールは使用可能なオブジェクトを見つけ出し、「使用中」に設定し、戻り値として返す。オブジェクトが不要になったときには「未使用」状態に戻す。このようにして、メモリ割当やその他のリソースを必要とせずにオブジェクトを自由に生成し破壊することができる。

Flyweightパターンもオブジェクトプールパターンもどちらも「再利用」するものだが、意味合いが異なる。Flyweightオブジェクトは複数のオーナーが同時に同じインスタンスを共用するという形で再利用される。同じオブジェクトを複数の場所で使うことで、メモリの重複利用を避けている。オブジェクトプール内のオブジェクトも再利用されるが、同時に使われることはなく、時間経過の中で再度使われるだけ。初めの所有者が使い終わったオブジェクトのメモリを再び使用するというもの。

Flyweightパターンの登場人物

Java言語で学ぶデザインパターン入門より。

  • Flyweight(フライ級)役: 普通に扱うとプログラムが重くなるため共有した方が良いもの。
  • FlyweightFactory(フライ級の工場)役: Flyweight役を作る工場。この工場を使ってFlyweight役を作ると、インスタンスが共有される。
  • Client(依頼者)役: FlyweightFactory役を使ってFlyweight役を生成し、それを利用する。

感想

Factoryを使わずにポインターで共有する方法は、Flyweightという言葉を使わずにC/C++では自然とやっていた方法のように思える。しかし、パターンとして覚えておくことで、議論の中で共通認識がとれるため、学ぶ価値はある。Game Programming Patternsでは、今回はFactoryを使ったコード例が確認できなかったが、UdemyとJava言語で学ぶデザインパターン入門ではFactoryを使ったコード例が確認できた。

Udemyでは、Flyweightオブジェクトがsetterを持っていてミュータブルだったが、実際にはイミュータブルにするべきなのであろうと理解した。実際、
https://stackoverflow.com/questions/30422525/what-are-the-differences-between-flyweight-and-object-pool-patterns
の回答にも、

Flyweight must either be immutable (the best option), or implement thread safety. (Frankly, I'm not sure if a mutable Flyweight is still Flyweight :))

と、イミュータブルがベストだろうと記載があった。

オブジェクトプールについてGoogle検索すると、Unityの記事が大量にHitした。弾幕ゲームで大量に弾を発射する場合、弾オブジェクトを数百、数千と必要になり、それらの生成・破棄が繰り返される。そうした場合にはオブジェクトプールが便利のようだった。無双シリーズのような大量の雑魚敵がいるものや、ピクミン等のように大量の似たキャラクターがいるゲーム等ではオブジェクトプールが使われているのかなと思った。

オブジェクトプールとFlyweightについてまとめている記事があった。
https://zenn.dev/shztmk/scraps/477ce1372d5e53

3種類のソースから学習し、他のサイトも参考にすることで、1つのソースだけからでは気付けなかった所も気付けたため、複数の本やUdemyで学習することは、デザインパターン初学者の方にはオススメする。本当は、原書オブジェクト指向のこころも読むとより理解が進むと思うが、抽象的でなかなか理解するのが難しいとのことなので、まずはデザインパターンをいわゆる「完全に理解した」と思った後にでも読もうと思う。ひとまずはこの3つのソースから学習することとする。いずれ有名なHead Firstは読もうと思っている。2022年6月に第2版が出たのでちょうど良さそうだとは感じている。

参考文献

デザインパターン関係で読んでいる、または読みたいものリスト

Discussion