はじめに
この記事は、第1章「ModelとViewの分離」の補足記事です。元記事の
Modelが純粋なオブジェクト指向で開発できる
という部分について、より詳しい考察を進めていきます。
オブジェクト指向の特徴
「オブジェクト指向とは何か」については既に大量に素晴らしい記事が書かれており、私が付け加えることはしません。
参考記事
人類がオブジェクト指向を手に入れるまでの軌跡
オブジェクト指向と10年戦って分かったこと
オブジェクト指向のいろは
「オブジェクト指向でなぜ作るか」と言う書籍もおすすめです。
要約すれば「セットになっているデータ」と「フィールドに付随する処理」を「オブジェクト」に包み込むことで、抽象化して再利用性、保守性を高めようというプログラミング手法です。
あくまでも最も大切な考え方は「データのカプセル化・抽象化」です。
また、根本には1つのオブジェクトは1つのまとまったデータを管理しようという発想(単一責任原則)があります。
コンポーネント指向の特徴
「機能のまとまり」を設計し「機能に付随するデータ」を含めたものを「コンポーネント」に包み込み、コンポーネントの集合としてオブジェクト・ソフトウェアを作成するという仕組みです。
Unityにおいては「動く」とか「当たり判定が出来る」とか「物理演算ができる」の機能をコンポーネントととして管理しています。
利点:機能の差し替えが簡単
全てのオブジェクトは、コンポーネントを付け替えることで機能を増減させることが出来ます。
UnityでもGameObject
にコンポーネントをつけると機能が拡張できますね。
具体的な状況を考えてみましょう。
下のように、「敵」と「プレイヤー」のオブジェクトがあるとします。
このときにわかるコンポーネントの特徴は以下の3つです。
-
コンポーネントを無効化すれば、無効化した機能のみが動かなくなる
上の状況では、プレイヤーは攻撃のみが無効化されるが、移動はできます。 -
新しいGameObjectをシーン上に生成しても、コンポーネントを適切につければゲームのキャラクターになる
上の状況では、新しく作ったGameObject
に対して、攻撃機能を追加しているので、攻撃が出来るようになります。
今までどんなオブジェクトだったかに関係なく、コンポーネントによって動作が決まるので柔軟性が高いです。
これをオブジェクト指向でやろうとすると、まず上位クラスがインターフェースを公布して、インターフェースに対してプログラミングすることである程度は実現できます。しかしながら、これは各クラスごとの機能追加であり、しかも各インターフェースごとの機能追加です。
- あるインスタンスに直接機能追加する
- インスタンスの機能をひとつずつ停止する
- あるインスタンスの機能の一部を別のオブジェクトにコピーして付け替える
などは難しいです。コンポーネント指向のもつ柔軟性を持てていません。
その理由は、オブジェクト指向におけるクラスは単一責任原則にのっとって「1つのまとまったデータ」しか扱うことが出来ないからです。あるデータが関与する領域から離れるような機能は、そもそもオブジェクトに含めるべきではないのです。
逆にAddComponent
やGetComponent
は非常に汎用性が高く強力で、オブジェクト指向にはない柔軟性を持っています。
問題点:コンポーネント間の通信が苦手
コンポーネント指向は「互いに関係ない機能の集まり」のようにしてソフトウェアを構成します。
例えば、Animator
コンポーネントとCollider
コンポーネントは何の関係もない存在ですね。
この「機能の集合」というのは、実は互いのデータをあまり参照しないという前提に立っています。
なぜなら、コンポーネントは差し替え可能な機能の集合だからです。データを参照するとは依存するということなので、差し替え可能性を著しく下げてしまうのです。詳しくは依存関係についての第1章補足を参照してください。
具体的には、
-
RigidBody
によるTransform
の操作とスクリプトによるTransformの操作が同時に発生して思い通りにGameObject
が動かない、みたいな状況を引き起こす -
RigidBody
・Collider
はそれぞれの有無によって挙動を変えるため、この2つのコンポーネントは常に同時に考えるひつようがある -
RequireComponent
属性を大量につけた結果、事由に付け替えられることが売りのはずのコンポーネントがガチガチに固まって、結局あるゲームオブジェクト専用のコンポーネントになってしまう -
Content Size Fitter
やLayout Element
など、複数コンポーネント間で通信する自動レイアウト機能がややこしい
などの例があります。
具体的に「キャラクターの移動」と「キャラクターの攻撃」をつかさどるコンポーネントを作ってみましょう。
この2つのコンポーネントが「プレイヤーのHP」を共通して参照する場合、実装方法としては
- コンポーネントを2つ合体させる
- 移動側・攻撃側のどちらかのコンポーネントにデータを持たせて、もう片方がそれを参照する
- ステータス保持用コンポーネントを作成する
という方法が考えられますね。
1.は機能の差し替えが出来ないのでコンポーネントのメリットがなくなります。
2.は明らかに保守性が下がりますね。なぜなら、どちらかにコンポーネントを置く必然性がないので、「移動コンポーネントにHPがある」という非直感的な情報を記憶しておく必要があるからです。
3.はコンポーネント指向の理念である「機能ごとにコンポーネントを作る」という原則を破壊しています。実質的に、移動や攻撃のコンポーネントがRequireComponent(ステータスコンポーネント)
と書いてしまうため、コンポーネントの柔軟性は失われます。
また、プレイヤーのHP操作という機能が各コンポーネントに分散してしまい、HP関連のバグの原因が追いにくいです。
逆にHP関連の処理をステータス管理コンポーネントにすべて入れた場合、コンポーネントが追加されるごとにそれに対応するようなメソッドを入れる必要があります。コンポーネントが単一機能だという観点から見ると、割と不自然ですね。
また、コンポーネントはGitで追えないですし、Visual Studioのリファクタリング・検索などの支援を受けられない点でも辛いです。
いずれにせよ、Unityにおける機能中心のコンポーネント指向は、複数のデータの共有や、データの一貫性保持という観点においては、オブジェクト指向に劣る部分があると思います。
Unityではどう使い分けたらいいのか
Viewにおいては、良い悪い以前にコンポーネント指向に従って開発するしかありません。
なぜならUnityがコンポーネント指向だからです。
Modelにおいては、機能よりも純粋性を保ったデータを処理することが大事なのでオブジェクト指向を利用したほうが良いでしょう。Modelの定義からして「画面に関係なくゲーム全体で使えるデータと処理」であるので、オブジェクト指向で書くべきです。
また、C#はオブジェクト指向言語ですので(マルチパラダイム言語なのでいろいろな書き方が出来るものの)、純粋なC#で書くことが望ましいModelは、オブジェクト指向で書く基盤が整っています。
おわりに
「コンポーネント指向」という言葉はWebの文脈とUnityの文脈でかなり違っているように感じます。
すくなくともUnityにおいては「単一機能のまとまり」と捉えるのが正しいと思います。
Monobehaviourというクラスを継承させらえることからも明らかですね。
私も当初は、C#がオブジェクト指向の言語であることから、Monobehaviourをオブジェクト指向の文脈で捉えようとして苦労しました。