はじめに
ストラテジパターンってなに?
ロジックを別々のオブジェクトに分け、実行時に切り替える方式!!!
もっと言うと、本来ならif文やswitch文で条件分岐するところを、条件分岐の文を使わずに書くこと!
なぜこんなことをする必要があるのか。別の条件分岐で書いてもよくないか。
⇒よくない
if文で分岐するということは、それぞれの処理を同じ場所に書いていることになる。
if文やswitchのcase文などが大量にあった場合、どれがどの処理なのかわからなくなることも考えられるはず。
これを防ぐためにこのパターンを使う!
普通に分岐してみた例
例えば、ゲームで技を繰り出す処理があったとする。
技の名前によって分岐し、それぞれの技で別々のロジックがあるとする。
void UseSkill(String skillName){
switch(skillName){
case "たいあたり":
animation.Play("たいあたり");
float attackPower = player.PhysicalAttack() * 0.5f;
enemy.TakeDamage(attackPower);
break;
case "かみなり":
animation.Play("かみなり");
float attackPower = player.MagicAttack() * 1.4f;
enemy.TakeDamage(attackPower);
enemy.TakeParalysis(30);
break;
case "かえんほうしゃ":
animation.Play("かえんほうしゃ");
float attackPower = player.MagicAttack() * 1.2f;
enemy.TakeDamage(attackPower);
enemy.TakeBurning(30);
break;
case ...
...
...
}
}
ポケモンっぽいゲームを想定する。
ここでは例として三つの技を用意した
- たいあたり
- たいあたりモーションを再生
- 物理攻撃力でダメージ計算、技の威力倍率 0.5 を乗算
- 敵にダメージを適用
- かみなり
- かみなりモーション再生
- 魔法攻撃力でダメージ計算、技の威力倍率 1.4 を乗算
- 敵にダメージを適用
- 独自の確率でまひ状態にする
- かえんほうしゃ
- かえんほうしゃモーション再生
- 魔法攻撃力でダメージ計算、技の威力倍率 1.2 を乗算
- 敵にダメージを適用
- 独自の確率でやけど状態にする
物理攻撃力、魔法攻撃力としてPhysicsAttack、MagicAttackをPlayerに用意する。
それぞれ技の威力を攻撃力に加算している。
そして一部の技は敵に状態異常を確率でつけることができる。
例えばTakeBurning(30)は「30%の確立で敵がやけどする」ことを意味している。
switch文で書くと、このような書き方が考えらえる。
こう書いてしまうことによって、以下の欠点が考えられる。
- 技が多ければ多いほどコードが縦に伸びていき、どこになんの技が書いてあったかわからなくなってしまう可能性がある。可読性が低下してしまう。
- バグが起きた時の影響範囲がほかの技まで広がる可能性も考えられてしまう。
ストラテジパターンでの同じ例
これをストラテジパターンで書いてみる。
public interface Skill{
void UseSkill(Enemy enemy);
}
public class Tackle extends Skill{
Player player;
Animation animation;
...
void UseSkill(Enemy enemy){
animation.Play("たいあたり");
float attackPower = player.PhysicalAttack() * rate;
enemy.TakeDamage(attackPower);
}
...
}
public class Thunder extends Skill{
Player player;
Animation animation;
float rate = 1.4f;
...
void UseSkill(Enemy enemy){
animation.Play("かみなり");
float attackPower = player.MagicAttack() * rate;
enemy.TakeDamage(attackPower);
enemy.TakeParalysis(30);
}
...
}
public class FlameThrower extends Skill{
Player player;
Animation animation;
float rate = 1.2f;
...
void UseSkill(Enemy enemy){
animation.Play("かえんほうしゃ");
float attackPower = player.MagicAttack() * rate;
enemy.TakeDamage(attackPower);
enemy.TakeBurning(30);
}
...
}
public enum SkillId{
Tackle, Thunder, FlameThrower, .........;
}
public class CharacterSkills{
...
Map<SkillId, Skill> skills = new HashMap<SkillId, Skill>();
...
public CharacterSkills(){
...
skills.put(SkillId.Tackle, new Tackle());
skills.put(SkillId.Thunder, new Thunder());
skills.put(SkillId.FlameThrower, new FlameThrower());
...
}
void UseSkill(SkillId id, Enemy enemy){
skills.get(id).UseSkill(player, enemy);
}
}
こんな感じで書ける。
技をクラスで分け、それぞれSkillインターフェースを継承させる。
⇒これにより、それぞれをSkillとして扱える
使用側がそれぞれの技をSkillIdと合わせてMapに登録することで、IDから技を引き出し、ロジックを実行させることができる。
合計のコード量は増えたかもしれない。しかし、様々な利点をもたらしてくれている。
- これらのクラスはファイルで分けることができる。(可読性向上)
- 条件分岐を書かずに同じロジックを作ることができた。
- switch文のネストを削減することもできた。
- 技同士がおかしな影響を与えうる可能性も低くなった。(疎結合化)
- interfaceを使うことにより、interfaceで書かれている関数を継承先で実装しなければならなくなり、未知の不具合を起こす前にコンパイラがエラーをはいてくれるようになった。(実装の強制)
- 技を追加する場合でも、Skillを継承したクラスを定義すればよく、ほかのシステムに影響を与えにくい。(拡張性向上)
今回はゲームの「技」をこのパターンで書いたが、ほかにも、ステートマシンのそれぞれの状態の切り替え、でもストラテジパターンで書ける。
最後に
この書き方をそのまま実装するのはまだまだ粗があるから使うべきではない!!!
考え方を簡単に具体的なコードに落とし込むと上で書いた感じになったということを念頭に置いてみてもらいたい。
(あと何か間違いや補足部分があったら是非おしえてください)