対外向けの前置き
東京大学工学部システム創成学科システムデザイン&マネジメントコースでは、五月祭と呼ばれる学祭で、学科の学域にちなんだ出展を行います。
コロナ前はただ教員からもらったスライドをずっと解説しているだけの無気力企画だったそうですが、 コロナ後最初の代(僕)が張り切ってゲームなどを出したせいで、何かプログラミングして実演して見せようというハードルができたようです。
この責任を取るために、 「システムデザイン&マネジメントコース」と名乗りながらどうしてかソフトウェア設計・工程・マネジメントに関してはノータッチのカリキュラムなので ちゃんとプログラミング初心者2回生でも安定した集団開発ができるように手引きするのがこの目的です。
当然、自己顕示欲の王、自己顕示欲キングなので、対外的にも公開します。
ぜひ、学生や初心者で集団開発される方は参考にされてみてください。
進め方考察
プログラミングを軸とするプロジェクト進行では、業界では
- ウォーターフォール開発
- アジャイル開発
が主流になっている
ウォーターフォール
IT業界の歩き方より引用
単純に、 プロジェクト全体を、 設計→実装→テスト を前から順番にやる発想。
基本的に突発的な拡張がない、短期集中でやる場合(五月祭がそう)はこちらが基本だと思われる。
一般的には「絶対に前工程に戻ってはいけない」と言われるが、 実際のところ、戻ったり、見直しが要求される場面は多い。
しかし、ウォーターフォールをする場合、次のことに気をつけねばならない:
- 要件定義を綿密に行わないと 、後戻りが発生、最悪 作り直し になる
- 特にゲーム開発は仕様変更・調整がつきものだけど、それでも次には心がける:
- 後戻りしなくても良い 緻密な設計
- 修正が容易な 拡張性のある実装
アジャイル開発
Cloud Naviから引用
機能単位で、 設計→実装→テストをするという発想。
業界では次々に要求が振ってくる場合を想定したスタイルなので、五月祭には不向きかも。
しかし、ゼロスタートではなく、(教員・先輩のプロジェクト)など既存のものを 拡張 する場合は妥当性がある。
(機能単位に付け足していけば、例え全て終わらなくとも五月祭に間に合う)
この場合、機能を追加するベースのプロジェクトが必要となるが、 ベースが拡張性に優れた設計 であることが必要条件。
さもなければ、機能を追加する度に作り直しになる。
要件定義
何をするのか 概要と挙動を 決める。
例えばゲームの場合なら、
- どのようなシーケンス(ステージ選択、インゲーム部分、リザルト画面など)があって、
- それがどのような画面構成か(実際に図を描く)
- 各操作・オブジェクト・システムがどのような挙動を想定するか
を洗い出す。
概要レベルなのでここで、例えばポイントを付与する計算式を立案するのはまだしなくていい。
業界レベルだとこんなテンプレートもあるが、学生の集団開発程度でここまで切り詰めなくともいいかも。
しかし、図も含めて(手書きok)ドキュメント化するべきなのは確か。
基本設計
一番重要。うまく分担できるか、拡張性を持てるかはここでだいたい決まる。
ここで説明する内容は業界内のキッチリとした「基本設計」と異なるかもしれません。
要件定義で見えたグランドイメージを 構成する
キーフレーズは
SOLID則に従おう
プログラム構成の種別
大別すると、
-
手続き型
全て続けて書く -
オブジェクト指向
対象概念ごとに、クラスあるいはクラスに準ずるものを作る。その概念にまつわる処理(攻撃、移動、死亡時挙動など)は全てそのクラスに内包する
対象概念の切り分け方: 交通ゲームの場合- オブジェクトとして存在するもの
車、交差点、信号機、道路、スコアUIなど - 実在してこそはしないが、動作上いてほしい・いるべき運営者概念
スコアを管理するScoreManager
、時間切れを検知し、リザルト画面へ遷移させるSequenceManager
、新しい車をスポーンさせるSpownManager
など
- オブジェクトとして存在するもの
-
関数指向
動作単位 でプログラムを切り分ける。動作の切り分け方: 交通ゲームの場合
「新しい車をスポーンさせる関数」→「車を移動させる関数」→「車の目的地到着を検知する関数」→...オブジェクトの動作に関する処理はクラスではなく各関数に記述される
「オブジェクト指向型言語」などと言ったりしますが、どの言語でも基本的にその気になればどのスタンスでも取ることができます
どの構成法を採用するか
まず、手続き型だけはやめておいた方がいい。
なぜなら、プログラムに「切り分け」があれば、その切り分け単位 で分担できる(後述)から。
関数型かオブジェクト指向か? やりやすい方でいい。
ただし、 切り分けの粒度が小さいほど分担がやりやすい
なお、Unity
などの「エンジン」に依拠する場合は、そのエンジンの仕様により確定する。
ちなみにUnity
はオブジェクト指向。(正しくはコンポーネント指向)
切り分け方
粒度が適度に細かいほどいい。
例えばBlender
を使った物理シミュレーションであれば
- 物理シミュレーションを抽象的に計算する
Physics
モジュール - シミュレーション内のオブジェクトを
Blender
に橋渡しして描画させるRender
モジュール
で切り分けると、
Blender
仕様の把握を要求されるのがRender
モジュール だけ で済む。
逆に Physics
モジュールはBlender
仕様を 知らなくとも 実装できる。
(最悪、急にBlender
が使えなくなっても、 Render
モジュールを書き換えるだけで 仕様変更に耐えうる→拡張性!!)
「切り分け」についてはこちらも参照: 初心者だらけでもとりあえず共同開発ができる手引き
重要なことは
責務をキッパリ分けることが重要
例えば「クラスGod
には当たり判定処理もUI処理も物理演算処理も記述されている状態」は、問題。
これは神クラスといい、多くのプログラマーを葬送してきた。
責務がたくさんあるため
- 神クラスへの外部からの依存が多くなり、
- 責務がちゃんと分かれていないため目的の処理がどこにあるのか見つからず、
- クラスの中身も責務の処理同士がぐちゃぐちゃに連結していたら修正しにくい。
もし人間の内臓が、脳から大腸まで一つの内臓「神臓」になっていたときの不都合を思い浮かべていただきたい。そういうことである。
「責務」をキッパリ分け、「この処理はこのクラス/モジュール/関数にまとめる、一元化する」「逆にこのクラス/モジュール/関数はこの責務だけ」という設計を心がけることでみんな幸せになれる
もう一回、 SOLID則に従おう
図にする
図にすると見やすい。
関数型のスタンスを取り、切り分けにフローがある場合はアクティビティ図、
が適している。
作図はPlantUMLを使えばテンプレート通り書いているだけで素早く綺麗な図を出せるが、
どうしても文法が理解できない場合や当意即妙の図を出せない場合などはdraw.ioで事足りる。(ただしマウス操作がめんどくさい)
図を描く意図として
- 順序関係・依存関係がわかる
- 誰がどのような責務を持つかすぐ分かる
というメリットがある。
詳細設計
業界レベルでは実際にフローチャートを描いて実際のプログラムを図示するが、そこまではやらなくてもいいと思う。(実装しながら考えれば)
隠蔽
ただし、オブジェクト、あるいはモジュール、あるいは関数がしっかり カプセル化 できるように、 隠蔽 できるようにイメージすることは重要である。
お馴染みの例で例えると、
我々は自動車の内部構造(パイプの配線など)を知らなくとも、ペダルとハンドルという インターフェースを知るだけで、車そのものを扱える
我々はTCP/IP通信を理解してしていなくとも、ブラウザを開けばWebページをアクセスできる。
このように、クラスAがクラスBを使う時、クラスAはクラスBの内部詳細を知らなくとも、 クラスBが 提供するインターフェース(public
メソッド)だけで扱える ように両者が設計する必要がある。
クラスAは、クラスBに踏み込みすぎず、
クラスBは、自分の内部処理を隠し (private
)、public
なインターフェース(メソッド)で 外見的に完結させる ことが肝要になる。
クラスAは、クラスBのことは メソッドしか 知らず、
クラスBは、クラスAのことを 全く意識せずに
実装するように。
隠蔽のメリット
- クラス/モジュール/関数が 密結合でなくなる
よからぬ挙動を見つけ手術するとき、ピンポイントで、他の挙動を壊さずに修正できる - 分担がスムーズ
きっぱり責務が分かれていると、 他人の実装を気にせずに 、その人のメソッドだけ書いて実装できる
クラス/モジュールが連絡しあうには関数の引数・戻り値を使う
これまでクラスないしモジュールをキッパリ分けると熱弁したが、もちろんデータを連絡せねばならない
例えばPhysics
モジュール内のオブジェクトの座標をRender
モジュールに送らないと、Render
モジュールはオブジェクトの状況を知らないので描画できない。
当然だけど、連絡には 関数呼び出し を使う。
送信するデータを引数、要求するデータを戻り値とすることで解決する。
受信データ = function(送信データ)
もちろん、データの送信のみならず、マルチエージェントシミュレーションの文脈でいうところの「相互作用」(たとえばボールが2つぶつかって跳ね返る)も 関数で 発火させる。
連絡フローが煩雑な場合; シーケンス図
単純な連絡網を目指すべきなのは間違いないが、ときには連絡網が複雑で追いにくい場合がある。
そのときは シーケンス図 を描くことが推奨される。
例えば、これは「交差点オブジェクトが動的に繋がった道路を認識し、それに応じて信号機を生成する」という処理。
もしこの図を書かずに何も知らない人へ修正を投げたら、さすがにどれだけ各個のコードを丁寧に書いても、この流れを追うのに数時間はかかる。
書いた自分もこの図なしではさすがにコード理解が厳しい。
さすがに「ボール2つぶつかったら跳ね返るだけ」のような連絡は省略しても良いと思うが(業務レベルではこれも要求する職場もある)、少しでも複雑だ(例えば 2往復以上 、あるいは 3クラス間の連絡が往来する 、など)と察知した場合は、迷わずシーケンス図を書かれたし。
これもPlantUMLだとすぐ作図できるけど、draw.ioでも一応可(めんどくさい)
時間差でのデータ受け渡し(コールバック関数)
これは設計というよりも実装寄りのお話だけど、せっかくなので。
直接の 関数による受け渡しは、直列繋ぎで往来する。
すなわち、「相手の処理の完了を待つまで自分の処理を完全停止」させる。
もしその処理が1フレーム完結のすぐ終わる処理なら気にすることはないけど、もし演出などで1秒待たせる処理の場合、機能停止する。
この場合、 受信用の関数 そのものを引き渡し(デリゲートという)、「そっち完了したらその関数発火してね」という方針に持ち込む。
これはコールバックと呼ばれる。
class A:
def a(self):
#デリゲート引き渡し
func(self.d)
def d(self):
print("呼ばれた!")
def func(deli):
#コールバック
deli()
実行
A().a()
実装
メインディッシュ。
弊記事初心者だらけでもとりあえず共同開発ができる手引きも参照。
GitHubを使う。
弊記事の通り。
GitHubを使わずにGoogle Driveを使おうとするならその日が命日。
注意として、
- 自分の作業は ブランチを切ること、
- 正常動作を確認したコードのみを
main
ブランチに提出すること -
main
ブランチに提出する場合は周りの確認を得ること
GitHub
の プルリクエスト機能 を使えば、「誰が何しようとしているのか」把握できるので便利。
コードレビューをするかは自由。業界人はみんなやってる。
VSCodeを使おう
システム創成学科ではなぜかEclipse
を使わされるが、Visual Studio Code
が強く推奨。
あえてVSCodeを使わない理由があるならそちらでもいいけど、VSCodeを知らないだけならVSCodeを使った方がいい
Visual Studio
とVisual Studio Code
は異なるので注意
- 予測変換
- GitHub Copilotが使える
- 拡張機能が豊富すぎる
- デバッガも使える
GitHub Copilotを使おう
LLMによる思考盗聴で開発速度が55%向上する。 [参考]
概要: VSCode ではじめる GitHub Copilot 活用術
学割で 無料で 使える。
学割を使う導入方法: 【GitHub】学生申請をして無料でGitHub Copilotを使う
東大の学生証の場合は、特に学生証の英訳を書かなくとも申請は通った。
コードを綺麗に書こう
クラス / モジュール単位で担当を分けていると、あまり他人の担当箇所を読みに行く機会はないかもしれないが、あるかもしれない。
授業以外でプログラミングをしたことのない大学生のコードはだいたい終わっているので、「綺麗に書け」という言葉を具体化する。
コメントを書く
一番先に思いつく点。
コメントは書かないと怒られることはたくさんあるが、書いて怒られることはあまりない(あるときはある)
「ほんとうはAしたかったけど、可読性が悪くなるからこの処理にした」のようなコメントでも、歓迎される。
改善の余地がある箇所 に関しては TODO
というコメントを書くこと。忘れないうちに。
後で「TODO」と全体検索をかけてしらみつぶしにする。
名で体を表す
可読性では 一番重要
変数名、関数名は「何者か」を丁寧に表すこと
ちょっと名前ぐらい長くなっても、VSCodeの予測変換で困ることはない。
一番最悪なのは、変数名をa
とかx
とか一文字にするもの。
おそらくそれは一時間後には自分も読めなくなる。
ここらへんに関しては、こちらを参照: リーダブルコードの要点整理と活用法をまとめた
関数名 に関しては、実装上も 名で体を表す ことを意識する。
例えば、 car.Drive()
メソッドに「実は銃撃の処理もあります」と言われたら、使用者は発狂する。
ちゃんと関数名が関数内の処理内容を実質的に表すように、命名し、実装することが肝要。
命名規則
業界ではプロジェクトごとにコーディング規約と呼ばれるルールで、プログラムの記法を画一化する文化がある。
おそらく読者諸君はif
文の書き方が
if (x < 0)
{
x = 0
}
でも
if (x < 0) {
x = 0
}
でもどうでもいい、興味ないと思うであろうし、正直気にする必要はない。
しかし、ただ一つ、「 命名規則 」だけは決めることを推奨する。
単純に、変数名・関数名・クラス名などを
UpperCamelCase
lowerCamelCase
snake_case
のいずれで記述するかは画一化しておいた方がいい。
参考: プログラミング ~命名規則~
例えば、
-
C#
では関数・クラス名はUpperCamelCase
, 変数名はlowerCameCase
-
Python
では関数名も変数名もsnake_case
、クラス名はUpperCamelCase
など、言語ごとのスタンダードが存在している。
どれでもいいが、 プロジェクト内で一つに統一する ことを強く推奨。
もしバラバラだと、変数・関数にアクセスする度にどの記法だったかいちいち確認する必要が生じるから。
フォーマッタは便利
-
Python
ならBlack
-
JavaScript
ならPrettier
(VSCode拡張機能) -
C#
ならCShapier
(VSCode拡張機能)
など、言語ごとにフォーマッタが存在する。
テスト
ビジネス的なシステムであれば、基本的に テストコードを書くが、ゲームやシミュレーションのようなステートがNP困難なシステムは、テストコード記述が難しい。 (参考:ゲームのデバッグをAI自動化したCyGames)
単純に、 実際に動かして、エラーが出たら原因箇所を掘り出す のが初心者向け。
原因特定の手法として、
- コンソールデバッグ
print()
などコンソール出力関数をスクリプトに埋め込み、変数の中身などを表示させる。原始的。 -
デバッガの使用
ブレークポイント を貼った場所でプログラムが止まり、その瞬間における各変数の中身全てを閲覧することができる。
参考: Unity2019とVSCodeでブレークポイントを使ってデバッグ出来るようにする手順
デバッガを使う方が詳細も見れて効率がいいが、言語によっては設定が楽だったりメンドウだったりするので、各自調べること。
公開までに
-
操作説明を用意する
チュートリアルがあれば一番いいけど、チュートリアルがこの世で一番実装がメンドウ -
実機で動作確認
ぶっつけ本番だけはやめるように。動くかどうか以前にも、例えば画面サイズが合わないなど動作確認でしか気づけない点もある。
最後に
いいね頂けると泣きながら喜びます><