はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第14回目です。
今回は、大量のオブジェクトを効率的に管理し、メモリ使用量を削減するためのFlyweight(フライウェイト)パターンについて解説します。
Flyweightパターンとは?
Flyweightパターンは、多くのオブジェクトが持つ共通の状態を共有することで、メモリ使用量を劇的に削減する構造パターンです。これは、オブジェクトが**本質的な状態(intrinsic state)と外的な状態(extrinsic state)**に分けられる場合に特に有効です。
- 本質的な状態(intrinsic state): オブジェクト内部にあり、他のオブジェクトと共有できる状態。例:木のモデルデータ、文字の色。
- 外的な状態(extrinsic state): オブジェクトの外部にあり、共有できない状態。実行時にクライアントから渡される。例:木の座標、文字のフォントサイズ。
例えるなら、電子図書館の蔵書管理です。
同じタイトルの本が複数冊ある場合、本のタイトルや著者名といった本質的な情報(フライウェイト)は一つだけメモリに保持し、各本の貸出状況や物理的な場所といった外的な情報は、個別のオブジェクトとして管理することで、メモリを節約します。
Javaでの実装:厳密な共有とファクトリー
Javaは、オブジェクトのライフサイクルとメモリ管理を厳密に制御できるため、Flyweightパターンを確実に実装できます。このパターンでは、フライウェイトオブジェクトをキャッシュするためのファクトリーが重要な役割を果たします。
このパターンでは、以下のコンポーネントが登場します。
- フライウェイトインターフェース(Flyweight): 本質的な状態を共有するオブジェクトのインターフェース。
- 具象フライウェイトクラス(ConcreteFlyweight): フライウェイトインターフェースを実装し、本質的な状態を保持するクラス。
- フライウェイトファクトリー(FlyweightFactory): 具象フライウェイトオブジェクトを生成し、キャッシュするクラス。クライアントはファクトリーを介してフライウェイトオブジェクトを取得します。
以下に、大量の木を描画するゲームの例を示します。
// JavaでのFlyweightパターンの実装例
import java.util.HashMap;
import java.util.Map;
// 1. フライウェイトインターフェース
interface Tree {
void draw(int x, int y);
}
// 2. 具象フライウェイトクラス (共有される木のモデル)
class ForestTree implements Tree {
private String modelData; // 本質的な状態(Intrinsic State)
public ForestTree(String modelData) {
this.modelData = modelData;
System.out.println("Creating a new ForestTree object for: " + modelData);
}
@Override
public void draw(int x, int y) {
// x, y は外的な状態(Extrinsic State)として受け取る
System.out.println("Drawing tree at (" + x + ", " + y + ") with model: " + modelData);
}
}
// 3. フライウェイトファクトリー
class TreeFactory {
private static final Map<String, Tree> treeCache = new HashMap<>();
public static Tree getTree(String modelData) {
// キャッシュに存在しなければ新規作成し、追加する
if (!treeCache.containsKey(modelData)) {
treeCache.put(modelData, new ForestTree(modelData));
}
return treeCache.get(modelData);
}
}
// 使い方
public class Main {
public static void main(String[] args) {
// 異なる種類の木を取得
Tree oakTree = TreeFactory.getTree("Oak");
Tree pineTree = TreeFactory.getTree("Pine");
// オークの木を複数回取得しても、新しいインスタンスは生成されない
Tree anotherOakTree = TreeFactory.getTree("Oak");
System.out.println("Are the two oak trees the same object? " + (oakTree == anotherOakTree)); // true が出力される
// 大量の木を描画(外的な状態は異なる)
for (int i = 0; i < 100; i++) {
oakTree.draw(i * 10, i * 10);
}
}
}
Javaでは、TreeFactoryがHashMapを使って、キャッシュされたオブジェクトを管理します。これにより、同じモデルの木オブジェクトがメモリ上に一つだけ存在することを保証します。
Pythonでの実装:辞書と動的なオブジェクト生成
Pythonでは、Javaと同様に**辞書(Dictionary)**を使ってフライウェイトをキャッシュします。Pythonはオブジェクトのインスタンス化が比較的軽量ですが、メモリ使用量が問題になるような場面では有効なパターンです。
以下に、同じ木の例をPythonで実装します。
# PythonでのFlyweightパターンの実装例
# 1 & 2. 具象フライウェイトクラス (共通のメソッドを持つ)
class ForestTree:
def __init__(self, model_data):
self.model_data = model_data # 本質的な状態
print(f"Creating a new ForestTree object for: {self.model_data}")
def draw(self, x, y):
# x, y は外的な状態
print(f"Drawing tree at ({x}, {y}) with model: {self.model_data}")
# 3. フライウェイトファクトリー
class TreeFactory:
_tree_cache = {}
@staticmethod
def get_tree(model_data):
if model_data not in TreeFactory._tree_cache:
TreeFactory._tree_cache[model_data] = ForestTree(model_data)
return TreeFactory._tree_cache[model_data]
# 使い方
oak_tree = TreeFactory.get_tree("Oak")
pine_tree = TreeFactory.get_tree("Pine")
# オークの木を再度取得しても、新しいインスタンスは生成されない
another_oak_tree = TreeFactory.get_tree("Oak")
print(f"Are the two oak trees the same object? {oak_tree is another_oak_tree}") # True が出力される
# 大量の木を描画
for i in range(100):
oak_tree.draw(i * 10, i * 10)
Pythonの実装も、TreeFactoryクラスの静的メソッドと辞書を使い、同じモデルの木が複数回生成されないようにしています。Javaと比べると、インターフェースの定義が不要なため、より簡潔に書くことができます。
まとめ:本質は「状態の分離と共有」
| 特性 | Java | Python |
|---|---|---|
| 主な解決策 |
Map を使ったキャッシュとファクトリークラス |
dict を使ったキャッシュと@staticmethod
|
| 設計思想 | 共有可能な状態と共有できない状態の厳密な分離 | オブジェクトの属性を動的に活用 |
| コードの意図 | 型を通じて共有の意図を明示 | 辞書と静的メソッドでキャッシュを管理 |
Flyweightパターンは、両言語で実装のスタイルは似ていますが、 「オブジェクトの状態を分離し、共有することでメモリを節約する」 という本質は共通です。Javaはインターフェースを介した厳密な共有を、Pythonは辞書と柔軟なオブジェクト構造を使って実現します。
次回からは、オブジェクト間の相互作用を設計する振る舞いパターンに入ります。お楽しみに!
次回のテーマは、「Day 15 振る舞いパターン入門:オブジェクト間のやり取りを設計する」です。