Javaを鍛える|デザインパターン全23回 #12

Flyweight:大量のラベル(タグ)オブジェクトの共有

  • Java
  • デザインパターン
  • Flyweight
  • Javaを鍛える

📋 この記事の目次
  1. Flyweightは何のためにあるか
  2. Before:同じ内容でも毎回新規作成してしまう
  3. After:Factoryでタグを名前ごとに使い回す
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftでは、チケットに「緊急」「フロントエンド」のようなタグを付けられます。タグはTagクラスとして表現されていますが、同じ名前のタグでも、チケットに付けるたびに新しいオブジェクトが作られています。

public class Tag {
    private final String name;
    private final String colorCode;

    public Tag(String name, String colorCode) {
        System.out.println("[Tag] 新規作成: " + name);
        this.name = name;
        this.colorCode = colorCode;
    }

    public String render() {
        return "#" + name;
    }
}

チケットの数が数千件になると、「緊急」というタグ名だけで数千個のTagオブジェクトが作られることになります。タグの種類自体は数十種類程度しかないのに、実質的に同じ内容のオブジェクトが大量に複製されている状態です。

Flyweightは何のためにあるか

Flyweightは、「内容が同じオブジェクトを使い回し、無駄な複製を避ける」 パターンです。会議室に置く共通の椅子を、参加者ごとに新品を用意するのではなく、同じ椅子を使い回すようなものです。

Before:同じ内容でも毎回新規作成してしまう

public class Main {
    public static void main(String[] args) {
        Tag t1 = new Tag("緊急", "#ff0000");
        Tag t2 = new Tag("緊急", "#ff0000"); // 同じタグなのに、また新規オブジェクトが作られる
        Tag t3 = new Tag("フロントエンド", "#0000ff");

        System.out.println(t1.render());
        System.out.println(t2.render());
        System.out.println(t3.render());
        System.out.println("t1 == t2: " + (t1 == t2));
    }
}

実行結果:

[Tag] 新規作成: 緊急
[Tag] 新規作成: 緊急
[Tag] 新規作成: フロントエンド
#緊急
#緊急
#フロントエンド
t1 == t2: false

「緊急」というまったく同じ内容のタグなのに、t1t2は別々のオブジェクトです(==の結果がfalse)。チケット件数が増えるほど、この無駄な複製も増えていきます。

After:Factoryでタグを名前ごとに使い回す

import java.util.HashMap;
import java.util.Map;

public class TagFactory {
    private final Map<String, Tag> cache = new HashMap<>();

    public Tag getTag(String name, String colorCode) {
        if (!cache.containsKey(name)) {
            cache.put(name, new Tag(name, colorCode));
        }
        return cache.get(name);
    }
}
public class Main {
    public static void main(String[] args) {
        TagFactory factory = new TagFactory();

        Tag t1 = factory.getTag("緊急", "#ff0000");
        Tag t2 = factory.getTag("緊急", "#ff0000"); // 同じ名前は同じインスタンスを共有する
        Tag t3 = factory.getTag("フロントエンド", "#0000ff");

        System.out.println(t1.render());
        System.out.println(t2.render());
        System.out.println(t3.render());
        System.out.println("t1 == t2: " + (t1 == t2));
    }
}

実行結果:

[Tag] 新規作成: 緊急
[Tag] 新規作成: フロントエンド
#緊急
#緊急
#フロントエンド
t1 == t2: true

「新規作成」のログが2回(「緊急」と「フロントエンド」でそれぞれ1回ずつ)に減り、t1t2は同じインスタンスを共有するようになりました(==の結果がtrue)。チケットが何千件あっても、実際に作られるTagオブジェクトはタグの種類の数(今回で言えば2個)だけで済みます。

🧓 ベテランの現場コラム Flyweightは、オブジェクトが「不変(一度作ったら中身が変わらない)」であることが前提になります。もしTagcolorCodeを後から変更できる作りにしてしまうと、1箇所での変更が、そのタグを共有している他のすべてのチケットに影響してしまいます。Flyweightを使うときは、共有するオブジェクトを不変にしておくこととセットで考えましょう。

こんな場面で使う/使わない

  • 使う: 同じ内容のオブジェクトが大量に作られ、メモリや生成コストが無視できない場合
  • 使わない: オブジェクトの数がそもそも少ない、またはオブジェクトごとに状態が変化する必要がある場合。無理に共有すると、意図しない副作用が起きる

まとめ

  • Flyweightは「内容が同じオブジェクトを使い回し、無駄な複製を避ける」パターン
  • Factory役のクラスが、名前などのキーをもとにオブジェクトをキャッシュして返す
  • 共有するオブジェクトは不変にしておくのが安全

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。