fspace
ユーザー
ツクールのプラグインやスクリプトを眺めているとちょくちょくこんなコード片を見かけたりします。
これを見て「このコードちょっとヤバそうだな」と思える人はいいんですが、「何のこっちゃわからない」「カプセル化の問題かな?」という人も少なくないんじゃないかと思います。
コアスクリプトを含め、ある程度の規模のソフトウェアには、コードを整理するための設計意図とルールが存在します。この設計意図やルールのことを「(ソフトウェア)アーキテクチャ」と呼び、これに違反してしまうと挙動が壊れやすくなり、不具合や競合の大きな原因となります。
アーキテクチャについては開発者による説明文書が用意されるのが一般的ですが、ツクールでは提供されていません。そのため、実際の構造や命名から推測するしかなく、プラグイン開発で初めてプログラミングを学んだような人には読み取るのが困難というのが実情です。
そこで、初心者向けにコアスクリプトのアーキテクチャについて簡単に説明しようと思います。
……いつか書こう書こうと思いつつ、今更感のある時期になってしまってますが、多分次回作が出ても設計はそんなに変わらない……はず。
初心者向けには書きますが、説明できる範囲にも限界があるので、次のことが理解できている程度の読者を想定します。
もし上記がわからないようであれば入門書を一冊読むことをおすすめします。
アーキテクチャの話をする前に、設計の基礎として「依存」について少し説明します。
設計というのはざっくりと言えば、コードを意味のあるまとまりに分割して、そのまとまりの間の依存を整理する作業です。そして、アーキテクチャはその分割と依存に関する方針やルールの集合にあたります。
依存とは何か、依存によって何が引き起こされるのかを理解することは、設計を理解するための第一歩です。
プログラミングにおいて「依存」とは、何らかの対象が別の何らかの対象の存在を前提としている状態のことをいいます。
例えば、クラスAがそのメソッドaにおいて、クラスBのメソッドbを呼び出しているとします。このとき、「メソッドaはメソッドbに依存している」「クラスAはクラスBに依存している」といいます。対象はメソッドやクラスでなくてもよく、特定の役割を果たす複数クラスから成る集合や、クラスの一部の性質をまとめたインターフェース、あるいはもっと小さく、変数や値なんかの場合もあります。
なぜ依存なんてものを考えるかというと、プログラムを書き換えたとき、どこに影響が波及する可能性があるのかを把握したいからです。
例えば、クラスAがクラスBに依存しているとします。このとき、クラスBの挙動を変更すると、クラスAはクラスBの挙動を前提としているため、クラスAの挙動もまた変わってしまう可能性があります。その結果、クラスAは想定通りに動かなくなってしまうかもしれません。そのため、クラスBを変更する際には、クラスAの挙動にも注意しなければなりません。
一方、クラスBがクラスAに依存していなければ、クラスAの挙動を変更してもクラスBの挙動が壊れることはありません。そのため、クラスAの変更は、クラスBのことを気にせずに行うことができます。
プログラムの規模が大きくなってくると、一箇所変更する度に別の場所で問題が発生しないかどうか、ソースコード全体を確認するなんてことはやってられなくなります。そのため、ソースコードを細かなパーツに分解して、適切に依存関係を設定してやることで、確認が必要な部分を小さく保つ設計作業が必要不可欠となります。
依存はもちろん存在しない方が挙動を変更しやすいのですが、プログラムを書く以上、依存は絶対に発生します。何からも依存されていないコードというのは、プログラムの開始点(エントリーポイント)でもない限り、単に実行されないコード(デッドコード)です。そのため、性質の悪い依存を避けて、性質の良い依存でパーツ同士を組み合わせていくことが重要となります。
じゃあ依存の性質の良し悪しって何さ、というのが次の問題ですが、実はこれに明確な基準はありません。様々な形とその性質を知って、ケースバイケースで目的に合ったものを選択していく必要があります。
例えば、一般にあまり良くないと言われている形として循環依存があります。これは依存を辿っていくと開始点に戻ってくるような依存関係のことで、最小構成としてはクラスAがクラスBに依存して、かつクラスBがクラスAに依存しているような状態です。どちらのクラスを変更しても、もう片方のクラスが影響を受ける可能性があり、実質的にひとつの大きなクラスを管理しているのと同じような状態となってしまう場合があります。
といっても、限られた小さな範囲では循環依存もあまり問題にはなりませんし、循環依存であっても依存方法によって十分に制限がかかっていれば問題のないケースも多く、あくまでもひとつの指標に過ぎません。
ここまで依存依存と書いてきましたが、実際の設計では単なる依存と捉えるよりも、より具体的な関係を定義して整理することの方が多くなっています。
代表的なものにその依存が「参照」か「変更」かという観点があります(※1)。
参照とは対象のデータや状態の読み取りのみを行うことを指し、変更とは対象のデータや状態の書き込みも行うことを指します。例えば、変数の値を読んだり、何らかの値を計算するだけのメソッドを呼び出したりするのは参照です。一方、変数の値を書き換えたり、インスタンスフィールドの値が変化する可能性のあるメソッドを呼び出したりするのは変更です。
このように参照と変更を分けて扱うのは、変更と比較して参照は非常に扱いやすいという特徴があるためです。
まずは変更が含まれるケースについてみてみましょう。クラスA、クラスB、クラスCがそれぞれ存在し、クラスAがクラスCを変更、クラスBがクラスCを参照しているものとします。このとき、クラスAの変更処理とクラスBの参照処理ではその順序が重要となります。クラスAが変更した後にクラスBが参照するのと、クラスBが参照した後にクラスAが変更するのでは結果が変わってしまうためです。そのため、クラスAとクラスBの間に直接的な依存が存在しなくとも、プログラム全体としてはクラスAとクラスBの処理の順序を制御しなければならなくなります。これはクラスAとクラスBが両方クラスCを変更している場合でも同様です(むしろこの場合は順序がより重要となります)。
一方、クラスAとクラスBがともにクラスCを参照のみしている場合には、この順序を気にする必要がなくなります。クラスAが参照した後にクラスBが参照しても、クラスBが参照した後にクラスAが参照しても結果は変わらないからです。これによりクラスAとクラスBはほぼ独立に扱えるようになります。
また、より自明な性質として、参照は依存対象の状態を壊さないということがあります。プログラム中の何らかの状態が壊れるバグに遭遇した時、その原因として依存関係にある対象を疑うことになりますが、参照のみの関係にある対象はすぐに容疑者から外すことができます。
※1 ここでは「参照」と「変更」としていますが、あまり決まりきった呼び方はありません。「読み取り(read)」と「書き込み(write)」だったり、「クエリ(query)」と「コマンド(command)」だったり、参照のみに注目して「読み取り専用(readonly)」といったり、もう少し広い意味で「副作用がない」といったりします。
依存の方向、そして参照や変更を意識した代表的な例に、「ドメインロジック」と「プレゼンテーションロジック」の区別があります。
ドメインロジックとは、プログラムの解決したい問題そのものを扱う処理のことを指します。「ドメイン」というのは「問題領域」のことで、一般的にソフトウェアはビジネスの課題解決を目的とすることが多いので「ビジネスロジック」とも呼ばれます。ゲーム分野ではゲーム性に関する部分に当たるため「ゲームロジック」と呼ばれることもあります。例えば、キャラクターの体力を表現するHPを用意する、敵の攻撃を受けるとHPが減少する、といった処理はドメインロジックに当たります。
一方、プレゼンテーションロジックとは、ドメインロジックによる結果をユーザーに対して示す、あるいはユーザーの指示をドメインロジックに伝えるといった、ユーザーとドメインロジックの橋渡しをする処理のことを指します。ざっくりとUIを表示する処理のことと理解してもよいかもしれません。例えば、HPゲージを表示する、HPが減少したときにHPゲージの幅を変化させる、といった処理はプレゼンテーションロジックに当たります。知覚に関するものは基本的に含まれるため、効果音の再生なんかも同様です。
ドメインロジックとプレゼンテーションロジックを分離する設計をしたとき、重要となるのはプレゼンテーションロジックが一方的にドメインロジックに依存するように処理を組み立てることです。つまり、ドメインロジックがプレゼンテーションロジックに依存してはいけません。
このようにするのはドメインロジックと比較してプレゼンテーションロジックが非常に変更されやすいためです。問題そのものを扱う方法にはそれほど多くのバリエーションがありませんが、ユーザーの使い勝手には正解がないためいくらでも改善の余地があります。ユーザーへの見え方を調整する度に、問題の扱い方まで変えたくはないのです。ドメインロジックからプレゼンテーションロジックへの依存が存在しなければこれが実現できます。
また、ドメインロジックとプレゼンテーションロジックの間の参照と変更のフローは明確に分けて規定するのが一般的です。特に参照と変更で担当やタイミングを完全に分けてしまうことも多くなっています。
ドメインロジックの要素とプレゼンテーションロジックの要素は一対多で結びつくことがあり、その際にそれぞれの表示要素を独立に扱いたい、というのがその理由の一つです。例えば、ドメインロジックの管理するHPという要素に対して、プレゼンテーションロジックにはHPゲージや残HPの数字表示、瀕死のキャラクター画像など複数の要素が対応する可能性があります。このとき、ある表示要素がHPに関する情報を勝手に操作してしまうと、処理順序等によっては他の表示要素がその変化をうまく反映できないままゲーム画面が表示されてしまうかもしれません。これを防ぐには、変更と参照のタイミングを明確に分けるか、変更の度に必ず再参照する仕組みが必要で、いずれにしても各表示要素が情報源の変更を伴わずに表示更新できなければなりません。
JavaScript:
SceneManager._scene._spriteset
これを見て「このコードちょっとヤバそうだな」と思える人はいいんですが、「何のこっちゃわからない」「カプセル化の問題かな?」という人も少なくないんじゃないかと思います。
コアスクリプトを含め、ある程度の規模のソフトウェアには、コードを整理するための設計意図とルールが存在します。この設計意図やルールのことを「(ソフトウェア)アーキテクチャ」と呼び、これに違反してしまうと挙動が壊れやすくなり、不具合や競合の大きな原因となります。
アーキテクチャについては開発者による説明文書が用意されるのが一般的ですが、ツクールでは提供されていません。そのため、実際の構造や命名から推測するしかなく、プラグイン開発で初めてプログラミングを学んだような人には読み取るのが困難というのが実情です。
そこで、初心者向けにコアスクリプトのアーキテクチャについて簡単に説明しようと思います。
……いつか書こう書こうと思いつつ、今更感のある時期になってしまってますが、多分次回作が出ても設計はそんなに変わらない……はず。
1. 対象読者
初心者向けには書きますが、説明できる範囲にも限界があるので、次のことが理解できている程度の読者を想定します。
- JavaScriptの基本文法
- 変数や関数、オブジェクトやプロパティなどの基本概念
- if文、for文、while文などの制御構文
- 各種データ型や演算子等
- その他入門書レベルのすべての内容
- オブジェクト指向プログラミングの基礎
- 継承、ポリモーフィズム(多相性、サブタイピング)、カプセル化の概要
- クラス、フィールド、メソッド、コンストラクタ、インスタンスなどの基礎用語
- プロトタイプベースの継承の仕組み(プロトタイプチェーン)
- その他コアスクリプトのオブジェクト指向的構造を理解できるだけの知識
もし上記がわからないようであれば入門書を一冊読むことをおすすめします。
2. 準備
アーキテクチャの話をする前に、設計の基礎として「依存」について少し説明します。
設計というのはざっくりと言えば、コードを意味のあるまとまりに分割して、そのまとまりの間の依存を整理する作業です。そして、アーキテクチャはその分割と依存に関する方針やルールの集合にあたります。
依存とは何か、依存によって何が引き起こされるのかを理解することは、設計を理解するための第一歩です。
2.1. 「依存」とは
プログラミングにおいて「依存」とは、何らかの対象が別の何らかの対象の存在を前提としている状態のことをいいます。
例えば、クラスAがそのメソッドaにおいて、クラスBのメソッドbを呼び出しているとします。このとき、「メソッドaはメソッドbに依存している」「クラスAはクラスBに依存している」といいます。対象はメソッドやクラスでなくてもよく、特定の役割を果たす複数クラスから成る集合や、クラスの一部の性質をまとめたインターフェース、あるいはもっと小さく、変数や値なんかの場合もあります。
なぜ依存なんてものを考えるかというと、プログラムを書き換えたとき、どこに影響が波及する可能性があるのかを把握したいからです。
例えば、クラスAがクラスBに依存しているとします。このとき、クラスBの挙動を変更すると、クラスAはクラスBの挙動を前提としているため、クラスAの挙動もまた変わってしまう可能性があります。その結果、クラスAは想定通りに動かなくなってしまうかもしれません。そのため、クラスBを変更する際には、クラスAの挙動にも注意しなければなりません。
一方、クラスBがクラスAに依存していなければ、クラスAの挙動を変更してもクラスBの挙動が壊れることはありません。そのため、クラスAの変更は、クラスBのことを気にせずに行うことができます。
プログラムの規模が大きくなってくると、一箇所変更する度に別の場所で問題が発生しないかどうか、ソースコード全体を確認するなんてことはやってられなくなります。そのため、ソースコードを細かなパーツに分解して、適切に依存関係を設定してやることで、確認が必要な部分を小さく保つ設計作業が必要不可欠となります。
2.2. 良い依存と良くない依存
依存はもちろん存在しない方が挙動を変更しやすいのですが、プログラムを書く以上、依存は絶対に発生します。何からも依存されていないコードというのは、プログラムの開始点(エントリーポイント)でもない限り、単に実行されないコード(デッドコード)です。そのため、性質の悪い依存を避けて、性質の良い依存でパーツ同士を組み合わせていくことが重要となります。
じゃあ依存の性質の良し悪しって何さ、というのが次の問題ですが、実はこれに明確な基準はありません。様々な形とその性質を知って、ケースバイケースで目的に合ったものを選択していく必要があります。
例えば、一般にあまり良くないと言われている形として循環依存があります。これは依存を辿っていくと開始点に戻ってくるような依存関係のことで、最小構成としてはクラスAがクラスBに依存して、かつクラスBがクラスAに依存しているような状態です。どちらのクラスを変更しても、もう片方のクラスが影響を受ける可能性があり、実質的にひとつの大きなクラスを管理しているのと同じような状態となってしまう場合があります。
といっても、限られた小さな範囲では循環依存もあまり問題にはなりませんし、循環依存であっても依存方法によって十分に制限がかかっていれば問題のないケースも多く、あくまでもひとつの指標に過ぎません。
2.3. 参照と変更
ここまで依存依存と書いてきましたが、実際の設計では単なる依存と捉えるよりも、より具体的な関係を定義して整理することの方が多くなっています。
代表的なものにその依存が「参照」か「変更」かという観点があります(※1)。
参照とは対象のデータや状態の読み取りのみを行うことを指し、変更とは対象のデータや状態の書き込みも行うことを指します。例えば、変数の値を読んだり、何らかの値を計算するだけのメソッドを呼び出したりするのは参照です。一方、変数の値を書き換えたり、インスタンスフィールドの値が変化する可能性のあるメソッドを呼び出したりするのは変更です。
このように参照と変更を分けて扱うのは、変更と比較して参照は非常に扱いやすいという特徴があるためです。
まずは変更が含まれるケースについてみてみましょう。クラスA、クラスB、クラスCがそれぞれ存在し、クラスAがクラスCを変更、クラスBがクラスCを参照しているものとします。このとき、クラスAの変更処理とクラスBの参照処理ではその順序が重要となります。クラスAが変更した後にクラスBが参照するのと、クラスBが参照した後にクラスAが変更するのでは結果が変わってしまうためです。そのため、クラスAとクラスBの間に直接的な依存が存在しなくとも、プログラム全体としてはクラスAとクラスBの処理の順序を制御しなければならなくなります。これはクラスAとクラスBが両方クラスCを変更している場合でも同様です(むしろこの場合は順序がより重要となります)。
一方、クラスAとクラスBがともにクラスCを参照のみしている場合には、この順序を気にする必要がなくなります。クラスAが参照した後にクラスBが参照しても、クラスBが参照した後にクラスAが参照しても結果は変わらないからです。これによりクラスAとクラスBはほぼ独立に扱えるようになります。
また、より自明な性質として、参照は依存対象の状態を壊さないということがあります。プログラム中の何らかの状態が壊れるバグに遭遇した時、その原因として依存関係にある対象を疑うことになりますが、参照のみの関係にある対象はすぐに容疑者から外すことができます。
※1 ここでは「参照」と「変更」としていますが、あまり決まりきった呼び方はありません。「読み取り(read)」と「書き込み(write)」だったり、「クエリ(query)」と「コマンド(command)」だったり、参照のみに注目して「読み取り専用(readonly)」といったり、もう少し広い意味で「副作用がない」といったりします。
2.4. ドメインロジックとプレゼンテーションロジック
依存の方向、そして参照や変更を意識した代表的な例に、「ドメインロジック」と「プレゼンテーションロジック」の区別があります。
ドメインロジックとは、プログラムの解決したい問題そのものを扱う処理のことを指します。「ドメイン」というのは「問題領域」のことで、一般的にソフトウェアはビジネスの課題解決を目的とすることが多いので「ビジネスロジック」とも呼ばれます。ゲーム分野ではゲーム性に関する部分に当たるため「ゲームロジック」と呼ばれることもあります。例えば、キャラクターの体力を表現するHPを用意する、敵の攻撃を受けるとHPが減少する、といった処理はドメインロジックに当たります。
一方、プレゼンテーションロジックとは、ドメインロジックによる結果をユーザーに対して示す、あるいはユーザーの指示をドメインロジックに伝えるといった、ユーザーとドメインロジックの橋渡しをする処理のことを指します。ざっくりとUIを表示する処理のことと理解してもよいかもしれません。例えば、HPゲージを表示する、HPが減少したときにHPゲージの幅を変化させる、といった処理はプレゼンテーションロジックに当たります。知覚に関するものは基本的に含まれるため、効果音の再生なんかも同様です。
ドメインロジックとプレゼンテーションロジックを分離する設計をしたとき、重要となるのはプレゼンテーションロジックが一方的にドメインロジックに依存するように処理を組み立てることです。つまり、ドメインロジックがプレゼンテーションロジックに依存してはいけません。
このようにするのはドメインロジックと比較してプレゼンテーションロジックが非常に変更されやすいためです。問題そのものを扱う方法にはそれほど多くのバリエーションがありませんが、ユーザーの使い勝手には正解がないためいくらでも改善の余地があります。ユーザーへの見え方を調整する度に、問題の扱い方まで変えたくはないのです。ドメインロジックからプレゼンテーションロジックへの依存が存在しなければこれが実現できます。
また、ドメインロジックとプレゼンテーションロジックの間の参照と変更のフローは明確に分けて規定するのが一般的です。特に参照と変更で担当やタイミングを完全に分けてしまうことも多くなっています。
ドメインロジックの要素とプレゼンテーションロジックの要素は一対多で結びつくことがあり、その際にそれぞれの表示要素を独立に扱いたい、というのがその理由の一つです。例えば、ドメインロジックの管理するHPという要素に対して、プレゼンテーションロジックにはHPゲージや残HPの数字表示、瀕死のキャラクター画像など複数の要素が対応する可能性があります。このとき、ある表示要素がHPに関する情報を勝手に操作してしまうと、処理順序等によっては他の表示要素がその変化をうまく反映できないままゲーム画面が表示されてしまうかもしれません。これを防ぐには、変更と参照のタイミングを明確に分けるか、変更の度に必ず再参照する仕組みが必要で、いずれにしても各表示要素が情報源の変更を伴わずに表示更新できなければなりません。