デザインパターンFlyweightについて勉強した

flyweightパターンとは

ボクシングなどのフライ級を意味します。
プログラミングでは軽量化と言ったような意味になります。

クラスを扱うにはインスタンスを生成します。
プログラムにもよりますが、オブジェクトが千や万も作られることもあるかもしれません。
また、オブジェクト数はそれほど多くはないけれど、一つ一つのオブジェクトのメモリ量が多いこともあります。
オブジェクトの数が多いほどpcのメモリは圧迫され、動作が遅くなっていきます。

メモリを節約するにはどうすればいいでしょうか。
その方法の一つに、同じ内容のオブジェクトは作らずに使い回すことが考えられます。

例えば画面上に同じ写真を10個並べて表示するプログラムを作ったとしましょう。
同じ画像ファイルを10回読み込むのでは明らかに無駄です。
読み込みは1回だけにして、残り9個は最初に読み込んだオブジェクトの参照を渡してやればいいのです。

コード例

ここではランダムに文字を選択して並べ、特定の文字列が揃ったらあたりになるプログラムを作ってみます。

CharItemクラス

public class CharItem {

	private char item ;

	public CharItem(char item) {
		this.item = item;
	}

	public char getItem() {
		return item;
	}

	@Override
	public String toString() {
		return ">>>  " + item + "  <<<";
	}
}

文字一つを表すクラスです。
既にcharクラスがあるので本当は不要ですが、説明のために作りました。
このクラスが何か重いデータを持っている(gif画像データなど)と考えてください。

ItemFactoryクラス

public class ItemFactory {

	private static ItemFactory factory = new ItemFactory();
	private HashMap<Character,CharItem> items = new HashMap<Character,CharItem>();

	private ItemFactory() {

	}

	public static ItemFactory getInstance() {
		return factory;
	}

	public CharItem createItem(char c) {
		CharItem item = items.get(c);
		if(item==null) {
			item = new CharItem(c);
			items.put(c, item);
		}

		return item;
	}
}

CharItemクラスを作成するためのクラスです。
シングルトンパターンになっています。

itemsフィールドは既に作成済みのCharItemを保持しておくためのものです。

createItemメソッドは、渡されたcharを元にCharItemを返します。
既に作成済みであればitemsフィールドに保持されたものを返し、未作成であれば新たにオブジェクトを生成しています。
このメソッドがflyweightパターンの本体です。
使いまわせるものがあればそれを使い、なければ新しく作ることで無駄なメモリの使用を抑えようとしています。

実装

ItemStackクラス

public class ItemStack {

	private CharItem[] items = new CharItem[0];
	private char[] elements = new char[] {
			'O','C','H','I','N',
	};
	private String[] lucky_text = new String[] {
	};

	public boolean randomItems(int size) {
		Random random = new Random();
		ItemFactory factory = ItemFactory.getInstance();

		items = new CharItem[size];
		try {
			for(int i=0;i<size;i++) {
				items[i] = factory.createItem(elements[random.nextInt(elements.length)]);
				System.out.println(items[i]);
				Thread.sleep(500);
			}
		}catch(InterruptedException e) {
			e.printStackTrace();
		}

		lucky:
		for(String text : lucky_text) {
			if(text.length()!=size) continue;
			for(int i=0;i<size;i++) {
				if(items[i].getItem()!=text.charAt(i)) continue lucky;
			}

			System.out.println("おめでとうございます!!!!");
			return true;
		}

		System.out.println("残念...");
		return false;
	}

	public boolean randomItems() {
		return randomItems(9);
	}

	public void auto() {
		while(!randomItems());
	}
}

特定のcharからランダムにいくつか選択し、特定の文字列になったらあたりになるゲームです。

itemsフィールドは、最後に選択されたCharItemの羅列を保持します。
elementsフィールドは選択できるcharです。
lucky_textフィールドは当たりの文字列です。elementsフィールドに含まれる文字で好きな文字列を入力してください。

randomItemsメソッドは、渡された数のCharItemの列を作り、それがあたりかどうかを判定しています。
forの中でCharItemを生成しています。factory.createItem(elements[random.nextInt(elements.length)]);
これで、既に作られているものと同じオブジェクトは新たに生成されず、以前に作ったものを使いまわすようになります。

randomItemsメソッドはオーバロードされています。
引数なしの場合はデフォルトで9文字の文字列をランダム生成します。

autoメソッドは、当たりが出るまで回し続けるメソッドです。

実行

public class Main {

	public Main() {
		ItemStack stack = new ItemStack();

		//stack.randomItems();
		stack.auto();
	}

	public static void main(String[] args) {
		new Main();
	}
}

stack.autoメソッドで当たりが出るまで回し続けます。
randomItemsに変えると、1回の実行ごとに1回だけになります。

特徴

メモリ量や、インスタンス生成の時間を節約したい時に使用するパターンです。

オブジェクトを使い回すので、あちらのオブジェクトをちょっと変更したら別のところにあるオブジェクトも変更されてしまいます。
冒頭の例で言えば、並べた1つ目の画像だけに落書きをしたつもりが、10個全部の画像が書き変わってしまった、というような状態です。
この場合は落書き情報はflyweightとは別に管理するなどの方法が考えられます。

まとめ

今回の例ではあまり恩恵は感じられなかったかもしれません。
しかしこれをGUI上の表示にして、gif画像のような大きな画像データを表示させ、文字ごとに音を出すなどのように重くする方法はいくらでも考えられます。

数年前と比べればpcのメモリは格段に増えています。
しかし同時に数年前よりも扱うデータ量もまた増えています。
富豪的なコードはほどほどにして、節約できるところは節約していきましょう。