ドラクエで学ぶオブジェクト指向(4) 戦いのプレリュード……の前に

2010/06/27 23:56:48

 大枠が決まったところで、中間ゴールを設定しようと思う。いくら後追いでも、いまのペースではいつまでたっても完成しないだろう。

 適切な目標がなければモノ作りは進まない。「OOPによるドラクエの完全再現」というゴールでは、誰が見てもあまりにも規模が大きい。分割して統治せよ。実現可能なサイズに分割するべきだ。

 お城やダンジョンを作って、フィールドを自由に動き回れるシステムというのもいい。しかし、ドラクエといえばバトルである。まずは、戦闘システムの構築から始めてみようと思う。文字ベースのバトルなら画像を用意しなくてもいいのでロジックに集中できるだろう。

 パーティバトルもあるが、最初は1対1のバトル。勇者対モンスターの構図も取り払うつもりでいる。Heroクラスは考える要素が多いので、しばらく寝かせておく。モンスター対モンスターの戦いから作っていこう。最近ではモンスターズなんてシリーズもあるみたいだ。

 n対nの戦い、対勇者戦、グラフィックやエフェクトの表現などはアジャイル的展開で追加していくことにしよう。真にOOP的な実装ができたなら、想定内の変更は容易なはずである。

 「テキストベースのモンスター対モンスター(1対1)のバトル」

 これをファースト・イテレーションのゴールと定める。

■リファクタリング

 さて、目標をモンスター同士のバトルと定めたので、既存クラスを修正していく。前回、考察した獲得経験値と獲得Gがずっと気になっていた。

Monster.java
/** 倒された相手に与える経験値 */
private int givenEx = 0;
/** 倒された相手に与えるG */
private int givenG = 0;

 『勇者にとって、経験値とGは獲得するものであるが、モンスターの経験値とGは与えるものである。モンスターが勝利しても、勇者の経験値が得られない。同じものでも考え方が違うので、モンスター側ではgivenEx、givenGとした』

 実は、根本的に間違っている感じがする。過ちに気がつけば、さっさと修正するのが吉だろう。モンスター同士のバトルという明確な目標が決まったので、普通に経験値とGは保持することにした。Heroクラスと共通の要素になるので、親クラスのBattlerに持たせておく。

 シンプルにするために、攻撃力と守備力もいったんBattlerクラスに預けておく。このあたりのメソッドは最終的にどうなるかは、いろいろと迷うことになるだろう。

 Battlerクラスのパラメータはこのようになっている。

/** 名前 */
protected String name = null;
/** ヒットポイント */
private int hp = 0;
/** マジックポイント*/
private int mp = 0;
/** 最大ヒットポイント */
protected int maxHp = 0;
/** 最大マジックポイント */
protected int maxMp = 0;
/** すばやさ */
private int quik = 0;
/** ゴールド */
private int g = 0;
/** 経験値 */
private int ex = 0;
/** 攻撃力 */
protected int offense = 0;
/** 守備力 */
protected int deffense = 0;

 max_hpという書き方が気に入らなかったので、微妙に変数名は修正している。納得できるまで名前を変更できるのも、リファクタリング機能のおかげである。そんなことはEclipseでもNetBeansでもVisual Studioでも常識なので特に解説はしない。

 戦闘に関する基本動作をBattlerクラスに持たせると、Monsterクラスには何も処理がなくなった。このクラスはモンスター固有の攻撃や特殊な振る舞いのために残しておくことにする。

■HPを増減させるメソッド

 各フィールドにはsetter/getterと呼ばれるアクセサメソッドを用意した。getterはよいとしてsetterの場合、

 setHp(10)→変数hpに10を代入

という処理となる。戦いは命の削りあいである。10のダメージを与えたら、hpから10を減らしたい。hp += damage のようなメソッドを用意する。

/**
* HPを0から最大HPの範囲で増減させる。
* @param hp
*/
public void addHp(int hp){
    this.hp = ( this.hp + hp < 0 ) ? 0 : ( this.hp + hp > this.maxHp  ) ? this.maxHp: hp;
}

 引数が正の場合はHP回復させる。負の場合はダメージを与えることができる。ただ、addというネーミングがどうもしっくりとこない。ほかによい名前が浮かんだらメソッド名を変更しよう。

 MPなど、ほかのメソッドにも同様のメソッドを追加しておいた。上限のないGなどは少しシンプルな処理になる。がんばればいくらだってお金持ちになれる、というユメのあるメソッドだ。厳密にいうとintの上限が存在するのだが……せめてlongにするべきか迷い中である。

/**
* Gを0以上の範囲で増減させる。
* @param g
*/
public void addG(int g){
    this.g = ( this.g + g < 0 ) ? 0 : g;
}

 お金が足りない場合は、HasNotGoldException を投げてみたい。

■スレッドについて

 HP、MPのsetterがスレッドセーフじゃないというご指摘をいろんな方からいただいた。よくよく考えてみると範囲チェック後、代入する前に別のスレッドが値を書き換える可能性がある。

 ふたつのスレッドA,Bが実行されているとして、setMpメソッドの動きを考えてみる。それぞれが同時にsetMpを実行したとする。

スレッドA 0~最大MPの範囲チェック
スレッドB MPが変更する処理をゴニョゴニョ
スレッドA MPに値を代入

 という順番になったときに スレッドAの範囲チェックが無意味になる。

 ギラの消費MPは2である。例えば、残りMPが2の場合にギラを唱えようとしている瞬間にMPを吸い取るマホトラの呪文がかけられたらどうなるだろう?

 このような場合、マルチスレッド下では、ギラ呪文を唱えるために必要なMP消費と、マホトラで奪われるMPのどちらが優先されるかは実行するまで分からない。MPが吸い取られているにも関わらず、ギラが実行されてしまう可能性もあるのだ。

 synchronizedメソッドを使えば、この問題は回避できる。しかし、synchronizedメソッドは、多用すればするほど全体の処理速度が低下する。本当に必要でない限り乱発するのは避けたい。StringBufferに対するStringBuilderクラスのようにスレッドアンセーフを選ぶという道もあるだろう。

 戦闘は単一スレッドで行うという方針もあるのでこれも保留しようと思う。マルチスレッド時における「同時ギラ-マホトラ問題」と定義しておく。

 それなりにペンディングが増えてきている。放置していると忘れることは間違いないので、簡単に課題管理表なんてものを作ってみた。課題が増えればtracでチケット管理をすることになるかもしれない。

Kadai_dq4_2

 これで下準備はできたと思う。次回はいよいよ攻撃メソッドに着手してみよう。

 どこか遠くからレベルアップを告げるアラート音が鳴り響く……。

hagane_dq_src4.zipをダウンロード

ばーちゃんの百科事典

2010/06/13 22:00:00

 先日、ウチのばーちゃんが米寿を迎えた。お祝いに、ささやかながらお誕生日会を開く。サプライズなので本人には当日までナイショにしていた。

 ばーちゃんの孫である、我が兄弟たち。みんな、それぞれがいい大人になった。協力して地デジ対応の液晶テレビをプレゼントをすることに決めた。最近の薄型テレビは梱包まで薄い。ラッピングをといて、ダンボールを開けて、テレビ本体にご対面するまで、それがテレビであるとなかなか気付いてくれない。「座布団か?」などとトボけたことを言う。

 じっと画面を見つめているばーちゃんは涙ぐんでいた。やはり、地デジは映像がキレイだ。

■記憶

 小学校への入学祝いに、ばーちゃんからプレゼントしてもらったものがある。小学生向けの百科事典。確か全部で15冊ぐらいだったと思う。本棚に納めていても自己主張が激しく、存在感のでかいシリーズだ。

 当時は、ウチもあまり経済的な余裕がなかった。それでも、これからの時代は勉強せなあかんとムリしてくれたらしい。という話を聞いたのは大人になってからのことだ。

 1冊1冊がとても分厚くて、全ページがカラー印刷で、使われている紙も上質だった。あまりにも上等すぎて、油断をすると指を切った。それでも、ページをめくるときはワクワクしていたように思う。目的もなくパラパラと眺めているだけで楽しかった。

■光景

 小学生が自分で買う本といえば、マンガぐらいしか思いつかない。そもそも、子どものわたしには、勉強するために本を買うなんていう発想はどこにもなかった。教科書以外に勉強するための本があることすら想像できないでいた。授業で分からないことがあっても放置。先生に聞くなんてことは滅多にしない。

 そんな態度でいたら、成績は下がるのは当然のこと。だんだんと勉強が分からなくて学校がつまらなくなっていく。楽しみは休み時間と給食の時間だけ。どこにでもいる典型的な小学生である。そうやって子どもの時間は過ぎていく。

 偶然か、思うところがあったのかは覚えていない。分からないことにぶつかると、事典をめくるようになっていた。ページをたどっていくと必ず答えがそこにある。先生に聞くよりも分かりやすくて、わたしの疑問にトコトンまで付き合ってくれる。

 ずっと続けていくと、分からないことなんてなくなる。成績もそれなりによくなった。授業がおもしろい。

■限界

 心強い味方である百科事典。しかし、小学生向けの事典に英語なんてものはなかった。いつまでも、通用するわけでもなく、進学するにつれて苦しくなる。だんだんと、授業の内容も高度になり百科事典の範囲を超えていく。事典なんてものはそうそうカンタンにアップデートできないものだ。

 中学生ぐらいまでは、勉強を続けていた貯金でなんとかやりくりができていた。さすがに高校になるともうキビシイ。まあ、結果はだいたいご想像のとおり。

 そんなこんなで、大人になった。本は好きなほうだったが、勉強というものに無縁でいた。どうやって勉強したらいいかも分からないし、どうして勉強するのかも分からない。そもそも、勉強をするつもりがなかった。

 バンドを続けていたので音楽のことだけはいろいろと勉強をしていた。理論も体系もなく、目に付いた音楽書を買っては読み漁っているだけの趣味の独学。目的もぼんやりとしているので、何のためにしているのかよく分からなかった。

■突破

 しばらくして、パソコンを手に入れた。ネットにもつないだ。

 初めて検索エンジンに入力した言葉は何だったのだろうか? 検索結果の一覧をみたときに昔の記憶が蘇える。「……ばーちゃんの事典だ」。

 知識のアーカイブは世界を広げてくれる。自分の知らないことを知るためには、外に答えを求めるしかない。あのときの百科事典よりも広大な世界がインターネット上にあった。

 ばーちゃんの事典から学んだこと。「分からないことは自分で調べる」。その習慣がわたしの人生を変えたのだと思う。エンジニアになれたのは自学自習の積み重ねの結果である。小学生のころと同じことをやっているだけである。

■無限

 最近、日本語で書かれている文献にわたしが知りたいと思うことが少なくなってきた。技術的な文献は読み手にスキルを要求する。高度な文献は、前提となる基礎技術を理解していないと、まともに読むことすらできないように作られている。

 しかし、世界を広げるための知識も同じ場所にある。情報がどこにもない、なんてことは言い訳にすらならない。ばーちゃんの事典が教えてくれたように、自分で欲しい情報を求めにいくのだ。いざゆかん、知識を求めて情報の海へ。

■追記

 新しいテレビ、すごく気に入ってもらえた。興奮のあまり夜更かしして、ずっと見ていたみたいだ。次の朝、ありがとうと言ってくれたばーちゃんは寝不足の顔。深夜2時まで起きてたとか。誕生日おめでとう。

 どこか遠くから長寿を祝うアラート音が鳴り響く……。

本日のスキル

  • ばーちゃんのスキル:レベル88

ドラクエで学ぶオブジェクト指向(3) 勇者とモンスターは似たもの同士

2010/05/24 22:29:51

 デザパタの前に継承の話をしていなかったことに気付く。プログラムの入門書を書くつもりはないので、基本的に順番はデタラメである。技術はつまみぐいが面白いのだ。

■継承 インヘリタンス

 第1回第2回で勇者クラスについて考えてきた。このまま肉付けして作り込んでいくのもいいが、このあたりでざっくり大枠を決めておきたいと思う。

 Heroクラスに、いくつかの属性を定義してきた。しかし、まだまだ足りてはいない。ドラゴンクエストI 公式ガイドブックによると、ちから、すばやさ、みのまもり、攻撃力、守備力、経験値、Gというパラメータを持っている。

 一方、モンスターにはHP、MP、すばやさ、攻撃力、守備力、経験値、Gがある。共通の項目を見つけたら抽象化のチャンスだ。

 勇者とモンスターの共通項目として、Battlerクラスを定義する。ひとまずメソッドは考えないで、共通要素を属性にする。Battlerクラスは実体がないので抽象クラスだ。

abstract public class Battler

 勇者クラスとモンスタークラスはBattlerクラスを継承する。

public class Hero extends Battler implements Serializable

public class Monster extends Battler

 当然、モンスターは冒険の書には保存しないのでシリアライズする必要はない。

■プルアップでリファクタリング

 ちから、みのまもりは勇者だけが持つのでHeroクラスで宣言した。

/** ちから */
private int power = 0;
/** みのまもり */
private int gurd = 0;
/**
* @return the power
*/
public int getPower() {
    return power;
}
/**
* @return the gurd
*/
public int getGurd() {
    return gurd;
}

 もともと、勇者クラスで定義していたプロパティとsetter/getterをBattlerクラスに持ち上げる。共通処理は親クラスへ。サブクラスで重複する処理を書く必要はない。

/** 名前 */
protected String name = null;
/** ヒットポイント */
private int hp = 0;
/** マジックポイント*/
private int mp = 0;
/** 最大ヒットポイント */
protected int max_hp = 0;
/** 最大マジックポイント */
protected int max_mp = 0;

 最大HP、最大MPは継承したクラスでも使いたいので protected にした。protectedにするのは属性にするかsetterにするかは悩ましい。HP、MPはロジックが入るため、サブクラスでもアクセサ経由でアクセスさせいたい。アクセス修飾子はprivateのままにする。

 もっと悩ましいのは、経験値と所持金であるG(ゴールド)の取り扱いだ。

 勇者にとって、経験値とGは獲得するものであるが、モンスターの経験値とGは与えるものである。モンスターが勝利しても勇者の経験値が得られない。同じものでも考え方が違うので、モンスター側ではgivenEx、givenGとした。

Classdiagram3_1_13

 Gは勇者ひとりで独占しているのではない。パーティ全体のお金である。II 以降のシリーズを考えた場合に勇者クラスに属性Gがあるのは好ましくないだろう。パーティ全体を表現するクラスがいずれ必要になるだろうが、これも保留とする。

 勇者とモンスターそれぞれのサブクラスに属性を追加し、getterメソッドだけを用意した。setterは必要になったときに追加すればよいと思っている。もっともパラメータ上昇の処理はどうするか未定のままだ。そんなのが多くてスイマセン。

■オーバーライド

 次は、攻撃力と守備力にとりかかろう。ここは今までと事情が違う。Battlerクラスでまずgetterを抽象メソッドとして定義する。

/**
* @return the offense
*/
abstract public int getOffense();
/**
* @return the deffense
*/
abstract public int getDeffense();

 なぜ、抽象メソッドにするか?

 モンスターは攻撃力と守備力というパラメータをダイレクトに持っている。

  • モンスター攻撃力=攻撃力
  • モンスター守備力=守備力

 であるが、勇者には武器と防具が存在する。

  • 勇者攻撃力=ちから+武器の攻撃力
  • 勇者守備力=みもまもり+防具の攻撃力

 同じ攻撃力、守備力でもアイテムによる補正が入る。

 処理が違うので抽象メソッドを使い、サブクラスで実装することを親クラス側で強制するためだ。これもBattler型変数で勇者とモンスターを扱うための方法である。

 モンスタークラスは特に考える必要はない。攻撃力と守備力を定義してメソッドを用意するのみ。

/** 攻撃力 */
protected int offense = 0;
/** 守備力 */
protected int deffense = 0;
@Override
public int getDeffense() {
    return deffense;
}
@Override
public int getOffense() {
    return offense;
}

 ちなみに@Overrideというのは、親クラスのメソッドをオーバーライドしていることを表すアノテーションである。今回は、継承の話がしたいのでスルーにした。便利な存在なので知っておいて損はない。それどころか、最近のOOPでは必須事項である。覚えておかないと仕事にならない。詳しく説明したいがキーワードの紹介だけに留めておく。

 武器防具の実装は、当然のごとくノープランなので形だけ定義しておく。「ちから」と「みのまもり」を返すようにしてみた。

@Override
public int getDeffense() {
    // TODO 武器力の補正値を追加
    return power;
}
@Override
public int getOffense() {
    // TODO 鎧、カブト、盾の補正値を追加
    return gurd;
}

 他にも、バイキルトやスカラ系の呪文などの要素が考えられる。敵味方入り乱れるともっと大変なことになるだろう。今の段階では、こういうことを意識しているだけで十分である。

 大事なことなのでもう一度繰り返しておこう。Battlerクラスを使うことによって hoge.getOffense()で勇者でもモンスターでも意識せずに攻撃力の取得ができる。この考え方を推し進めると勇者クラス、モンスタークラスに関係なく、Battlerクラスで戦闘を行うことができるのだ。いわゆるポリモルフィズムというやつである。

 すばやさの順でBattlerクラスをキューに詰めて、勇者とモンスターの違いを意識することなく、こうげきメソッドを実行させることができるだろう。

■登場人物たち

 すべての命のためにCreatureクラスを導入する。Creatureと命名したのはモンスターも含まれているからだ。

public abstract class Creature

 ここからさらに、王様や武器屋の商人のためにこのクラスを継承したPersonクラスを定義する。ぱふぱふ娘も、このクラスを継承することになるだろう。

public class Person extends Creature

 あわせて、BattlerクラスもCreatureクラスを継承する。

abstract public class Battler extends Creature

 人間とモンスターを同じくくりになることに違和感を感じる人もいるかもしれない。もちろん、城の兵士や、村人を継承して勇者というクラス設計も悪くない。人間とモンスターという対立図式も自然な考え方である。今回は戦闘システムに着目したのでBattlerクラスで闘うものたちをまとめたにすぎない。

 もっと考察を進めていけば、よりよい設計をみつけることができるのかもしれない。しかし、クラス設計というものは視点によって変わることもありえると思っている。究極の再利用性を求めるのも悪くはないが、最初は戦闘システムがやりたい。

■基底クラス

 最後に、全体を統括するクラスを作り、Creatureクラスを継承させる。

public abstract class DqObject

public abstract class Creature extends DqObject

 DqObjectは武器や、防具、呪文クラスなどをまとめる基底クラスという役割を果たす。登場人物だけでなく、大道具や小道具も含めた総括的なアイテムを取り扱う。今の段階ではまったく必要のないクラスだが今後、システムが複雑になってきたときに役に立つときがくるだろう。

Classdiagram3_2_4

 どこか遠くからレベルアップを告げるアラート音が鳴り響く……。

hagane_dq_src3.zipをダウンロード

ドラクエで学ぶオブジェクト指向(2) 勇者とは唯一無二の存在なり

2010/05/05 19:03:30

 前回のコラムでは勇者を定義した。しかし、まだまだ課題は山積みである。ひとつずつ片付けていこうと思う。設計と実装を同時に行えるのも趣味のプログラムならではの魅力である。まだ、わたしの頭の中にしか設計図は存在しない。ぶっちゃけ、完成するか破綻するのか分からない。それも含めて楽しんでもらえるとうれしい。

■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){
    if ( Hero.hero == null ){
        Hero.hero = new Hero(name);
    }
    return Hero.hero;
}

 getInstance(String name)が2回呼ばれた場合は、先勝ちで名前が決定される。2回目にこのメソッドを呼ぶ場合は例外を投げるような処理にしてもいいのかもしれない。

 synchronizedを指定することによって、誰かがメソッドを呼び出している間は他のスレッドからの呼び出しは待機される。複数スレッドからの同時呼び出した場合にインスタンス生成が重複する可能性を排除する。

■オーバーロード

 毎回、名前を入力しなければならない。面倒である。それに、2回目以降の呼び出しで不要な情報を与えることは避けたい。引数なしのメソッドも追加しよう。getInstance()でインスタンスの取得はできる。しかし、名前が決まらないので生成はできない。

public static Hero getInstance(){
    if ( Hero.hero == null ){
        throw new NullPointerException();
    }
    return Hero.hero;
}

 生成しないので同期をとる必要はない。synchronizedの指定はなしとする。

■Singletonを別の方法で実装する

 そもそも、同期化コストを支払ってまでスレッドセーフにする必要があるだろうか? という疑問に立ち返る。デフォルトコンストラクタを追加。staticフィールドの初期化でインスタンス生成を行うようにする。

private static Hero hero = new Hero();
private Hero(){
}

 このままでは名前の設定ができない。引数付きコンストラクタを初期化メソッドに変更する。

public boolean init(String name){
//private Hero(String name) {

 戻り値をbooleanに変更してみた。voidでもよいかもしれない。二重の初期化を防止するためにnameフィールドをフラグにしてチェックをかける。

public boolean init(String name){
//private Hero(String name) {
    if(this.name == null){
        return false;
    }

■使う側に(なるべく)負担をかけない

 このメソッドは原則、1回だけしか呼ばれないのであまり深く考えていない。……待て。staticにしないとgetInstance()で生成してからインスタンスでinit(name)という順序で呼び出さなければならない。それ以前にgetInstance()で毎回、例外が発生する。init()もstaticにするべきだ。

public static Hero init(String name){
//public boolean init(String name){
//private Hero(String name) {

 こうなるとFactoryにしたいのでHeroクラスを返す。

 synchronized Hero getInstance(String name)は不要になったので削除。あわせて、引数なしgetInstance()の条件チェックも変更

public static Hero getInstance(){
    //if ( Hero.hero == null ){
    if ( Hero.hero.getName() == null ){
        throw new NullPointerException(); //TODO 例外クラス検討

 init()を呼ばないで、getInstance()を実行した場合は例外を返す。NullPointerExceptionは適切ではないだろう。何か他の例外を検討すべきであるが、例外設計は先送りにしているので今は考えない。

 初回呼び出しは、init()で名前登録してインスタンス生成

h = Hero.init("ああああ");

 2回目以降はinit()で生成したインスタンスを取得する。

h = Hero.getInstance();

 同期コストのかからないSingletonパターンで修正した。getInstance()を使う前に、必ずinit()を呼ばなければいけないという制約がブサイクではある。しかし、最初に考えていたgetInstance()のオーバーロードも同じようにブサイクなので五十歩百歩だろう。init()はゲーム開始時の名前登録のときに呼び出され、必ず初期化されているという前提でかまわないはずだ。

■セーブするために

 最後に、復活の呪文や冒険の書でシリアライズ化するので直列化可能にしておくことも忘れない。

public class Hero implements Serializable

 どこか遠くからレベルアップを告げるアラート音が鳴り響く……。

ソースファイル(hagane_dq_src2.zip)をダウンロード

ドラクエで学ぶオブジェクト指向(1) 勇者を作ろう!

2010/05/02 22:35:18

 深い理由はないが、Javaで遊びたくなった。エンジニアの与太話でドラクエとオブジェクト指向の関連性みたいな話をするのは好きだ。つい最近もそんな冗談をどこかでつぶやいた記憶がある。何も考えないでお酒でも飲みながら組んでみようと思う。

 まずは、我らがヒーローを定義することから。基本となる勇者クラスだ。

class Hero

 次にアクセス制限。やはり勇者はいろんなオブジェクトと交流して成長させたいのでpublicにする。privateな引きこもり勇者なんていらない。

public class Hero

 ここまでは、お約束なのであまり深く考えていない。ただ、Heroだと女性の勇者を表現できないことは懸念事項とする。ヒーローとヒロインで挙動が違っていればありがたいが……。

■引数つきコンストラクタ

 次に、勇者の名前を入れる変数を用意する。フィールドは特別な理由がない限り privateにするのは基本である。

private String name = null;

 もちろん、クラスを定義しただけでは、勇者は現世にあらわれない。勇者インスタンスを実体化する手続きであるコンストラクタを定義しよう。

 「なまえを いれてください」

 最初の仕事は名前を登録することだろう。やはり、コンストラクタの引数に名前が欲しい。

public Hero(String name) {
    this.name = name;
}

 名前は4文字の制限があるので、入力チェックをかけたい。名前が4文字以上や空文字の場合は例外を投げる。パラメータ不正なので、IllegalArgumentExceptionをスローする。記号など不適切な文字のチェックもいるかもしれない。

public Hero(String name) {
    if( null == name || name.equals("") || name.length() > 4 )
    {
        throw new IllegalArgumentException();
    }
    this.name = name;
}

 そもそも、入力画面は決められた文字しか入力できない仕様であることを思い出す。必須チェック、文字数チェックもやっているのでパラメータの検証は不要な気がしてきた。ひとまず保留にする。あとで消すことは分かっているが雰囲気は残しておきたい。おそらく普通にValidatorクラスを作ることになるだろう。

■getter/setter

 続いて、名前以外の属性を検討する。すばやさとか攻撃力とかいろいろとあるが、初期段階なのでHP(ヒットポイント)、MP(マジックポイント)だけ定義する。

private int hp = 0;
private int mp = 0;

 Javaの場合、初期化されていないint型は0が代入される。しかし、気持ち悪いので0を設定してみた。Javaで書くときはいつも迷う。

 フィールドにアクセスする場合は専用のメソッドを作成する。getter/setterと呼ばれるメソッドを作成し、値の取得と設定を行う。作り方は簡単で、get + フィールド名(先頭は大文字)、set +フィールド名(先頭は大文字)。setterにはフィールドに代入する引数を用意する、というルールを守るだけ。

public int getHp() {
    return hp;
} public void setHp(int hp) {
    this.hp = hp;
}

 最初からフィールドをpublicにすればよい、という意見もあるが、メソッド経由のメリットはたくさんある。コンパイル時の違いやパターンに持っていきやすいことを説明するのは大変なので省略する。

 それに、HPやMPは0以下にはならない。HPが負の数になれば0にする処理を追加する。

public void setHp(int hp) {
    this.hp = hp;
    if( this.hp < 0 ){
        this.hp = 0;
    }
}

 しかし、この書き方だと問題があることに気づく。hpが一瞬でもマイナスになる可能性がある。マルチスレッドでは致命的だ。チェックしてから代入という順番に書き直す。

public void setHp(int hp) {
    if( hp < 0 ){
        this.hp = 0;
    } else {
        this.hp = hp;
    }
}

 HPが残り少なくなってきた場合や0になった場合に特別な処理をさせたいという要望があるらしい。ここにトリガーを置くことになるかもしれない。HPへの代入は他のパターンも考えられるので、それなりの仕掛けが必要になるだろう。

 同様に最大HP以上に設定されることはないのでフィールドを宣言して上限にも制限をかける。最大HPはまだ決まっていないので仮で0にする。

private int max_hp = 0;
public void setHp(int hp) {
    if( hp < 0 ){
        this.hp = 0;
    } else if ( hp > this.max_hp  ) {
        this.hp = this.max_hp;
    } else {
        this.hp = hp;
    }
}

■趣味で……

 3項演算子にしてみた。こういう書き方は好き。

public void setHp(int hp) {
    this.hp = ( hp < 0 ) ? 0 : ( hp > this.max_hp  ) ? this.max_hp: hp;
}

 3項演算子を知らないエンジニアに時々出会うことがある。入門書に書いてあることを質問されても説明する時間がもったいないので仕事ではあんまり使わないようにしている。本を紹介しても読んでもらえないことが多いし……。昔は丁寧に解説していたが、最近は、あきらめモードに入っている。そういうストレスもここで解消しておこう。

 同様にMPのsetterにも範囲制限を追加した。

public void setMp(int mp) {
    this.mp = ( mp < 0 ) ? 0 : ( mp > this.max_mp  ) ? this.max_mp: mp;
}

■コンストラクタの修正

 フィールドの初期化はコンストラクタの中で設定する。最大HP、MPに代入する10は仮の数字。最終的に外から初期値を取得することになるだろう。

public Hero(String name) {
    if( null == name || name.equals("") || name.length() > 4 )
    {
        throw new IllegalArgumentException();
    }
    this.name = name;
   
    this.max_hp = 10;
    this.max_mp = 10;
    this.hp = this.max_hp;
    this.mp = this.max_mp;
}

■読み込み専用

 最後に、名前へのアクセス手段をまだ作っていなかったことに気づく。ここもgetter/setterのを作りたい。しかし、一度決めた名前は変更できないというビジネスルールが存在する。勇者の名前はRead Onlyなのでsetterメソッドは作らない。

public String getName() {
    return name;
}

 これで、コンストラクタで初期化した名前からは変更できなくなった。もちろん、クラス内部から名前の編集は可能である。大切なのは外部から操作できないということだ。名前変更は特殊なビジネスルールがあると聞いている。詳細はわからないが、クライアントからのリクエストがあれば専用メソッドを用意して、それ相応の手続きを行うことになるだろう。

 どこか遠くからレベルアップを告げるアラート音が鳴り響く……。

Hero.javaをダウンロード

@IT Special 注目企業
@IT Special ラーニング

エンジニアライフ 最新の投稿コラム

@IT自分戦略研究所 新着記事

エンジニアライフ スポンサー

コラムニスト プロフィール

はがねのつるぎ
スキルを捨てることもまたスキルではないかと、禅問答のような世界に迷い込むフリーエンジニア。本当にフリーなブログはコチラ

- PR -
@IT Special 注目企業
インデックス

@IT Special ラーニング