はじめに
この記事は、第1章「ModelとView」の補足記事です。未読の方はまずそちらをご覧ください。
ここでは、メイン記事における
Viewに依存したロジックを書くこと自体が出来なくなり、見た目の変更がバグを誘発する状況を作りにくくできる
という部分について、より詳細に書いていきます。
ソフトウェア設計における「依存する」とは
「ソフトウェアAがソフトウェアBに依存する」とは、「ソフトウェアAはソフトウェアBが存在しないと動かない」という意味です。
具体的には、クラスAが内部の関数でクラスBに定義されているメソッドを呼んだ場合には、クラスAはクラスBが存在しないと動かなくなります。この状況を「クラスAはクラスBに依存する」と示すわけです。
「依存」という言葉は、深い理解なしに「とにかく悪いもの」「とにかく疎結合は常に正義」のように受け取られがちです。しかし、プログラムを分割することと依存関係は不可分なものであり、だれにも依存しないクラスは神クラスになります。
したがって、依存による問題点とそれを解決する正しい依存方法について学ぶことが大切です。
以下、上位モジュールは「呼び出す側」、下位モジュールは「呼ばれる側」を意味することにします。
依存による問題点
下手に書くと役割分担がうまくいかない
クラスAがBの変数を大量に参照し、クラスBの持つべきデータを操作すると、クラスBのやるべきことがクラスAに奪われます。
つまり、上位モジュールが仕事をしすぎた場合、下位モジュールは役割を失います。
さて、この現象がどのように依存と関係しているのかというと、この状況で下位モジュールは上位モジュールで行われる処理がないと実質的に仕事をできません。なぜなら、上位モジュールがBのやるべき仕事をやりすぎているからです。
結果、「BはAがないと存在できない」という状況が生まれます。これは、「Aがないとダメ、BもAがないとダメ」ということになるわけです。これは実質、役割分担が完全に破綻している状況です。本質的にこれは、A,Bが一つのクラスである場合と同じです。
加えて、1つのクラスでやるべき内容を複数クラスでやることで、書く場所が分散して処理が追いづらく、可読性・保守性が下がります。使いこなせない分割・共通化は巨大な1つのクラスよりも複雑性を増します。
変更の影響は伝搬する
AがBに依存している状況において、Bが変更したとしましょう。
すると、AはBの影響を受けてしまい、Aの改修が必要です。
つまり、あるソフトウェアの要素を変更すると、それに依存するすべての存在が変更を強制されるということです。
具体例を挙げましょう。Unityが「Unity2023.1からMonobehaviourが最初に呼び出すメソッドであるStart()メソッドの名前をBegin()に変更する!」とアナウンスした場合、すべてのUnityユーザーがStart()を全部Begin()に書き換えないといけません。
良い依存関係の原則
安定依存原則
「ソフトウェアが別のソフトウェアに依存する場合は、自分よりも安定しているものに依存すべきだ」という原則です。「たくさんのものに依存されるソフトウェアは、安定させるべきだ」という言いかえもできますその利点は次の2つです。
- 変更の影響の伝搬を減らす(保守性の向上)
- 不安定なものを安心して頻繁に変更できるということである(柔軟性の向上)
この原則は問題点「変更の影響は伝搬する」から導き出されます。変更の伝搬を最小限に抑えるために、依存される存在はできるだけ安定化させることが重要だ、という理屈になります。
しかしこれは、デメリットの低減以上のメリットを作り出してくれます。
それは、「安定なものは不安定なものに依存しないので、不安定なものを変えても安定なものは変わらない。したがって、不安定なものを安心して変更できる」というメリットです。
これは、不安定なものを微調整することが容易になることにつながり、特にゲーム開発においては大きいメリットです。
抽象依存原則
「ソフトウェアが別のソフトウェアに依存する場合はより抽象的なものに依存するべきだ」という原則です。
ここでいう抽象的とは「具体的な実装を無視して、外部から見た振る舞いだけをとらえたもの」ということです。
具体的な例をいくつか挙げると
-
「配列から特定の要素を探索する」という振る舞いを実装する方法には「線形探索」「二分探索」など複数の実装方法があります。しかし、利用者が欲しいのは「配列から特定の要素を探索する」と言う部分であり、具体的なアルゴリズムに依存する必要はない。
-
UIにおいて、「選択状態のボタンを示す」という振る舞いを実装する方法には「選択ボタンだけ色を変える」「ボタンをアニメーションさせる」などの実装方法があります。しかし、利用者がやりたいことは「選択状態のボタンを示す」ことであり、具体的なアルゴリズムに依存する必要はない。
この原則をもっともよく使う状況は、SOLID原則における「依存性逆転の原則」を使おうとするときだと思います。
上位モジュールが下位モジュールに仕事を委託するときには、直接下位モジュールを呼ぶのではなく、インターフェースを公布して、インターフェースに対してコードを書くべきだという原則です。
詳しくは参考記事などを参照してください。この原則が有用な理由は
- 上位モジュールが具体的な実装(変わりやすい、不安定)ではなく、抽象的な振る舞い(変わりにくい、安定)に依存することで、上位モジュールが変更の影響を受けにくく、安定性が上がる。
- 下位モジュールの差し替えが簡単に行えるため、下位モジュールの実装の柔軟性が上がる。
つまり、抽象依存原則は、実質的に安定依存原則の1パターンだということです。
ゲームにおける安定・不安定
抽象依存原則、安定依存原則はあくまで原則であり、その具体的な適用、実装とは距離がある話です。
では具体例にゲームにおいてはどれに依存させればいいのかを考察してみましょう。
今回は経験的にどのようなものが安定かを安定度という仮の数値で表してみました。
安定度1:画面装飾
例:座標変更、ボタン画像の差し替えなど
Webに例えるとCSSに相当します。
常に変化しますし、どうしても画面を見て決めたい部分です。
もし画面装飾が依存する項目を持つと、「デザイナーが画面の見た目をいじるたびにプログラマが依存するコードを書き換える」という辛い工程が発生します。
安定度2:画面構造
例:画面に表示するボタン、スクロールバーなどのUI部品など
Webで例えるとHTMLに相当します。Viewが担当します。
もし画面構造が依存する項目を持つと、「デザイナーが画面の要素を増やす、減らすたびにプログラマが依存するコードを書き換える」という工程が発生します。
安定度2:画面操作(出力)
プログラムからのUI情報の書き換えに相当します
例:アニメーション、テキスト変更など
ゲームではないアプリケーションでは安定度3に分類できるものです。
ゲームでは画面を動的に変更する演出がとにかく豊富です。むしろ「ゲーム画面は固まった無機質な画面よりいつでも何かを動かしていた方が面白く思える」といった事情から、動的な変更がない方が少ないです。
したがって今回は安定度2に分類しました。
安定度3:画面操作(入力)
Viewから入力を受け取り、Modelに渡す部分です。
こちらは出力とは違って、Modelに依頼するだけなのでどのように書こうとゲームのプレイヤーの目には触れません。したがって、安定度は出力よりも高く3です。Viewから受け取った入力データをModelに渡す部分です。
安定度4:細かいゲームロジック
Modelにおける下位モジュールを指します。
例:レースゲームにおいて、「車情報」を保存している部分。新しいステータスなどの増減で変更が加わる可能性がある。
例:テトリスにおける「ブロックの落下速度」を保存している部分。ゲームバランスの観点から変更が加わる可能性がある。
ここに変更が入る場合は、依存性逆転法則を意識しないコードだった場合、Model全体の改修が必要となります。
安定度5:おおまかなゲームロジック
Modelにおける上位モジュールのことを指します。
例:1vs1格闘ゲームにおいて、「2体のキャラクターを選択する」というような処理。1vs1格闘ゲームである限りは変わらない。
例:テトリスにおける、「横にそろったら消える」処理、「テトリミノ一覧」というデータ。テトリスである限りは変わらない
ここに変更が入る場合、そもそもほとんど別ゲームになります。したがって、上位モジュールに依存するか否かにかかわらず、上位モジュールが変更される場合は下位モジュールも変更を余儀なくされます。したがって、このレベルの要素に依存することは全く悪いことではありません。
安定度4,5の違いについて
なお、安定度4,5の区切りはゲームの状況によって変わります。大雑把に言えば5が呼び出す側、4が呼び出される側のModelです。明確な判断基準はありません。また、5だと思って設計したものが仕様変更で4になることもしばしばあります。(ゲームは雪だるま式に拡大していくことが多いので4→5はあまり見かけません)
しかし、心配することはありません。なぜなら、数値やロジックの差し替えが必要そうだと分かった時点で、リファクタリングを実行し、依存性を逆転することで問題を解決できるからです。ViewやModelといった明確に分かれた存在での切り替えには手間がかかりますが、Model内部のみでのリファクタリングは比較的簡単です。
依存したときの安定度の変化
安定度4であるはずの「細かいゲームロジック」ですが、もしこれが「画面装飾」の変化によって影響を受けて変わってしまう場合、変更の頻度は安定度1相当になります。
逆に、細かいゲームロジックが変化した場合、画面に表示されるものは細かいゲームロジックによって決まるはずですから、ソフトウェア自体の依存性に関係なく、論理的に考えて画面は変更せざるを得ません。
この具体例から「安定度Xである要素Aが安定度Yである要素Bに依存している場合、XがYよりも大きい場合はAの安定度はYに下がる。XがYよりも小さい場合はAの安定度は変わらない」という一般論が導き出せます。
依存性を減らしたUnityでのコードの書き方
Unityにおける依存性設計の原則は「安定度の低い要素に対して安定度の高い要素を依存させない」ことにあります。
この原則を把握したうえで、現実的なコードに落とし込んでいきます。
安定度2の画面操作(出力)と安定度1である画面装飾をつなげる
画面操作(出力)ではアニメーションが相当すると書きました。
しかし、もしここのプログラムで「座標(1200,800)から(1000,800)へ1秒で移動」と言ったようなコードを書いてしまえば、それは実質的に安定度1のコードになります。なぜなら、デザインの変化でアニメーションも変更しないといけないからです。
では、どのように依存性を逆転させればよいのでしょうか。それは、アニメーションなどの画面操作(出力)部分で具体値を持たず、画面装飾からデータを提供させることです。
具体的には、「座標Aから座標Bに移動する」というコードを書いて、安定度1の部分で「座標Aは(1200,800)、座標Bは(1000,800)」という指定をさせます。
すると、画面操作のスクリプトは、(1000,800)などの画面装飾が担当するべきデータを保持しないことで保守性が上がります。
安定度4である細かいロジックと、安定度2,3である画面操作(出力・入力)をつなげる
この手法についてはPresenter、MVPの項目で詳しく解説していきます。
端的に言えば「Modelは完全に純粋にして誰にも依存させない」かつ「PresenterはがっつりModelを意識して依存する」という立場をとっています。この立場の根拠にあるのが安定依存原則です。
安定度5である大まかなゲームロジックと安定度4である細かなゲームロジックをつなげる
この問題を解決するにはソフトウェア工学の書籍・記事におけるインターフェースによる依存性逆転を利用します。
具体的な実装方法についてはほかの記事を参照してください。
4,5は純粋なModelで、Unityであるものことを意識しなくていい存在であるので、依存性逆転を適用しやすいです。
引用部分の詳しい説明
最初に挙げた引用部分
Viewに依存したロジックを書くこと自体が出来なくなり、見た目の変更がバグを誘発する状況を作りにくくできる
という部分について説明しようと思います。これを補足して書き換えれば
(適切に依存関係を管理したうえでModelとViewの分離をすると、ModelはViewへの依存が出来ないので、
Viewに依存したロジック( つまりModel )を書くことは禁止されるので、見た目の変更が( Model側の )バグを誘発する状況を作りにくくできる。( さらに、依存性逆転による柔軟性の確定によってViewの変更が容易になる )
というようになります。
MVP設計におけるPresenterの立ち位置
UnityにおけるMVP関連の記事では、なぜかPresenterがViewを参照、つまりは依存している場合が多いです。これでは、Viewの変更によってPresenterが影響されかねず、若干設計が難しくなるように感じています。
ここまでの議論を振り返れば、依存関係は原則に則って考えて「安定したものに依存」を徹底し、PresenterはViewを全く参照せず、逆にViewがPresenterを参照すべきです。
おわりに
依存性の純粋な理解はUnityだけではなく、ソフトウェア工学的にも大切です。
安定度の高さごとにソフトウェアをレイヤー分けする発想は多様な分野で使われています。オニオン型の図であったり、3階建てビルのレイヤー構造の図であったりで説明されることがありますが、根底にある考え方は同じです。
Unityでの安定度を考えていくことによって実感を持てるようになるとほかの概念も素早く理解できると思います。