抽象化は詳細に依存すべきではないとは

はいどーも一日5時間のお昼寝を義務付けているニートですこんばんは。
本日は前回の記事の追記となります。

お昼寝バンザイ

前回僕は「抽象化は詳細に依存すべきではない。詳細は抽象化に依存すべきである」の項目で「これはまだ理解が完全ではないのですが、インターフェイスを使えば自ずと達成されるのではないかなと思っています」と書きました。
しかし実際のところ、しっくりと来ていませんでした。
ですがお昼寝を終えて出かける準備をしていたころ、ふと「抽象化は詳細に依存すべきではない」の本当に言いたいことが理解できてしまいました。
それはまさに僕がかつて「これインターフェイス使う意味あるか~?」と思いながら作っていたものに相当するダメパターンに対する警告だったのです。
ちゅーわけでそのダメパターンについて説明していきますよ。

抽象化が詳細に依存するとは

例えばあなたは今、アクションゲームを作っているとします。
ゲーム画面で飛んだり跳ねたりするキャラクターのことをGameActorBaseクラスとして表現したいと考えたとしましょう。
GameActorBaseはさらに子クラスとしてGamePlayerやGameNormalEnemyやGameBossEnemyなどの具象クラスとして表現されるかもしれませんね。
さてこのときGamePlayerは仮に以下の要件を満たさなければならないとします。

・移動可能
・射撃攻撃可能
・ダッシュ可能

このときインターフェイスとして以下のようなIGamePlayerというものを作ったとします。

void Move()
void FireAction()
void Dash()

このGamePlayerはこのIGamePlayerインターフェイスを実装すれば、

・移動可能
・射撃攻撃可能
・ダッシュ可能

を実装したことになります。

この動きは汎用性がありそうですし、似たようなIGameNormalEnemyなんていうインターフェイスを作りたくもなかったので、GameNormalEnemyにもIGamePlayerインターフェイスを実装させようとあなたは思い立ちました。
最初はそれでうまくいきそうです。
ですが後になって「そうだ、プレイヤーはジャンプもできるようにしたいぞ」と思い、IGamePlayerインターフェイスに「ジャンプ」を表す機能を追加したとしましょう。
具体的にはこんな感じでしょうか。

void Jump()

そうすると、本来は抽象化であるはずのIGamePlayerインターフェイスが、詳細であるGamePlayerに依存して内容を変更したことになります。
これはまさに「依存性反転の原則」に違反していることになります。

さらに言うと、IGamePlayerインターフェイスを変更したことにより、本当は実装を変化させたくないかもしれないのに、GameNormalEnemyの実装までも変化させなければなりません。
これは抽象化が不十分であることの兆候でしょう。

インターフェイスを見直す

先ほど作成したIGamePlayerインターフェイスは以下の4つの機能を持っていました。

・移動可能
・射撃攻撃可能
・ダッシュ可能
・ジャンプ可能

ですがこれは、例えば以下のようなインターフェイスに分離できるでしょう。

・IMoveable
・IAttackable
・IDashing
・IJumpable

そしてGamePlayerクラスは上記全てのインターフェイスを実装し、GameNormalEnemyはIJumpable以外のインターフェイスを実装しておきます。
こうすると、例えばあとでGamePlayerクラスが「そうだ、いっそプレイヤーは攻撃できなくしても面白いかもしれないぞ」という要求が出てきたとき、単にIAttackableインターフェイスを外せばよいことになります。
詳細であるGamePlayerクラスは抽象であるIAttackableインターフェイスに依存しているので中身を変更しなければなりませんが、「依存性反転の原則」は守っていることになります。
また、先ほどと違い、GamePlayerクラスに対する変更はGameNormalEnemyクラスに一切の影響を及ぼしません。

さらに言うなら、GamePlayerクラスを利用する側のクラスも、GamePlayerクラスという詳細ではなくインターフェイスに依存しているならば、GamePlayerクラスの変更は「GamePlayerクラスを利用する側のクラス」にも影響を与えないはずです。
いやあ素晴らしいですね!

補足の補足

先ほどインターフェイスを分離しました。
この後さらに、キャラクターをワープさせられる機能をつけたくなったとします。
インターフェイスを増やすのが面倒だという理由で例えばIMoveableインターフェイスにDoWarpというメソッドを付け加えたとしましょう。
これでは再び「抽象化が詳細に依存する」ことになってしまっています。
さらに言うなら、本当はワープが必要でないIMoveableインターフェイスを実装しているキャラクターまで実装を変更しなければならなくなります。
以上の弊害を避けるため、ここでも新しく「ワープ可能であることを示すインターフェイス」を作成することが望ましいでしょう。

詳しくは「インターフェイス分離の原則」でググってもらえればいいんじゃないでしょーか。
SOLID原則もそれぞれが繋がっているんだなあと感じました。まる。

実際のゲーム開発では

市販さているようなアクションゲームがこのような手法で作られているかどうかは、ぶっちゃけ知りませんw
この辺はアクションツクールMVを買って勉強しようかなと思っています(まだ販売してませんが)。

また、僕はまだまだ大規模どころか中規模な開発すらしたことがないので「全てがこんなに上手くいくのかなー」という疑問もあるっちゃあります。
例えばクライアントがインターフェイスに依存するのはいいが、でも実は例えばGamePlayerクラスの中身を意識して作らないと難しいときもあるんじゃないかなーとか思わないでもないです。
この辺は単に僕の設計力がないからなんでしょうかね……。
ただ少なくとも、IGamePlayerのような大きなインターフェイスを作るよりは、インターフェイスを分離した方がよい理由は理解できましたよ。

だが、だがしかし。
それでもやっぱりインターフェイスじゃなくて直接クラスの全てを扱えたほうが最初は簡単に色々できそうだな……とか思っちゃうこともありそうです。
特に開発の初期は「インターフェイスなんか作らんと直接操作したら今すぐ動くものが作れるんじゃー!」とかいう誘惑に負けそうです。
それに、ごく単純な話ですけど、設計するのが難しいですよね、この原則を守ってるコードって。
変更に強いのは間違いなくSOLID原則を守っているコードでしょうが、自分の手腕とのバランスがまだまだ難しそうです。

終わりに

ちゅーわけで昨日の記事の補足を書きました。
寝てる間にピンときたのでよかったです。
お仕事も相変わらず募集中です。

ほなそんな感じでまた。

フォローする