
本記事ではGoFのデザインパターンのプログラムの構造に関するパターンの一つである「Flyweight」パターンを解説します。このパターンを一言で説明するならば、「1つのオブジェクトを再利用することで計算資源を節約する方法」と言えるでしょう。「Flyweight」パターンをサンプルを踏まえて解説します。
「Flyweight」パターンとは
先述した通り、「Flyweight」とは「1つのオブジェクトを再利用することで計算資源を節約する方法」です。「Flyweight」とはボクシングやレスリングの階級を示す言葉で、軽量級を意味しています。
Flyweightパターンは、メモリ消費を抑えるために多数の小さなオブジェクトを共有して使うデザインパターンです。同一のデータを共有しオブジェクトの生成を最小限に抑えることで、メモリやリソースを軽量に保つことが可能になります。この軽量化という点で「flyweight」(軽量級)という言葉の由来となっています。

「Flyweight」パターンの書き方
本記事では特定のAsciiアートの文字列を生成して、標準出力を行うコードでFlyweightパターンを適用してみます。Asciiアートのテキストとは通常の文字ではなく、さまざまなテキストで大きな文字を作ることを目的とした手法です。例えば「FLYWEIGHT」をAsciiアートのテキストに変換すると以下のようになります。「figlet」コマンドを利用して生成することができます。
>> figlet FLYWEIGHT
_____ _ __ ____ _______ ___ ____ _ _ _____
| ___| | \ \ / /\ \ / / ____|_ _/ ___| | | |_ _|
| |_ | | \ V / \ \ /\ / /| _| | | | _| |_| | | |
| _| | |___| | \ V V / | |___ | | |_| | _ | | |
|_| |_____|_| \_/\_/ |_____|___\____|_| |_| |_|
今回はこの文字列をファイルから読み込んで、オブジェクトとして生成するようなコードをFlywEightパターンを利用して作成します。
まずはAsciiアートテキストのファイルを用意します。このファイルからAsciiアートテキストのオブジェクトを生成します。今回は「A」「B」「a」「b」のAsciiアートを用意しています。下記は「A」のファイルを 「large_A.txt」で用意しています。
large_A.txt
_
/ \
/ _ \
/ ___ \
/_/ \_\
次にAsciiFontクラスを作成します。このクラスは、idとAsciiアートの文字列の2つのフィールドを持っており、idからファイルを検索し、ファイルの中身をString型に変換するメソッドを持っています。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class AsciiFont {
private final String id;
private final String font;
public AsciiFont(String id) {
this.id = id;
this.font = readFont(this.id);
}
public String getFont() {
return font;
}
// 文字ファイルの内容をStringに変換
private String readFont(String id) {
String filePath = "./stamp/" + id + ".txt"; // ファイルのパスを指定する
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
return stringBuilder.toString();
} catch (IOException e) {
return "";
}
}
}
次にこのAsciiFontのオブジェクトを生成して管理する「AsciiFontFactory」クラスを作成します。
import java.util.HashMap;
import java.util.Map;
public class AsciiFontFactory {
public static AsciiFontFactory asciiFontFactory = new AsciiFontFactory();
private Map<String, AsciiFont> asciiFontMap = new HashMap<>();
private AsciiFontFactory() {}
public static AsciiFontFactory getInstance() {
return asciiFontFactory;
}
public synchronized AsciiFont getFont(String id) {
if (asciiFontMap.containsKey(id)) {
System.out.println("メモリからロードします");
return asciiFontMap.get(id);
}
System.out.println("新しくインスタンスを作成します");
AsciiFont target = new AsciiFont(id);
asciiFontMap.put(id, target);
return target;
}
}
このクラスはasciiFontFactory自身とasciiFontMapのフィールドを持っています。このAsciiFontFactoryはSingletonなクラスである必要があります。SingletonとはAsciiFontFactoryクラスから生成したオブジェクトが1つしか存在しないことを保証する手法です。Singletonパターンの詳しい解説は下記の記事で紹介しています。
なぜSingletonである必要があるかというと、それはメモリの消費を抑えるためです。AsciiFontFactoryは全てのAsciiFontのオブジェクトを生成します。仮にAsciiFontFactoryが複数あった場合、それぞれのAsciiFontFactoryオブジェクトが、AsciiFontオブジェクトを持つことになります。AsciiFontFactoryオブジェクトが増えるたびにAsciiFontオブジェクトも増えていきます。これではFlyweightパターンの意味がありません。FlyweightFactoryもただ一つのオブジェクトとして保証することで効力を発揮します。
asciiFontMapのフィールドも持っていますが、これは生成したAsciiFontオブジェクトを保持しておくためのフィールドです。このフィールドにはString型のidをkeyにAsciiFontオブジェクトを保持することが可能です。
getFont()のメソッド内ではまずasciiFontMapから特定のkeyのAsciiFontオブジェクトが存在するかを確認し、もし存在すればそのオブジェクトを返却します。なければ、新しくasciiFontオブジェクトを生成して、asciiFontMapに格納後返却します。このメソッドは様々な箇所から同時に呼ばれる可能性があり、asciiFontMapを同時に書き換える可能性があります。そこで競合を避けるためにsynchronizedなメソッドとしてロックをかける必要があります。
では実際にいくつかのAsciiアートをファイルから読み込んで生成してみましょう。mainメソッドの中で利用してみます。
public class Main {
public static void main(String[] args) {
AsciiFontFactory factory = AsciiFontFactory.getInstance();
AsciiFont A = factory.getFont("large_A");
System.out.println(A.getFont());
AsciiFont B = factory.getFont("large_B");
System.out.println(B.getFont());
AsciiFont a = factory.getFont("small_a");
System.out.println(a.getFont());
AsciiFont b = factory.getFont("small_b");
System.out.println(b.getFont());
AsciiFont A2 = factory.getFont("large_A");
System.out.println(A2.getFont());
AsciiFont B2 = factory.getFont("large_B");
System.out.println(B2.getFont());
AsciiFont a2 = factory.getFont("small_a");
System.out.println(a2.getFont());
AsciiFont b2 = factory.getFont("small_b");
System.out.println(b2.getFont());
// AとA2はFactoryを介しているため同じインスタンスでtrue
System.out.println(A == A2);
// Factoryを介さずAsciiFontのインスタンスを生成
AsciiFont A3 = new AsciiFont("large_A");
// 同じ内容のオブジェクトを重複して生成しているためfalse
System.out.println(A == A3);
}
}
>> 実行結果
新しくインスタンスを作成します
_
/ \
/ _ \
/ ___ \
/_/ \_\
新しくインスタンスを作成します
____
| __ )
| _ \
| |_) |
|____/
新しくインスタンスを作成します
__ _
/ _` |
| (_| |
\__,_|
新しくインスタンスを作成します
_
| |__
| '_ \
| |_) |
|_.__/
メモリからロードします
_
/ \
/ _ \
/ ___ \
/_/ \_\
メモリからロードします
____
| __ )
| _ \
| |_) |
|____/
メモリからロードします
__ _
/ _` |
| (_| |
\__,_|
メモリからロードします
_
| |__
| '_ \
| |_) |
|_.__/
true
false
AsciiFontFactoryのオブジェクトを生成後、それぞれの文字を2回ずつ呼び出しています。1回目の呼び出しでは「新しくインスタンスを作成します」とログが出ていますが、2回目の呼び出しでは「メモリからロードします」と出ています。これでFlyweightパターンを用いて同一のオブジェクトを複数建ててしまうことを防いでいます。
試しに、Factoryを介して取得した同じ文字同士を比較してみます。「A == A2」は同一のオブジェクトの場合trueを返却し、実行結果を確認してもtrueとなっており、同一のオブジェクトということがわかります。逆に、Factoryクラスを介さずに自前で生成した「A3」は「A」のオブジェクトとは出力が一緒でもオブジェクトが異なるため「A == A3」はfalseになっています。
まとめ
本記事で利用したサンプルケースのクラス図は下記になります。

最後に「Flyweight」パターンとは「1つのオブジェクトを再利用することで計算資源を節約する方法」です。このパターンを使うことでメモリやリソースの消費を格段に抑えられる可能性があり、コストの低減やパフォーマンスの向上が期待できるでしょう。
本記事で利用したコードは下記のgit上で公開しております。
コメント