読者です 読者をやめる 読者になる 読者になる

オブジェクト指向を行使する心

今日、とあるツイートでプログラミングにおけるよくある問題が顕現していた。

奇妙な行コメントには目を瞑るとして、このコードは要約すれば以下のような処理を実現していることが窺える。

  • ゲームプログラミングでは、現在のシーンによって処理を切り替える必要がある。メニュー画面ならメニューの処理を、戦闘画面なら戦闘を、マップならマップの表示をそれぞれ行う。
  • 現在のシーンの種類は変数によって与えられる。
  • その変数の値によって、対応する処理を選ぶ。

こうしてみると単純だが、caseによる単純な分岐では扱いにくい。新しいシーンを作るたびに場合分けを書き換えなければならないし、何よりそれは「処理」と「処理を表す値」の一覧表を作るという面白みのない処理だからだ。

一つの(そして、よく用いられる)解決法はオブジェクト指向プログラミングである。各シーンをオブジェクトとして扱うことにより、問題となっている分岐を扱う必要がなくなる。

新たに、操作の集まりによって定義される「インターフェイス」という構造を導入する。以下の疑似コードでは、Sceneとして扱う型は、drawという処理が使えることを示している。

interface Scene:
    draw()

インターフェイスによって定められた操作を実装するのが「クラス」である。MenuMapBattleという3つのクラスを定義し、それぞれ異なるdrawの実装を持っている。

class Scene ⇒ Menu:
    draw():
        …

class Scene ⇒ Map:
    draw():
        …

class Scene ⇒ Battle:
    draw():
        …

これらの処理を実際に使うには「インスタンス」を用いる。

s ← new Menu()

これはMenuの実体を持つインスタンスを生成し、sに代入する操作を表す。ここで得られたインスタンスsSceneの型を持つ(サブタイピングがあれば、Menuの型を持ってもよいが、Sceneであることがここでは重要である)。

s.draw()Sceneが保証しているdrawの処理を実行する。MenuインスタンスなのでMenudrawが実行されるが、仮にnew Map()で生成した場合はMapdrawになる。ここで注目すべきは、コードや型は一緒でも、実際に行われる処理は動的に決まるという点だ。今までは、シーンを表す値を見て処理を自分で選択しなければならなかったが、その必要がなくなっているのがわかる。したがってシーンの種類が増えても、drawを呼ぶ部分を修正する必要はない。

なぜこのようなことを可能にしているのか?そのからくりはインスタンスに宿っている。インスタンスは言語によって様々な実装方法があるが、大きく分けて「クラス(処理)のインデックス」か「処理そのもの」のどちらかがインスタンスの内部に隠れている。

前者の場合、s.draw()は、sの中身のインデックスを元に、対応するクラスのdrawを取り出して実行する。new Menu()で作ったインスタンスには、Menuを表すインデックスが入っており、new Battle()で作ればBattleを表す値が入っている(文字列でも数値でも、一意に対応させられれば何でもよい)。今までの方法と実は全く同じだが、自動化されていると言える。

後者はより簡単で、インスタンスにはインターフェイスが保証する処理の実装がすべて入っている。s.draw()は、sの中に入っているdrawの処理を取り出しているに過ぎない。

C++などの静的型付きの言語は前者を、Pythonなどの動的型付きの言語は後者を取る傾向がある。なお、静的型付きの言語であるHaskellではどちらの方法でも実現できるが、あまり使われていない。

いずれにせよ、特にゲームプログラミングにおいて、動的に処理を選択したい場合は少なくない。オブジェクト指向がその便利な解決法であることは間違いなく、実際にゲームプログラミングで使われている言語のほとんどはオブジェクト指向をサポートしている。