1. はじめに
会社で勉強会を開いたので学習内容のメモとなります。
タイトルの通りクラス設計に関する勉強会を開きました。
2. 概要
今回のゴールは、自己防衛責務を持ったクラス設計の基礎を知ろうです。
今回の記事は、RPGゲームを題材とし内容を進めていきます。
内容は次のとおりです。
- 変数の再代入は行わない
- データクラスをなくそう
- コンストラクタを使い不正状態を防ぐ
- 新しいインスタンスを作る思考を持つ
参考資料:「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」
3. 内容
3.1 再代入は行わない
場面設定1:ミミックへのダメージは勇者の攻撃力とエネミーの防御力からなる
設計の話とは少しずれますが後々必要になる考えなので先に話します。
下記のコードについて、①はプレイヤーの攻撃力を表す計算でありダメージの総量ではない。
②のカッコ内はエネミーの防御力を計算している。
①と②それぞれ変数名と処理の内容が乖離していて直接関係ない。
→ 変数が今どんな状態なのか気にしないといけない
int damageAmount = 0; // ダメージの合計
damageAmount = playerArmPower + playerWeaponPower; // 1
damageAmount = damageAmount + (enemyBodyDefence + enemyArmorDefence); // 2
変数を目的ごとに用意すると意味や状態が1目でわかるようになる。
totalPlayerAttackPower:プレイヤーの攻撃力を表してそうだ
totalEnemyDefence:エネミーの防御力を表してそうだ
damageAmount: ダメージの合計を表してそうだ
int totalPlayerAttackPower = playerArmPower + playerWeaponPower; // 1
int totalEnemyDefence = enemyBodyDefence + enemyArmorDefence; // 2
int damageAmount = totalEnemyDefence - totalPlayerAttackPower;
3.2 データクラスをなくす
データクラスとは、インスタンス変数しか存在しないクラスです。
変数しかないので必ずそれを操作するクラスとの関連が必要。
関連しあう者同士の認知が困難、重複コードの発生、重複個所の修正漏れ、不正値の混入などこれらが技術的負債となる。
※列挙型などは例外だと思う・・・
他のクラスに変数の初期化だったり、入力チェック、操作これらを任せるのは未熟なクラスである証拠です。
ここでタイトル回収、自己防衛責務を備えた良いクラスの構成要素は下記の通り。
- インスタンス変数をもつ
- インスタンス変数不正状態から防御し、正常に操作するメソッドもつ
3.3 コンストラクタを活用する
場面設定2:勇者が攻撃したらミミックに -35 ダメージを与えた
勇者の攻撃が - になるのは明らかな不正状態です。
この記事での不正状態の定義は、要件を満たさない値、意図せず値が変わるとします。
なぜ不正状態が発生するのか、インスタンス変数に個別に値を代入して初期化しているsetter
みたいな動きが良くない。← 再代入
setterで状態を変更するたびに不正状態になる機会が増えるのであぶないです。
これを防ぐには、インスタンス生成時、インスタンス変数に正常値が確実に設定されている状態にする。
- 再代入をシステム的に防ぐ:finalなど
- 初期化値も不変にする:finalなど
- 不正値が入らないよう必ず初期化:バリデーション
playerArmPowerを使うメソッドは勝手に正常に操作するメソッドとなる
→ 自己防衛責務を持ったクラスの完成
でも違う値を使いたいときあるよなあ・・・
class Player {
final int playerArmPower; // 不変にする 1
public Player(final int playerArmPower) { // 不変にする 2
// 攻撃力に 負の数字は入らない
if(playerArmPower > -1) {
this.playerArmPower = playerArmPower;
} else {
this.playerArmPower = 0; // 不正値を防ぐ 3
}
}
}
3.4 インスタンスを生成する思考を持つ
場面設定3:勇者の攻撃力が下がることがある
playerArmPowerを別の値に変更したいがfinalで不変状態になっているどうしよう?
そんな時には新しい値を持ったインスタンスを生成する思考を持ちましょう。
下記のプログラムであればもとの攻撃力を持ったインスタンスと、攻撃力が下がったインスタンスが共存できます。
インスタンスへの代入もコンスタラクタでの初期化だけなので不正状態になる可能性も低いです。
// Mainクラスのつもりで
// 初期化
Player oldPlayer = new Player(10);
// 攻撃力を下げたい
Player powerDown = oldPlayer.powerDown(5);
// powerDownを使って戦いの計算
powerDown.attack();
class Player {
final int playerArmPower; // 不変にする
public Player(final int playerArmPower) { // 不変にする
// 攻撃力に 負の数字は入らない
if(playerArmPower > -1) {
this.playerArmPower = playerArmPower;
} else {
this.playerArmPower = 0; // 不正値を防ぐ
}
}
public Player powerDown(final int down) {
// 新しい攻撃値を持ったPlayerを返却
return new Player(this.playerArmPower - down);
}
}
また次のような再代入の書き方は状態変化が暗黙的と言われます。
- 値がどう変わったのかわからない 比較できない
- 元の値が使いにくい 戻しにくい
oldPlayer.powerDown(down);
// 処理の内容
public void powerDown(final int down) {
this.playerArmPower = down;
}
対して新しいインスタンスを生成する次のコードは、
これをしたらこうなるという時間の流れがあるので状態変化が明示的と言えます。
さらに言えば、2つの状態のインスタンスが存在するので比較や前の状態を使用することも容易です。
Player oldPlayer = new Player(10);
// こうなる ← これをしたら
Player powerDown = oldPlayer.powerDown(5);
4. まとめ
自己防衛責務を持ったクラス設計の基礎を知るために次のような要素が必要です。
- 変数の再代入は行わない
- データクラスをなくそう
- コンストラクタを使い不正状態を防ぐ
- 新しいインスタンスを作る思考を持つ
5. 参考資料
- 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方