はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回は、構造に関するパターンの中のFlyweight(フライウェイト)パターンを解説します。
大量のオブジェクトによるメモリ消費を抑えるための、やや特殊な位置づけのパターンです。
解決したい課題
大量の似たようなオブジェクトを生成すると、メモリ使用量が問題になることがあります。
例えば、テキストエディタで「1文字ずつをオブジェクトとして表現する」ケースを考えます。
1文字ごとに「文字そのもの」「フォント名」「フォントサイズ」「色」といった情報を持つオブジェクトを作ると仮定すると、10万文字のドキュメントでは10万個のオブジェクトが生成されます。
しかし実際には、「フォント名」「フォントサイズ」「色」といった書式情報は、多くの文字で同じ値を共有していることがほとんどです。
それにもかかわらず、文字1つ1つが書式情報を個別に保持していると、同じ内容のデータが大量に重複してメモリを消費してしまいます。
Flyweightパターンは、複数のオブジェクト間で共有できるデータ(不変な情報)を1つのオブジェクトにまとめ、複数の箇所から共有して使い回すことで、メモリ使用量を削減します。
クラス構成
- Flyweight(フライウェイト): 複数のオブジェクト間で共有される、不変(イミュータブル)な情報を持つクラス
- FlyweightFactory(フライウェイトファクトリー): Flyweightのインスタンスを管理し、同じ内容のFlyweightが要求されたら、新規生成せず既存のインスタンスを再利用して返すクラス
- Context(コンテキスト): Flyweightへの参照と、共有されない固有の情報(状態)を持つ、利用側のクラス
Flyweightが持つ情報のうち、複数のオブジェクトで共有可能な不変の情報を「内部状態(intrinsic state)」、個々のオブジェクトごとに異なる情報を「外部状態(extrinsic state)」と呼びます。
Javaでの実装例
先ほどのテキストエディタの例で実装してみます。
Flyweight:共有される書式情報(内部状態)
import java.util.Objects;
public final class CharacterStyle {
private final String fontName;
private final int fontSize;
private final String color;
public CharacterStyle(String fontName, int fontSize, String color) {
this.fontName = fontName;
this.fontSize = fontSize;
this.color = color;
}
public void render(char c, int x, int y) {
System.out.printf("'%c' を (%d, %d) に [%s, %dpt, %s] で描画%n",
c, x, y, fontName, fontSize, color);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CharacterStyle)) return false;
CharacterStyle that = (CharacterStyle) o;
return fontSize == that.fontSize
&& fontName.equals(that.fontName)
&& color.equals(that.color);
}
@Override
public int hashCode() {
return Objects.hash(fontName, fontSize, color);
}
}
FlyweightFactory:同じ内容のFlyweightを共有・再利用するファクトリー
import java.util.HashMap;
import java.util.Map;
public class CharacterStyleFactory {
// 同じ書式(fontName+fontSize+color)のCharacterStyleを、キャッシュして再利用する
private final Map<String, CharacterStyle> cache = new HashMap<>();
public CharacterStyle getStyle(String fontName, int fontSize, String color) {
String key = fontName + "_" + fontSize + "_" + color;
// すでに同じ書式のインスタンスがあれば、それを再利用する(新規生成しない)
return cache.computeIfAbsent(key, k -> {
System.out.println("[Factory] 新規に CharacterStyle を生成: " + key);
return new CharacterStyle(fontName, fontSize, color);
});
}
public int getCachedStyleCount() {
return cache.size();
}
}
Context:個々の文字ごとに異なる情報(外部状態)を持つクラス
public class CharacterContext {
private final char character; // 外部状態: 文字そのもの
private final int x; // 外部状態: 表示位置X
private final int y; // 外部状態: 表示位置Y
private final CharacterStyle style; // 内部状態への参照(共有されるオブジェクト)
public CharacterContext(char character, int x, int y, CharacterStyle style) {
this.character = character;
this.x = x;
this.y = y;
this.style = style;
}
public void render() {
style.render(character, x, y);
}
}
利用側のコード
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
CharacterStyleFactory factory = new CharacterStyleFactory();
List<CharacterContext> document = new ArrayList<>();
String text = "Hello";
int x = 0;
for (char c : text.toCharArray()) {
// 全ての文字が「Arial, 12pt, black」という同じ書式を使う想定
CharacterStyle style = factory.getStyle("Arial", 12, "black");
document.add(new CharacterContext(c, x, 0, style));
x += 10;
}
for (CharacterContext ctx : document) {
ctx.render();
}
System.out.println();
System.out.println("文字数: " + document.size() + "文字");
System.out.println("実際に生成されたCharacterStyleの数: " + factory.getCachedStyleCount());
}
}
実行結果:
[Factory] 新規に CharacterStyle を生成: Arial_12_black
'H' を (0, 0) に [Arial, 12pt, black] で描画
'e' を (10, 0) に [Arial, 12pt, black] で描画
'l' を (20, 0) に [Arial, 12pt, black] で描画
'l' を (30, 0) に [Arial, 12pt, black] で描画
'o' を (40, 0) に [Arial, 12pt, black] で描画
文字数: 5文字
実際に生成されたCharacterStyleの数: 1
5文字のCharacterContextが作られていますが、内部で共有しているCharacterStyleのインスタンスはたった1つしか生成されていません。
[Factory] 新規に CharacterStyle を生成というログが1回しか表示されていないことからも分かる通り、2文字目以降はFlyweightFactoryが既存のインスタンスを再利用しています。
文字数(10万文字など)が増えても、書式のパターンが少数であれば、CharacterStyleのインスタンス数はそのパターン数までしか増えません。
使用場面
- 大量の類似オブジェクトを生成する必要があり、かつそれらの多くが共通のデータ(内部状態)を持つ場合
- テキストエディタの文字ごとの書式情報、地図アプリのアイコン(同じ種類のマーカーを大量に表示する場合)、ゲームにおける大量の弾丸・パーティクルの見た目情報など
- メモリ使用量がボトルネックになっていて、共有可能な不変データを分離することで削減できる余地がある場合
注意点
Flyweightパターンは、「内部状態(共有可能)」と「外部状態(個別)」を明確に分離できることが前提となります。
この分離を無理に行うと、かえってコードが複雑になり、可読性が下がることがあります。
また、近年はメモリそのものが安価になっているため、明確なパフォーマンス上の必要性がない限り、最初から適用を狙うパターンではなく、実際にメモリ使用量が問題になった際の改善策として検討されることが多いパターンです。
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 大量の類似オブジェクトを生成することによる、メモリ使用量の増大 |
| 実現方法 | 共有可能な不変データ(内部状態)をFlyweightとして切り出し、ファクトリー経由で使い回す |
| メリット | オブジェクトの実質的な生成数を、共有可能なパターンの数まで抑えられる |
| 適用場面 | 実際にメモリ使用量が問題になった場合の改善策として検討されることが多い |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


