広告

Java : Flyweight パターン (図解/デザインパターン)

Flyweight パターンとは、GoF によって定義されたデザインパターンの1つです。
同じ内容のオブジェクトを再利用して、リソースを節約するという設計です。

本記事では、Flyweight パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

Flyweight パターン(フライウェイト・パターン)とは、GoFによって定義されたデザインパターンの1つである。等価なインスタンスを別々の箇所で使用する際に、一つのインスタンスを再利用することによって計算資源の浪費を減らすことを目的とする。

Flyweight パターンとは、内容の同じオブジェクトが必要となる場合に、

  • 複数のオブジェクトは作らない
  • 1つのオブジェクトを再利用(共有) してリソースを節約する

という設計です。

例えば、

  • 大きなオブジェクトを使う
  • 小さなオブジェクトでも大量に生成する

といったケースで有用でしょう。

Flyweight は、日本語的に発音すると「フライウェイト」となります。
意味は「フライ級」で、ボクシングなどの格闘技で使われる階級の1つです。階級のなかでも体重が軽めの階級ですね。


【 Flyweight パターンのイメージ図 】

Flyweight パターンの図

Flyweight パターンでは、クライアントは ファクトリ を介してオブジェクトを取得します。
オブジェクトを直接生成しないのがポイントですね。

一方、ファクトリは、

  • 必要に応じてオブジェクトを生成
  • もし、同じ内容のオブジェクト取得要求がきたら、作成済みのオブジェクトを返す

という感じになります。

Flyweight パターンは、特に 不変オブジェクト と相性のよいパターンです。
不変オブジェクトとは簡単にいうと、オブジェクトを生成した後に、内容の変更がいっさいできないオブジェクトのことです。

詳しくは下記の記事で解説していますので、そちらもご参照いただけたら幸いです。

クラス図とシーケンス図

クラス図

上の図は、Flyweight パターンの一般的なクラス図です。
右側の Flyweight クラスが、再利用の対象となるオブジェクトになります。

FlyweightFactory クラスの pool フィールドは配列になっていますが、

といった コレクション を使うことも多いと思います。
また、getFlyweight メソッドには、オブジェクトの同一性を判断するためのパラメータが必要になるでしょう。

シーケンス図は次のようになります。

シーケンス図


コード例

先述した Flyweight パターンのクラス図ですが、コード例を作るにあたり、もう少し具体的な名前に変更します。

クラス図2

それぞれのクラスは、

  • ProductFactory → FlyweightFactory
  • Product → Flyweight

に相当します。

Flyweight パターンを使わないケース

まずは、Product クラスを見ていきましょう。
このクラスのオブジェクトは 不変 になります。

public class Product {
    private final String name;
    private final int price;

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "商品 : %s (%d円)".formatted(name, price);
    }
}

Product は "商品" という意味になります。
属性として、名前(name) と 値段(price) を持ちます。

この Product を、Flyweight パターンを使わないで … つまり new 演算子で生成すると次のようになります。

final var product1 = new Product("テーブル", 5000);
final var product2 = new Product("テーブル", 5000);
final var product3 = new Product("椅子", 2000);

// オブジェクトはすべて異なります。
System.out.println(product1 == product2); // false
System.out.println(product1 == product3); // false
System.out.println(product2 == product3); // false

System.out.println(product1); // 商品 : テーブル (5000円)
System.out.println(product2); // 商品 : テーブル (5000円)
System.out.println(product3); // 商品 : 椅子 (2000円)

内容の同じ Product でも、オブジェクト(インスタンス) は別になっていることが分かりますね。
オブジェクトが別ということは、それだけ余計にリソースを消費していることになります。

Flyweight パターンを使うケース

次は、Flyweight パターン を使う例を見てみましょう。

まずは、FlyweightFactory に相当する ProductFactory クラスを作ります。

public class ProductFactory {
    private record Key(String name, int price) {
    }

    private final Map<Key, Product> pool = new HashMap<>();

    public Product get(String name, int price) {
        final var key = new Key(name, price);

        if (pool.containsKey(key)) {
            return pool.get(key);
        } else {
            final var product = new Product(name, price);
            pool.put(key, product);
            return product;
        }
    }
}

今回、pool フィールドは Map で管理することにしました。
Map のキーには、nameprice パラメータを持つ レコードクラス を使います。

get メソッドでは、

  • 内容の同じオブジェクトが pool に存在するか?
    • 存在していれば、それを返す
    • 存在していなければ、新しくオブジェクトを生成して pool に追加

という処理を行っています。

それでは、ProductFactory を使ってオブジェクトを取得してみましょう。

final var factory = new ProductFactory();

final var product1 = factory.get("テーブル", 5000);
final var product2 = factory.get("テーブル", 5000);
final var product3 = factory.get("椅子", 2000);

// "テーブル (5000円)" のオブジェクトは同じになります。
System.out.println(product1 == product2); // true

// "テーブル (5000円)" と "椅子 (2000円)" のオブジェクトは異なります。
System.out.println(product1 == product3); // false
System.out.println(product2 == product3); // false

System.out.println(product1); // 商品 : テーブル (5000円)
System.out.println(product2); // 商品 : テーブル (5000円)
System.out.println(product3); // 商品 : 椅子 (2000円)

無事に、同じ内容のオブジェクトは再利用されていることが確認できました。

注意点

クラス図

FlyweightFactory の pool フィールドには注意が必要です。

pool フィールドは、内容の異なるオブジェクトが増えるほどに、だんだんとサイズが大きくなっていきます。
そして、減ることはありません。ガベージコレクション (GC) の対象にもなりません。

せっかく Flyweight パターンでリソースを節約しているのに、pool のサイズが巨大になっては本末転倒です。

例えば、

  • ある程度のサイズになったら pool をクリアする
  • WeakReference などの参照を使う

といった処理が必要になるかもしれません。

まとめ

Flyweight パターンとは、内容の同じオブジェクトが必要となる場合に、

  • 複数のオブジェクトは作らない
  • 1つのオブジェクトを再利用(共有) してリソースを節約する

という設計です。

ただ、最近はリソースが潤沢な環境も多いため、あまり出番はないかもしれません。
組み込みシステムなどのリソースが限られた環境では、必要になることもありそうです。


関連記事

ページの先頭へ