【Unity】インターフェースって結局いつ使うの?
目次
はじめに
こんにちは!RiG++副団体長、コーディング課2回生のにしちゃんです!
本日はUnityやオブジェクト指向言語を勉強していく中で、多くの人がつまずくであろう「インターフェース」について解説します!
- インターフェースが何かは分かるけど使い方が分からない!
- Unityではどのように使えばいいの?
以上の疑問点を解決したいと思います。
この記事がインターフェースを使い始めるきっかけになってくれたらうれしいです!
これからUnityを勉強していく大学の後輩たちに見てもらう気持ちで書いていきますので、Unityでの例を挙げたりUnityのライブラリを使用したりしていることにご留意ください。
必要な前提知識
インターフェースを理解するにあたって、前提知識として知っておいてほしい(後から話に出てくる)事項が2点あります。
冒頭にもある通り、インターフェースの定義の仕方等は学習済みのものとしています。
知っている人にとっては「当たり前じゃん」なことも書いてますが、確認程度にさらっと読んで行ってください。
①継承関係にあるクラスのキャストについて
例を見てみましょう。
上位クラスから順番に、モブクラス、敵クラス、スライムクラスと継承しています。ここで、スライムクラスのインスタンスを生成したり取得したりする旨を記述する際、以下のような書き方が許されています。
Slime slime = new Slime(); //通常通り
Enemy enemy = new Slime(); //親クラスEnemyとして参照可能
Mob mob = new Slime(); //より上位でも参照可能
またUnityにおいてこのスライムクラスがゲームオブジェクトにアタッチされており、GetComponentを利用して取得する場合でも、以下のような書き方が許されています。
Slime slime = ゲームオブジェクト.GetComponent<Slime>(); //通常通り
Enemy enemy = ゲームオブジェクト.GetComponent<Enemy>(); //親クラスEnemyとして取得・参照可能
Mob mob = ゲームオブジェクト.GetComponent<Mob>(); //より上位でも取得・参照可能
このように、正しい継承関係にあれば、自身より上位のクラスのインスタンスとしてでも取得・参照することができます。
インターフェースの解説においてもこの事実は大きな意味を持ちますので、頭の片隅に置きつつ次の話に行きましょう。
②インターフェース型での取得
これが一番大切です!!!!
インターフェース勉強したけど分からないという人は、この大切な事実を忘れがちor蔑ろにしがちです。これが一番大事です。これができるからインターフェースはめちゃくちゃ便利なんです!!
インターフェースを継承したクラスのインスタンスは、そのインターフェース型で取得・参照することができます。
ここでまさに前項の「継承関係にある自身より上位のクラスでも取得・参照が可能」が出てきましたね!
インタフェースも「継承」しているものなので、自身より上位のクラスと言えます。
ここでも例を見てみましょう。
スライムクラス、勇者クラス、木箱クラスにIMovableインタフェースが継承されています。今回は省略していますが、各クラスではMove()メソッドなんかが実装されている状態でしょう。ここで、スライムインスタンス(名前をslimeとします)、勇者インスタンス(hero)、木箱インスタンス(box)を取得する旨を記述する際、以下のような書き方が許されています。
IMovable moveObj1 = slime; //SlimeのインスタンスをIMovable型で参照
IMovable moveObj2 = hero; //HeroのインスタンスをIMovable型で参照
IMovable moveObj3 = box; //BoxのインスタンスをIMovable型で参照
また、Unityにおいてこれら3つのクラスがそれぞれのゲームオブジェクトにアタッチされており、GetComponentを利用して取得する場合でも、以下のような書き方が許されています。
IMovable moveObj1 = スライムのゲームオブジェクト.GetComponent<IMovable>();
IMovable moveObj2 = 勇者のゲームオブジェクト.GetComponent<IMovable>();
IMovable moveObj3 = 木箱のゲームオブジェクト.GetComponent<IMovable>();
このように、インターフェース型でインスタンスを取得・参照できることを忘れないでください!!とても大事なことです!!!
特にGetComponentのジェネリクスにインタフェースを指定できることを知らなかった人もいるのではないでしょうか?
インターフェースに限らず、上位クラスをジェネリクスに指定したGetComponentで下位クラスを取得することができます。
さて、以上2点を意識したまま本題に入りましょう!
インターフェースっていつ使うの?
「インターフェースについて勉強したけど、いつどうやって使えばいいのか分からない!!」
それなら一回困ってみましょう!!!
普段通りプログラミングをしていく中で実際に困った問題にぶち当たり、その解決法として新たな技術を取り入れるのが一番勉強になります。
またまた例を挙げます。
勇者を操作するアクションゲームを作っているとします。勇者は攻撃することでスライムや村人にダメージを与えたり、木を破壊することができます。
クラス設計としては、攻撃を受ける側(スライム、村人、木)のクラスにDamage()メソッドを実装し、呼び出された際の処理をそれぞれ書いておきます(スライムクラスや村人クラスであれば自身のフィールド変数であるhpを減らすだとか、木クラスであれば自身を非アクティブにするだとかですね)。
そして勇者クラス側では、攻撃時にヒットしたゲームオブジェクトを取得した後、それにアタッチされているスクリプトをGetComponentで取得しDamage()メソッドを呼び出す、という構造にします。
まあまあよくある構造に見えますが、実際にコーディングに入ると、勇者クラスが少し面倒くさいコードになってしまいました。以下のコードを見てください。
//勇者クラスの任意の箇所
GameObject hitObj = 攻撃が当たったゲームオブジェクトを取得するコード;
if (hitObj.GetComponent<Slime>() != null){
//対象がスライムクラスであればSlime型で取得する
Slime slime = hitObj.GetComponent<Slime>();
slime.Damage(100);
}
else if (hitObj.GetComponent<Villager>() != null){
//対象が村人クラスであればVillager型で取得する
Villager villager = hitObj.GetComponent<Villager>();
villager.Damage(100);
}
else if (hitObj.GetComponent<Tree>() != null){
//対象が木クラスであればTree型で取得する
Tree tree = hitObj.GetComponent<Tree>();
tree.Damage(100);
}
攻撃があたったゲームオブジェクトであるhitObjにアタッチされているスクリプトをGetComponentで取得する際、そのスクリプトが何クラスかによって記述を変えなければならないため、if文で対象が何クラスかを判定して、それに合った参照方法で取得しDamage()メソッドを呼ぶようにしました。
またGetComponentを減らしてみたりと、より良い書き方に直したりもしましたが結局if文からは逃れられませんでした…
この3つだけならまだしも、スライム以外に敵を実装したり木以外にも壊せるオブジェクトを実装したりする度にif文が増えていくと考えると、とてもこのコードではやっていけないと判断しました…
う~ん困ったなあ!!!
まず、今どういう状況で、なぜ困っているのかまとめてみましょう。
<状況>
- 継承関係にない全く異なるクラスであるスライム・村人・木クラスに、似たような処理であるDamage()メソッドがある
<問題>
- Damage()メソッドを呼び出すためにはまず対象のインスタンスを取得・参照しなければいけない!
- しかし参照時にクラスを宣言したり、GetComponentのジェネリクスにクラスを指定したりといったタイミングで、対象が何クラスのインスタンスであるかによって記述を変えなければいけない!
- そのために対象が何クラスのインスタンスかを判別するif文から逃れられない!!!
さて、この問題をどう解決すれば良いでしょうか?
大問題なのは、対象によってコードが変わってしまうという点です。このせいでif文から逃れられなくなっています。
それなら、対象が何クラスかに関係なく同じコードで書ければif文は要らないはずです!
ここで前提知識①を思い出してください。これによれば、スライム・村人・木クラス共通の親クラスがあれば、3つともその親クラスで一様に取得・参照できます…!
しかしスライム・村人・木となると全く異なる概念であるため、共通の親クラスを作ったりはしたくないはずです。
相手がどんなクラスだろうと一様にDamage()メソッドを呼び出す方法はないでしょうか…?
ここでインターフェースの登場です!
以下の図のようにインターフェースを実装しました!
まずIDamagableインターフェースを定義します。Damage()メソッドの宣言のみを行っており、実装については今まで通り各クラスで記述します。
次にスライム・村人・木クラスでIDamagableインターフェースを継承します。Damage()メソッドは既に書いているので変更はないです(オーバーライド扱いになりました)。
最後に勇者クラスの記述を以下のように変更します。めちゃくちゃきれいになりました!
//勇者クラスの任意の箇所
GameObject hitObj = 攻撃が当たったゲームオブジェクトを取得するコード;
//damageObjは実際にはSlimeかVillagerかTreeのいずれかのインスタンス
//どれが来ても上位クラスであるIDamagableでなら取得・参照できる
IDamagable damageObj = hitObj.GetComponent<IDamagable>();
//Damage()メソッドの宣言はIDamagableインターフェースで済ましてあるので呼び出せる
damageObj.Damage(100);
美しすぎる…。何が起こったかお分かりでしょうか?
スライム・村人・木クラスは共通の上位クラスであるIDamagableインタフェースを継承しています。
ここで前提知識①より、この3つのクラスはどれも共通の上位クラスであるIDamagableで一様に取得・参照できます。
また前提知識②より、GetComponentのジェネリクス部分もIDamagableにすることができます。
これにより、勇者クラスは対象が何クラスか知らずとも、IDamagableを継承さえしていればDamage()メソッドが呼び出せます!!
この「対象が何クラスか知らずとも」がめちゃくちゃ助かるんです!
この先どれだけダメージを受けるやつを実装しようと、IDamagableを継承すれば勇者クラスに変更を加えることなく正しく動作してくれます。これがまじででかい。
改めて、インターフェースとは何か?
今回挙げた例では無駄なif文を無くすための手段としてインターフェースを利用しましたが、インターフェースの使い方やメリットは他にもたくさんあります!!
当たり前ですが、メリットがあるからこそわざわざインターフェースを利用するわけで、無理やりにでも使わなきゃいけないものではありません。
ここで改めて、インターフェースとは何か?を考えてみましょう。前項の例を思い出しながら読んでいくと分かりやすいと思います。
小難しい言い回しですね。前項の例(インターフェース導入後)に当てはめて考えましょう!
勇者クラスが実際にダメージを与えるのはスライムクラス、村人クラス、木クラスです。
しかし勇者クラスのコードを見てみると、Slime、Villager、Treeクラスの宣言が一切ありません。
その代わり、それらすべての上位クラスであるIDamagableインターフェースで取得・参照しています。
すなわち、勇者クラスは対象が具体的に何クラスなのか、一切気にしていません。
より抽象的な、”ダメージを受けれるよ”インターフェースに依存してます。
具体的な型ではなく抽象的な型に依存してることによって、多くのメリットがあります。…が、深追いするとどんどんややこしい話が出てくるジャンルなので、この記事では一つだけに着目します。
柔軟性・拡張性が向上
少し前の項でさらっと言ってしまいましたが、例えばスライム・村人・木以外にもダメージを受けるモノを実装したいとなったとき、インターフェース導入前のコードだと、勇者クラスにも変更が生じます。
新たなif文を追加して、新規実装したクラスに合わせた取得・参照とDamage()呼び出しが必要です。
また、ダメージを与えるモノが増えたらどうなるでしょうか?
例えば周囲にダメージを与える爆弾クラスを実装するとなったら、その中にも勇者クラス同様のめちゃ長if文を書くことになります。
さらにここでダメージを受けるモノを追加すると…考えたくないですね。変更箇所だらけです。
とにかく、
面倒くさいうえ、コードの保守性に欠けます。
これに限ります。
これに対しインターフェース導入後のクラス設計であれば、新たにダメージを受けるモノを実装する際、IDamagableインターフェースを継承し、Damage()メソッドの実装を書いたクラスを追加するだけで良いです。
その新規クラスが敵だろうと置き物だろうと関係ありません。勇者からの攻撃を受けたいならIDamagableインターフェースを継承しDamage()メソッドの中身を書く、これだけです。
これにより、勇者クラスはどんな”ダメージを受けれるよ”インスタンスでもダメージを与えられる(柔軟性)し、これからどんなダメージを受けるモノが追加されても対応できます(拡張性)。
おわり
さて、ここまで読んでなんとなくインターフェースの使いどころが分かったでしょうか?
クラスの継承だけでは解決しきれなかった、異なる概念の似た処理に対する呼び出しが簡単かつ安全に実装できました。
しかし、インターフェースの素晴らしいところはこんなもんではありません!!!
この記事で話したインターフェースの使い方は、かなり初歩的で理解しやすいものの一つでしかありません。
この先きっと多くの人が「良いクラス設計とはなにか?」という疑問にたどり着くと思います。そのとき、世の中の頭が良いプログラマーさんたちが考えたいろんな設計思想やフレームワークを見ると思いますが、そこでインターフェースがたびたび登場します。
気になった人は、こんな単語を調べてみると良いと思います!!
- ポリモーフィズム
- 抽象クラスとの使い分け
- 依存性逆転の原則(DIP)//ちょっと難しい
- 依存性の注入(DI) //難しい
後半2つは少し難しいですが、かなりオブジェクト指向の核心に触れる内容ですので、興味があったら是非調べてみてください!
ここまで読んでいただきありがとうございました!!
記事を読む前より、ほんの少しだけインターフェース便利じゃん!と思ってもらえていたらうれしいです!
以上です!