どうも、solaです。
今回はみんな大好き(?)オブジェクト指向プログラミングの話です。
オブジェクト指向に関しては、書籍にしろインターネット上にしろ、有益な情報が多いですが、いろいろな側面からあたることで理解も深まるので、私なりにオブジェクト指向プログラミングの概要について語ってみます。
ちなみに、簡潔に説明する(つもり)ために一般的でない説明だったり、言葉足らずな部分もあるかと思いますが、ご了承ください。
■なぜオブジェクト指向プログラミングは難しいのか
プログラミング初心者にとってなぜオブジェクト指向プログラミングが難しいのかというと、専門用語の多さもその一因でしょう。
クラス、インスタンス、オブジェクト、継承、カプセル化、ポリモーフィズム、インターフェース……どれもオブジェクト指向プログラミングにとって重要な仕様なのですが、プログラミング経験がない場合、「なぜそんなものが必要なんだろう」という、そもそもの部分で躓いてしまうことになるでしょう。解説書にはサンプルコードがついていることが多いのですが、それだけでは理解できません。実際に、意味のあるプロジェクトの中で実装していかないと、その仕様がなぜ存在するのか理解できないことのほうが多いはずです。
だからといって、初心者向けの説明によくある概念的な話も、それはそれで不明瞭で、じゃあ実際どうすればいいの?と混乱してしまう元になってしまうことが多いと思われます。
■結局のところオブジェクト指向プログラミングとは何なのか
オブジェクト指向とは、けっしてプログラミング初心者をふるいに落とすための試練ではありません。より複雑で高度になってきたプログラムを設計・実装・管理・メンテナンスしやすくするための手法なわけです。
プログラミングというのは「データとそのデータの処理」=「機能」を記述していく作業と言えます。
いくつか例を挙げてみます。
・プレイヤーキャラクタを移動する機能
「プレイヤーキャラクタの座標」データを、「特定量だけ動かす」処理
この処理は、十字キーの操作という入力情報によって呼び出され、結果として移動後の座標にプレイヤーキャラクタが表示されます。
・敵キャラクターに物理ダメージを与える機能
「プレイヤーの攻撃力」データと「敵キャラクターの防御力」データから「ダメージ量」データを計算して、「敵のHP」データから差し引く処理
この処理は「たたかう」というコマンド操作によって呼び出され、結果として敵のHPが減ります(実際に画面に表示されなくとも内部のデータは変更されています)。
・ゲームがはじまる機能
「ゲームの現在の状態」データを「タイトル画面状態」から「メインゲーム画面状態」に変更する処理
この処理はタイトル画面の「スタート」のクリックで呼び出され、結果としてメインゲーム(ステージ1-1など)画面が表示されます。
オブジェクト指向というのは、単にそれらデータとその処理=機能の切り分け方が従来の手法と違うだけのことなのです。オブジェクト指向的に設計していくことで、データの保護やメンテナンス性・再利用性を高めて、複数人での巨大プロジェクトや頻繁な更新作業、再利用などに有利になる……という触れ込みなわけです。
実際、”処理”の中身に関しては、従来の方法だろうがオブジェクト指向だろうが、同じようにコーディングしていきます。
■どうやってオブジェクト指向を身につけていけばいいのか
私はオブジェクト指向が嫌いです。
なぜ嫌いなのかというと……
・個人作業ので複数人メリットの恩恵がない。
・従来の関数とかモジュール単位でも充分に再利用性があると思っている。
・オブジェクト指向はいちいち記述が冗長になり、全体の把握が面倒。
ざっとこんなところでしょうか。
でもオブジェクト指向、やってます。なぜなら私の使っているUnity(ゲームエンジン)やVisual C#(統合開発環境)がオブジェクト指向に基づいた設計になっているからです。使わざるを得ないってやつです。
逆に言えば、UnityとかVisual C#のようなエンジンや開発環境を使ってある程度勉強を進めている方は、すでにオブジェクト指向プログラミングへの入門を済ませているといっても過言ではありません。
すでに用意されているクラスからインスタンスを生成して、そのメソッドを利用したり、メソッドを追加したりしているわけです。意識せずとも、立派にOOP(オブジェクト指向プログラミング)しています。
他のパラダイム(手続き型とか)から移行してきた私のような人間にとって、オブジェクト指向的な考え方に切り替えるのは結構大変でした。しかし、UnityやVC#からプログラミングに入ればオブジェクト指向もすんなり頭に入ってくると思います。現に、私自身もJavaやC++に挫折後、VisualBasicのおかげでオブジェクト指向を理解する取っ掛かりができたところがあります(こちらのページ参照)。
じゃあもうオブジェクト指向について学ぶこともないじゃないかというと、そうでもありません。いずれ高度なゲームなりツールなりを作っていく場合、エンジンや開発環境が用意しているクラスでは事足りず、独自にクラス設計を行うことになります。そのためにはオブジェクト指向的な考え方は避けて通れません。
では、クラス設計について軽く見てみましょう。
■オブジェクト指向の概念的なお話
よくある横スクロールアクションゲームをオブジェクト指向的な考え方でプログラミングしていくことを考えてみます。
はじめに、オブジェクト指向とは「オブジェクトと呼ばれるモノが互いにやりとりをする」ことでプログラムが進んでいきます。なんのこっちゃ分からないかもしれませんがそう思っておいてください。
上記1~10の数字がふってあるモノがオブジェクトと呼ばれるものです。オブジェクトはそれぞれ「データ」と「処理」を持っています。
1はプレイヤーキャラクターです。データとして「座標」を持っています。処理としては「左右の移動」「ジャンプ」「死んだら残機数を1減らす」を持っています。
2は敵キャラクターです。データとして「座標」「得点」を持っています。処理は「ただ左に移動し続ける」「倒されたらスコアを+200する」とします。
3はアイテムブロックです。データとして「パワーアップアイテム(取ると大きくなる)を出せる状態かそうでないか」を持っています。処理は「アイテムを出せる状態であれば、プレイヤーキャラクターに下から叩かれた時にアイテムを出し、さらに自身をアイテムを出せない状態にする」とします。
4はただのブロックです。処理は「プレイヤーキャラクターに下から叩かれたときに破壊され、スコアが+10される」とします。
5はただの地面ブロックです。処理は「プレイヤーキャラクターが落下しないようにする」とします。
6はコインです。処理は「プレイヤーキャラクターが触れた場合に消え、コイン枚数を一枚増やし、スコアが+100される」とします。
7は残機数です。データとして「プレイヤーキャラクターの残り機数」を持っています。処理は「現在の機数を表示する」「プレイヤーキャラクターが死んだ場合、残り機数を1減らす」「0になったらゲームオーバー」です。
8は獲得コイン数です。データとして「獲得したコイン数」を持っています。処理は「獲得コイン数を表示する」「100になったら残機数を1ふやしコイン数を0にする」です。
9はスコアです。データとして「得点」を持っています。処理は「得点を表示する」「10000点に達したら残機数を1増やす」です。
10はタイムです。データとして「残り時間」を持っています。処理は「ゲームが進行している場合カウントダウンを続ける」「残り時間が0になったら残機数を1減らす」です。
※全てのデータや処理を抜き出すとわかりにくいので適当です。例えば実際は上記すべてのオブジェクトが「座標」をデータとして持っています。また、わかりやすさ優先ということで、実際にはそうは記載しないというものもあります。その辺りはご了承ください。あくまで一例ということで。
でもって、このオブジェクトたちがやりとりを行うことでゲームが進んでいくわけです。
例えば、1のプレイヤーキャラクターが上から2の敵キャラクターとぶつかったときに、敵キャラクターの「倒されたらスコアを+200する」処理と9の「得点を表示する」処理が実行されるというわけです。
つまり、「オブジェクト同士のやりとり」とは、ある条件のときにお互いの任意の処理を実行することと言えます。
ついでに、クラスの話もしておきます。クラスとはオブジェクトの設計図のことを指します。オブジェクト指向プログラミングにおいて最重要項目です。この図の1~10のオブジェクトはインスタンス(実体)と呼ばれるもので、クラス(設計図)から生成されるのです。
つまり、実際のオブジェクト指向プログラミングとは、クラス(設計図)を作り、その設計図を元にしてオブジェクトを生成(インスタンス化する)するわけです。
例えば2の敵キャラは最初から表示されておらず、ある程度マップを進んだ先で出現するとします。その場合、ある程度マップを進んだところで敵キャラクタのクラスを元にして敵キャラクタのオブジェクトを生成=画面に表示し、倒されたならオブジェクトが破棄=画面から消えるというわけです。
なぜ直接オブジェクトを記述しないで、わざわざクラス(設計図)を作ってからそれを元にオブジェクトをインスタンス化するのかということですが、例えば同じ敵キャラクタが複数出現することを考えてください。同じオブジェクトであれば出現数の分オブジェクトを記述するよりも、クラスを一つ記述しておいて、そこから出現数だけインスタンス化した方が楽です。そんなわけでオブジェクト指向ではクラスを作りオブジェクトを利用するときに生成するという仕様になっています。
上図の例では、画面上に表示されている=見えるものをオブジェクトとしていますが、画面に見えないものもオブジェクトとなりえます。
例えば、通常のゲームは以下のように”ゲームの状態”が遷移します。
・タイトル画面 ―(スタートボタンが押される)→ メインゲーム画面(上図のようなもの) ―(死ぬ)→ ゲームオーバー画面 ―(残機が0の場合)→ タイトル画面に戻る
他にも、オプション画面(メニュー画面)、ステージクリア画面、エンディング画面などへの遷移も考えられますね。
ある意味で一番重要な「ゲームの状態を管理する」機能ですが、これを受け持つのが”目に見えないオブジェクト”というわけです。
ここではそれを「ゲームマネージャー」と名付けることにします。ゲームマネージャーはデータとして「ゲームの状態」を持っています。処理は「ゲームの状態に応じて画面上に必要なオブジェクトを配置する」「メインゲーム画面のステージを管理する」などゲーム全般の進捗管理を行う処理を多数持つことになるでしょう。
他にも目に見えないオブジェクト(クラス)は考えられます。例えば、ゲーム音楽を鳴らしたり止めたりする「サウンドマネージャー」、入力機器(パッドとかキーボードとか)によらずに入力情報を担保する「インプットマネージャー」などなど……。
とにかく、目に見えようが見えまいが、ゲーム(プログラム)に必要な機能を抜き出して、それぞれクラスとして切り分け組み上げていくこと、つまり「クラス設計」こそがオブジェクト指向プログラミングの肝となります。
ちなみに、ここで出したクラス、オブジェクトの内容ははあくまで一例です。クラス設計はプログラマによって千差万別です。
千差万別ではあるのですが、大前提として関連するデータと処理は同じクラスにまとめます。例えば、プレイヤーキャラクタのHPというデータや、プレイヤーキャラクターのHPを増減する(ダメージとか回復とか)という処理は、プレイヤーキャラクターのクラスに含めるわけです。当たり前といえば当たり前なんですが、この大前提を常に頭に入れておくことで堅牢なクラス設計が可能になります。
■オブジェクト指向の用語について
できるだけ専門用語を使わない方向で話を進めてきましたが、ここでオブジェクト指向の仕様についていくつか説明しておきます。わからないものは適当に飛ばしてください。
・プロパティとメソッド
言語の仕様にもよりますが、今までデータと呼んでいたものを「プロパティ」、処理を「メソッド」と呼びます。手続き型言語に当てはめれば、プロパティとは変数のことで、メソッドとは関数のことです。
・カプセル化
オブジェクト指向、クラスの重要な思想の一つです。例えば前項の図にあった3のアイテムブロックオブジェクトを振り返ってみてください。このオブジェクトはデータとして「パワーアップアイテム(取ると大きくなる)を出せる状態かそうでないか」を持っていますが、このデータを利用するのはこのアイテムブロックオブジェクトのみで他のオブジェクトからは一切このデータにアクセスする必要がないということにします。
(※ゲームの内容によっては、このデータに他のオブジェクトがアクセスすることもあります。例えばステージクリア時にマップ上全てのアイテムブロックの状態を取得して、全ブロックを叩いていればボーナススコアが入るようにする場合などです。しかしここでは、他のオブジェクトがこのデータにアクセスすることはないとします)
その場合、その仕様を知らない別のプログラマが(もしくは時間が経っていてそういう仕様だと忘れていた場合も)他のオブジェクトから「パワーアップアイテム(取ると大きくなる)を出せる状態かそうでないか」データを書き換えてしまうようなコードを書いてしまうと困りますよね。
そんなときには、カプセル化という仕様によってこのデータを守り、アイテムブロックオブジェクトからしか変更できないように制限することができます。
・継承
分かりにくくなるので前項の例では全く触れていませんが、オブジェクト指向には継承と呼ばれる仕様があります。どんな仕様かというと、あるクラスを元にして(親クラス)、そのデータや処理を引き継いたクラス(子クラス)を作ることができるのです。
例えば前項の図の、3:アイテムブロックと4:ただのブロックオブジェクトを振り返ってみます。この二つ、似ていると思いませんか? そう、どちらもプレイヤーキャラクターに下から叩かれることによって変化が起こります。違いといえば、叩かれた際に壊れるのか(ついでにスコアが増える)、壊れずにアイテムを出すのかだけです。
そんなとき、「ブロック」というクラスを設計し、「プレイヤーに叩かれたときの処理」をメソッドとして記述しておきます。そして、それを継承して、新たに「ただのブロッククラス」と「アイテムブロッククラス」を作り、それぞれが継承した、「プレイヤーに叩かれたときの処理」を上書きして(オーバーライドといいます)、それぞれ「叩かれたら壊れてスコアになる処理」、「叩かれたらパワーアップアイテムを出す処理」とするのです。
また、図には表示されていませんが、キノコ的な敵キャラだけでなく、カメ的な敵キャラも出したい場合も考え方は同じです。
なんでそんなことするの?と思われるかもしれませんが、同じ系列のクラスの親クラスが存在している(抽象化、汎化)ことで、拡張が楽になります。例えば、新しいブロック(叩くとワープする)や、新しい敵キャラ(花のお化けキャラ)を追加するのが楽になるのです。また、キノコ的な敵キャラ、カメ的な敵キャラは別クラスですが、「敵キャラクラス」が存在することであたかも同じもののように扱うこともできるのです。
この辺りは、実際にクラス設計をしていかないとわからないと思います。ちなみに、最初から親クラスを作り、それを継承して子クラスを作ることもあれば、逆にいくつかの子クラスに対して(機能追加の必要性から)その親クラスを作ることもあります。
その他、インターフェース、ジェネリック、デリゲートなどなど、いろいろな仕様があるのですが、これらは実際に使いながら覚えていったほうがいいです。
一つだけ言えるのは、どれもオブジェクト指向を便利にするためのものだということです。別に知らなくても困らないけれど、知っているとすっきりと設計できるとかそういうレベルの話なだけです。
■今後のために
というわけで、オブジェクト指向の概要について触れてみました。少しでもヒントになれば幸いです。ほかのサイトや書籍、そして実際に自分で設計をして、オブジェクト指向をマスターしていってください(マスターしてない自分が言うのもなんですが)。
オブジェクト指向に慣れてきたら、デザインパターンについても勉強してみてください。デザインパターンとは簡単に言ってしまえばプロジェクトのジャンル毎にこんなパターンで組むといいよというものです。
とにかく、勉強のポイントは、最初から気張ってなんでもかんでも吸収しようとせず、適当に無理せず、そして実際に手を動かしながら(コーディング)学んでいくことだと思います。途中で詰まろうが挫折しようが、いずれ理解できるはずです。
それでは。
<プログラミング独学法 : 物語を伝えるための日本語基礎技術>