Web Analytics

Technically Impossible

Lets look at the weak link in your statement. Anything "Technically Impossible" basically means we haven't figured out how yet.

実用抜き、ただ理解するためのオブジェクト指向 - 抽象クラス、インタフェイス、継承、抽象化

Dragon Quest I, II & III (1, 2 & 3) Collection (輸入版)アジア

オブジェクト指向の説明とくれば、次のような題材が教科書的な典型例だ。

  • 自動車を抽象化して、フェラーリやベンツとして具体化する
  • 動物を抽象化して、犬や猫として具体化する

そして、この投稿も似たような説明に終始している。
RPGを意識して、戦士、魔法使い、僧侶のパーティと、その戦闘をイメージした題材で、次の事柄を例示している。

  • 抽象クラス
  • インタフェイス
  • 継承

ちなみに、サンプル・コードにはKotlin*1を用いている。この投稿末尾にあるサンプル・コードをPlayground*2へコピー&ペーストすれば、実行結果をオンラインで確認することもできる。


まず基本を考える。戦士、魔法使い、僧侶には、共通のコマンド(振舞い)と、それぞれに独自のコマンドが存在するとしよう。具体的には、

共通コマンド

  • たたかう
  • ぼうぎょ
  • にげる

※「どうぐ」は省略した

独自コマンド

戦士 2回攻撃+防御力強化
魔法使い 魔法が使える
僧侶 お祈りできる

この構成に基づいて職業を定義するならば、職業とは基本コマンドと独自コマンドの組み合わせということになる。具体的には、基本コマンドを備えたキャラクタが存在するとして、それに適切な独自コマンドを付与することになる。
基本コマンドだけを備えたキャラクタはplayableではない、つまり実体化する必要がないし、させるべきではない。また独自コマンドだけを備えたキャラクタも作成することはできないとすれば、それぞれを次のように定義することになる。

基本クラス 共通コマンドを定義
共通コマンドの振舞いを定義
実体化できないので、抽象クラスとする
職業コマンド 独自コマンドを定義
コマンドだけなので、インタフェイスとする
職業クラス 基本クラスを継承したクラス
職業コマンドを実装したクラス

ここまでをコード化すると、次のようになる。

抽象クラス

まず基本クラスを抽象クラスとして実装している。

abstract class BasicRole(name:String){
    var name = name
    
    fun attack(){
        println("${name}は攻撃した")
    }
    
    open fun guard(){
        println("${name}は身を守っている")
    }
    
    fun run(){
        println("${name}は逃げ出した")
    }
}

抽象クラスは、そのメンバーも抽象化され、それらの実装を継承先のクラスに委ねることがある。この例では、ここではクラスの具体化を妨げる以外の目的は持たないので、クラスのみを抽象化している。

「guard」(ぼうぎょ)の先頭に「open」と付与しているのは、この振舞いを変更する可能性があるためだ。この点については後述する。

インタフェイス

インタフェイスは、職業を特徴づける要素ではあるものの、それ自体はコマンドに過ぎない。そのため、クラスではなくインタフェイスとして定義している。
インタフェイスなので、職業ごとのコマンド(メソッド)だけを定義する。その振舞いは、職業クラスで定義することになる。

interface FighterSkill{
    fun doubleAttack(){}
}

interface MagicianSkill{
    fun magic(){}
}

interface PriestSkill{
    fun pray(){}
}

継承

抽象クラスとインタフェイスを継承したクラスによって、それぞれの職業が具体化される。共通コマンドは抽象クラスから継承し、インタフェイスによって、職業に応じた独自コマンドを実装することが強制される。
戦士の特性の一つである防御力強化は、共通クラスの振舞いをオーバーライドしている。これは共通クラスとして定義された「ぼうぎょ」の振舞いを、戦士だけは独自に定義するためだ。
抽象クラスでの「guard」の先頭に「open」を付与したのは、このオーバーライドに対応するためだ。

class Fighter(name:String):BasicRole(name), FighterSkill{
    override fun doubleAttack(){
        println("${name}の2回攻撃!")
        this.attack()
        this.attack()
    }
    
    override fun guard(){println("${name}の防御力強化")}
}

class Magician(name:String):BasicRole(name), MagicianSkill{
    override fun magic(){
        println("${name}は呪文を唱えた!")
    }
}

class Priest(name:String):BasicRole(name), PriestSkill{
    override fun pray(){
        println("${name}は祈りをささげた!")
    }
}

抽象化→汎用性の確保

以上のように定義することで、抽象化の利点を生かしたプログラムを作成することができる。例えば、それぞれ異なるクラスとして定義されている職業も、継承元となるクラスは共通だ。この特性を活かせば、共通クラスの配列(リスト)として、パーティを定義することができる。

この配列を活用すると、例えば戦闘シーンは単一のループ処理で表現できる。実行コマンドと、数値を次のように割り当てる。

1 たたかう
2 ぼうぎょ
3 にげる
4 職業コマンド

「4」が選択された場合は、それぞれの職業に応じたコマンドが実行されるように表現するならば、コードは次のようになる。

fun battle(party: List<BasicRole>, command: List<Int>){
    fun special(member: BasicRole){
        when(member){
            is Fighter -> member.doubleAttack()
            is Magician -> member.magic()
            is Priest -> member.pray()
        }
    }
    
    for (i in 0..party.size - 1){
        when(command[i]){
            1->party[i].attack()
            2->party[i].guard()
            3->party[i].run()
            else -> special(party[i])
        }
    }
}

fun main(){
    val rhodesia = Fighter("ローデシア")
    val sumaltria = Magician("サマルトリア")
    val moonburg = Priest("ムーンブルグ")    
    val party: List<BasicRole> = listOf(rhodesia, sumaltria, moonburg)
    
    battle(party, command=listOf(4, 4, 4))
    battle(party, command=listOf(2, 1, 3))
}
汎用性

さらに深入りすれば、次のようなことも考えられる。

Q:「すばやさ」の変化によって、攻撃順序が変化するなら?
A:「すばやさ」に応じて、配列(リスト)の格納順序を変えたらよい

Q:パーティの人数が増えたら?
A:配列に新たなメンバーを追加すればよい

Q:職業が増えたら?
A:職業に応じたインタフェイスと、それを実装した新クラスを定義すればよい

それぞれに必要な対応は、

  • かなり局所的かつ限定的なものとなること
  • 論理構造そのものを変える必要がないこと

が理解できるだろう。