時代に翻弄されるエンジニアのブログ

ゲームプログラマをやっています。仕事やゲームや趣味に関してつらつら書きたいと思います。

SOLID原則 ゲームで使える リスコフの置換原則

f:id:tkymx83:20190120104124p:plain

こんにちは、皆さんは継承関係をうまく使っていますか?継承関係がうまく使えていないプロジェクトでは依存関係が増えるだけで複雑性がどんどん増していきます。

今回はそのような継承関係をうまく使うためのリスコフの置換原則について触れたいと思います。

「リスコフの置換原則」って?

一言とでいうと、継承先に依存することなく親クラスを使用できる設計のことを言います。

簡単にいかのクラス構成を使って解説していきたいと思います。

f:id:tkymx83:20190120104231p:plain

このクラスはキャラクタが攻撃する仕組みを表しています。このゲームでは通常攻撃と必殺技の2つの攻撃方法が存在します。仕組みは違うがどちらも攻撃なので抽象クラスから派生する形で攻撃の仕組みクラスが作られています。ここで、実際に攻撃する際のコードを見たいと思います。

void Attack()
{
  AbstractPlayerAttack attack = GenerateAttack();
  if (attack is NormalAttack) {
    attack.NormalAttack();
  }
  else if (attack is SpecialAttack) {
    attack.SpecialAttack();
  }
}

上のコードは攻撃が抽象化できていません。使用するときに毎回通常攻撃か必殺技かを判断する必要があるからです。判断するコードを書かないと行けないということは、クラスの変更や新しい具象クラスを作ったときに使用している側のコードも修正する必要があるということです。

つまりは、親クラスを使用する際に継承先が何であるかを気にしているので、リスコフの置換原則に則っていないということです。

では、理想的なクラスはどのようなものでしょうか?

継承先を見る必要のない設計

通常攻撃も必殺技も変わらず攻撃です。そのため、どちらのメソッドも攻撃でいいのです。

f:id:tkymx83:20190120111144p:plain

こうすることでコードは以下のように継承先を気にする必要がなくなります。

void Attack()
{
  AbstractPlayerAttack attack = GenerateAttack();
  attack.Attack();
}

意外と起きてしまうもの

リスコフの置換原則は、当たり前のように見えます。ですが、ゲームの仕様を追加していくと落とし穴に落ちるときがあります。

ここで、必殺技を使用するときにだけスキルポイントを消費すると考えてみましょう。何も考えなければ以下のようなコードを書いてしまいがちです。

void Attack()
{
  AbstractPlayerAttack attack = GenerateAttack();
  if (attack is NormalAttack) {
    attack.Attack();
  }
  else if (attack is SpecialAttack) {
    // スキル使用時はスキルポイントを消費する
    DecreaseSkillPoint();
    attack.Attack();
  }
}

これでは、意味がありませんね。スキルポイントを使用する処理を必殺技に限定しているため、継承先で攻撃クラスの判断が入ってしまっています。これによって、コード間の依存が発生してしまいました。

リスコフの置換原則はこのように常に気を使っていなければすぐに違反してしまう原則です。しかし、守ってさえいれば継承のメリットを最大限に活用です原則なのです。

まとめ

「リスコフの置換原則」は継承先に依存することなく親クラスを使用できる設計のことを言います。

そして、それを守ることで継承のメリットである、依存性の分離を最大限に活かすことができます。

僕の体験談ですが、

ゲームの長期運用を行っていると、継承先の数が大量になっていきます。状態異常が100個あるゲームなどはいい例です。そのときに仕様が追加されたら、特定のクラスに特化した内容を書いてしまいがちです。ゲームの複雑性はそこで格段に上昇していきます。そうならないように、SOLID原則をチームで徹底することは重要なことです。