ドラクエで学ぶオブジェクト指向(2) 勇者とは唯一無二の存在なり
前回のコラムでは勇者を定義した。しかし、まだまだ課題は山積みである。ひとつずつ片付けていこうと思う。設計と実装を同時に行えるのも趣味のプログラムならではの魅力である。まだ、わたしの頭の中にしか設計図は存在しない。ぶっちゃけ、完成するか破綻するのか分からない。それも含めて楽しんでもらえるとうれしい。
■Singletonパターン
勇者インスタンスが複数作られては困る。Singleton パターンを適用して勇者は世界でただ1人を表現したい。まずは勇者クラスをstaticで宣言し、クラス変数とする。
private static Hero hero = null; |
インスタンス生成に制限をかけたい。コンストラクタをprivateにして外部から直接生成されることを防ぐ。
private Hero(String name) |
インスタンスを返すメソッドは専用のメソッドを作成。static変数へはクラス名.static変数名でアクセスできる。インスタンスの生成に名前が必要なので引数に名前を指定する。このメソッドはstaticにしてあるから、Hero.getInstance("えにくす")でいつでも呼び出せる。
public static synchronized Hero getInstance(String name){ |
getInstance(String name)が2回呼ばれた場合は、先勝ちで名前が決定される。2回目にこのメソッドを呼ぶ場合は例外を投げるような処理にしてもいいのかもしれない。
synchronizedを指定することによって、誰かがメソッドを呼び出している間は他のスレッドからの呼び出しは待機される。複数スレッドからの同時呼び出した場合にインスタンス生成が重複する可能性を排除する。
■オーバーロード
毎回、名前を入力しなければならない。面倒である。それに、2回目以降の呼び出しで不要な情報を与えることは避けたい。引数なしのメソッドも追加しよう。getInstance()でインスタンスの取得はできる。しかし、名前が決まらないので生成はできない。
public static Hero getInstance(){ |
生成しないので同期をとる必要はない。synchronizedの指定はなしとする。
■Singletonを別の方法で実装する
そもそも、同期化コストを支払ってまでスレッドセーフにする必要があるだろうか? という疑問に立ち返る。デフォルトコンストラクタを追加。staticフィールドの初期化でインスタンス生成を行うようにする。
private static Hero hero = new Hero(); |
このままでは名前の設定ができない。引数付きコンストラクタを初期化メソッドに変更する。
public boolean init(String name){ |
戻り値をbooleanに変更してみた。voidでもよいかもしれない。二重の初期化を防止するためにnameフィールドをフラグにしてチェックをかける。
public boolean init(String name){ |
■使う側に(なるべく)負担をかけない
このメソッドは原則、1回だけしか呼ばれないのであまり深く考えていない。……待て。staticにしないとgetInstance()で生成してからインスタンスでinit(name)という順序で呼び出さなければならない。それ以前にgetInstance()で毎回、例外が発生する。init()もstaticにするべきだ。
public static Hero init(String name){ |
こうなるとFactoryにしたいのでHeroクラスを返す。
synchronized Hero getInstance(String name)は不要になったので削除。あわせて、引数なしgetInstance()の条件チェックも変更
public static Hero getInstance(){ |
init()を呼ばないで、getInstance()を実行した場合は例外を返す。NullPointerExceptionは適切ではないだろう。何か他の例外を検討すべきであるが、例外設計は先送りにしているので今は考えない。
初回呼び出しは、init()で名前登録してインスタンス生成
h = Hero.init("ああああ"); |
2回目以降はinit()で生成したインスタンスを取得する。
h = Hero.getInstance(); |
同期コストのかからないSingletonパターンで修正した。getInstance()を使う前に、必ずinit()を呼ばなければいけないという制約がブサイクではある。しかし、最初に考えていたgetInstance()のオーバーロードも同じようにブサイクなので五十歩百歩だろう。init()はゲーム開始時の名前登録のときに呼び出され、必ず初期化されているという前提でかまわないはずだ。
■セーブするために
最後に、復活の呪文や冒険の書でシリアライズ化するので直列化可能にしておくことも忘れない。
public class Hero implements Serializable |
どこか遠くからレベルアップを告げるアラート音が鳴り響く……。