LoginSignup
17
11

More than 5 years have passed since last update.

RPGで考えるオブジェクト指向(1)

Last updated at Posted at 2018-10-18

まえおき

「たい焼き型がクラス、そこから作ったたい焼きがインスタンスだよ☆」
というよくある初心者向けオブジェクト指向の説明に対して、
「たい焼き型からたい焼きインスタンス生成するとか意味わかんなくない??メソッドとかどうするの??」
というイキリツイートをしてしまい、じゃあお前が説明してみろよという業を背負ってしまったので、責任持ってQiita初投稿します。

目的

初心者がしっくりくるオブジェクト指向の解説を目指します。
また、筆者のアウトプット能力の向上を図ります。

内容

  1. クラスの作成、インスタンス化、抽象クラスと継承の解説
  2. インターフェース、ダックタイピングの解説 ← 次回予定

今回は第1回ということで、クラス、インスタンス化、抽象クラスについて記述します。言語はKotlinを使用しますが、JavaやC#、VBの経験があれば大丈夫かと思います。

「RPGで考える」とは

RPGといえば主人公や仲間、町人などたくさんのキャラクタがいますが、今回の立場は主人公ではなくラスボスです1。イメージは勇者のくせになまいきだ2
ラスボスは主人公の行く手を阻むモンスターを自由に生成し、使役できるものとします。
RPGっぽい世界観をにじませつつ、急にコードが入ってきますのでご注意ください。

スライム作るよ! - クラス作成、インスタンス化

さて、あなたはこれから迫りくる勇者に対抗する手段として、モンスターを作り出します。手始めにスライムを作りますが、スライムの仕様は次のとおりとしました。仕様とかとてもメタいですね。

  • スライム
  • HP 30 (hp)
  • MP 5 (mp)
  • 攻撃力 5 (physPower)
  • 防御力 2 (physProtect)
  • その他ステータス (...)
  • 行動
    • 対象に攻撃 (attack)
    • 防御 (protect)

これをクラスとして記述すると次のようになります。
念の為、「:」の前にあるhpやmp等が変数で、funが行動を表すメソッドです。

Slime.kt
class Slime {
    var hp: Int = 30
    var mp: Int = 5
    val physPower: Int = 5
    val physProtect: Int = 2
    ...

    fun attack(target: Human){
        //physPowerだけダメージを与える
        target.hp = target.hp - physPower
    }
    fun protect(){ ... }
}

さてスライムの仕様が決まったので、次はいよいよスライムの生成(インスタンス化)をします。
Javaではnew演算子、Kotlinではクラス名()と書いてインスタンス化を行います。

val slime1 = Slime()
val slime2 = Slime()
... //スライムたくさん

これで仕様どおりのスライムを生成し、各slime変数に格納しました!
それでは始まりの町周辺にスライムを配置し、レベル1の勇者に喧嘩を売りに行きましょう。

───さっそく勇者とエンカウントしました。
あなたはスライム1に対して、勇者を攻撃するよう指示を出します。
スライム2には防御指示を出しましょう。

slime1.attack(yuusya)
slime2.protect()

attackメソッドをコールして、ターゲットに5ダメージを与える攻撃を仕掛けます。果たして結果は───!?


さて戦闘結果は置いといて、ここまでがクラスの記述とインスタンス化でした。クラスは仕様、インスタンスは仕様を基に作った実体というイメージです。
たい焼きの例では、動かないたい焼きのメソッドコールという点が納得できなかったため、スライムを使役することでメソッドのコールを表現しています。
オブジェクト指向では、オブジェクトに指示を与えて働いてもらうことが大事です

"モンスター"作るよ! - 抽象クラスと継承

前節ではスライムを作りましたが、もっと多くのモンスターを作らないと日々強くなる勇者御一行には勝てません。
そこでさまざまなモンスターの仕様を作成します。

class KingSlime {...}
class KillerMachine {...}
class RedDragon {...}
...

たくさんモンスターのクラスを作成したので、全部勇者にぶつけて一気に倒してしまう戦法を考えましょう。全てのインスタンスに対してattackを指示します。ゲスい。

kingSlime.attack(yuusya)
killerMachine.attack(yuusya)
... //全部のインスタンスに対して攻撃指示。めんどい。

インスタンスが多くなるほど、同じattackの呼び出しがめんどうですし、指示漏れになる可能性も発生します。ここでモンスターを「抽象化」し、めんどくささを低減します3
すべてのモンスターに共通していることは「モンスターである」ことなので、次のようなMonsterという抽象クラス(abstract class)を作成します。Monsterクラスは基本動作であるattackとprotectを抽象メソッドとして定義します。

Monster.kt
abstract class Monster() {
    //抽象メソッドは実装をもたない
    abstract fun attack(target: Human)
    abstract fun protect()
}

Monsterはすべてのモンスターを抽象化した存在のため、生成(インスタンス化)はできません。
さて、前述のモンスターたちにMonsterクラスを継承し、attackとprotectを実装します。大事なことは、Monsterクラスを継承しているものだけがモンスターと言えることです。
Kotlinにおいて、継承は「:」で行います。Javaでは「extends」です。

class Slime : Monster() { //←Monsterを継承
    ...
    override fun attack(target: Human){
        //やっぱりスライムは固定ダメージ
        //各モンスターによりダメージ計算を変えられる
        target.hp = target.hp - physPower
    }
    override fun protect() {...}
}
class KingSlime : Monster()  {...}
class KillerMachine : Monster()  {...}
class RedDragon : Monster()  {...}
...

これで、次のように一気にattack指示を与えることが可能になりました。

//モンスターのリストを生成
val monsters = listOf<Monster>(Slime(), KingSlime(), ...)
for (monster in monsters){
    //forでリストに格納した全モンスターに指示をする
    monster.attack(yuusya)
}

SlimeやKingSlimeはMonster型を継承しているため、Monster型で定義されたmonstersリストに格納することができます4
逆に、Monsterを継承していないクラスはモンスターとは言えない(言い切れない)ので、リストに格納することができません。

個別にattackを指示していたのが、forでリストを回すことによりmonster.attackの1行になりました。勇者集中攻撃されてかわいそうですね。
さて、当たり前のようにMonster#attackをコールしていますが、このとき勇者に与えるダメージ計算はどのようになるでしょうか?
Monster#attackは抽象メソッドのため、ダメージ計算の実装をもちません。

答えは、SlimeクラスやKingSlimeクラスでオーバーライドしたattackの実装が読み込まれます。monster変数の中身がSlimeなら固定ダメージになるし、KingSlimeなら実装が異なれば違うダメージになります。

このように、基底クラスを継承した派生クラス(Slime、KingSlime、...)を作成することで、同じメソッド(Monster#attack)を呼んだのに異なる動作をさせることができます。

問題点 - 基底クラスの拡張

さて、前節で述べたように継承はとても便利で強力な機能ですが、強力すぎるがゆえの副作用があります。
たとえば、新たに特殊攻撃(Monster#skillAttack)を追加する場合を考えてみます。

Monster.kt
abstract class Monster() {
    abstract fun attack(target: Human)
    abstract fun protect()
    abstract fun skillAttack() //追加
}

すると、skillAttackは抽象メソッドのため、Monsterを継承したクラス全てにskillAttackを実装しなければならなくなります
もし全モンスターに特殊攻撃が備わるのであれば仕方ないですが、単にattackとprotectを繰り返すだけのモンスターばかりであれば、無駄な実装を繰り返すハメになります。

class Slime : Monster() {
    ...
    override fun attack(target: Human){...}
    override fun protect() {...}
    override fun skillAttack() { /*空の実装*/ }
}
...

もしさらに別の行動が与えられたら…モンスターの種類が今以上に増えていたら…基底クラスの膨張と派生クラスの修正は指数関数的に増大していきます。勇者に倒される前に勝手に倒れてるかもしれません。
そこで、次回は動作の共通点に着目して、インターフェースを説明していきます。

まとめ

  • クラスは仕様、インスタンスは仕様を基に作られた実体
  • オブジェクト(インスタンス)に指示して働いてもらうのが大事
  • 継承は強力だけど副作用がある

次回はインターフェースとダックタイピングのテーマでお送りします。
デザインパターンも少し書ければいいな~


  1. なぜわざわざラスボスかって、主人公サイドでは何人も同じキャラを生成できないためです。王道RPGなんて基本シングルトンじゃないですか!← 

  2. 名作。それパズルゲームじゃね?とかつっこんではいけない 

  3. 抽象化=めんどくささを低減と言いたいわけではないですが、説明の一環ということでご容赦いただきたい 

  4. Kotlinでは型推論があるので、本来listOfに型情報は不要です 

17
11
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
11