Updated at: 2013-12-16
プログラミングについて考える
美しいコードとは
- 美しいといっても様々な評価軸がある
- 処理効率や芸術性などもあるが、ここでは「人が読みやすい」(Readable)にフォーカスする
リーダブルという観点
- リーダブル・コードとは何か
- 処理の流れが人にとって理解しやすい
- 素直で想像しやすいシーケンスで書かれている
- 一つのまとまりが一つの仕事をしている
- ネストが少ない(頭の中でスタックしなくてよい)
- 命名が適切で具体的
- 無駄がなく、どこを読めばいいか分かりやすい
- 重複がない
- 書き方に一貫性がある
- 処理の流れが人にとって理解しやすい
ToDo: あとで書籍「リーダブル・コード」をまとめよう
それ以外のソフトウェア品質の観点
- 機能が充実・完成している
- CPU 消費が少ない(処理負荷が低い)
- 計算時間が短い
- メモリ消費が少ない
- スケーラビリティがある
- 同時に大量の処理をさばける
- ディスク消費が少ない
- 移植しやすい
- 保守しやすい
- 問題が発生したときにどこをどう直せばいいか分かりやすい
- 拡張性がある
- 処理の組み替えや、つけ外しが容易なコードになっている
- テストしやすい
- 参照透過性が保たれている(入力に対して一意に出力が決まる)
- 脆弱性が無い(セキュリティ対策がされている)
- ユーザビリティがある(ライブラリなどの場合、利用者にとって使いやすい)
コーディングの作法
- スコープはできるだけ短く
- ネストはできるだけ浅く
- ガード節
- 直交性
- 参照透過性
- やっている処理のレベルを揃える
- コメントは How ではなく What を書く
- コメントは専有面積に対して情報量が多くあるべき
複雑なシーケンスを書くとき
- 特にゲームではよくある
- 後から見る人が、全体の流れを追いやすいようにしておきたい
- いくつも関数ジャンプしたり分岐したりすると読むの大変 (mental stack 激しい)
- 1 カ所に「ステートの遷移」だけがまとまっててほしい
- シーケンシャルなタスクがあるなら、そのタスクを 1 カ所に並べるような書き方をしたい
- 非同期の場合はちょっと工夫がいる
- いずれにせよ、システムにタスクを登録する形で書きたい
- 複雑な状態遷移(AI とか、ゲームの流れ制御とか)を整理するとき、ツリーみたいなダイアグラムを書くでしょう
- それをそのままコードから見てとれるようにあってほしい
- 階層形ステートマシンを作って、状態遷移の定義が 1 ヶ所で俯瞰できるようにする
- 状態遷移の定義が外部データにもできる状態だとよい
- (イベントを文字列にした json などを用意するとか)
- 外部データにできる形になっていればそれを生成する GUI のツールを用意できる
- ステートマシンで処理制御するのはコールバックチェーンの置き換えにも使える
- いわゆる Differed 的な書き方ができるから読みやすくなる
- やることが最初に決まらない場合や、途中で割り込みとかかけたくなるような場合は 優先度付きキュー を使う
- コードが少し複雑になる(何が起こるか一望しにくくなる)ので使うときは慎重に
コンポーネント指向
- 複雑で、色々組み替えたいようなものに対してはコンポーネント指向がうまくいく
- それぞれ干渉しないような部品を足し合わせて機能を実現するイメージ
- 特に色んな要素が変化しやすいゲーム開発においては有効
- ツールのプラグインとかもこれに該当
- ゲームのルールや、キャラクターの動きの特性などもそれぞれコンポーネントにすると吉
- ルールのような無形のものもクラスにする
- 名詞だけでなく動詞をクラス化する
- プログラミング初心者はここが思いつきにくい
メッセージングの pros/cons
メッセージング = Observer パターン、 メッセージ = イベントと読み替えてもらっても問題ない
特性と向き不向き
- あるイベントに対して多くのものが反応するような場合はメッセージングが向いてる
- メッセージ(動的なワンクッション)をかましていないと、偉いクラスが全てのコンポーネントに命令しなきゃいけない
- メッセージングだと、影響力ある人はメッセージを場に投げるだけでよい。 あとは反応したい人たちが勝手に反応する
- メッセージを投げる側は受け取る側のことを気にしなくてよいし、 受け取る側も興味があるのはメッセージだけ
よい
- メッセージという動的なもの(文字列とか)を一段階かますことで、各コンポーネントの静的な依存度が減る
- 足し引きをするときの実装コストが低い
- データ駆動にしやすい
- メッセージを投げる側は後々受け取る側のことを気にしなくてよいので、機能追加が気楽
- 開発では「メッセージを受け取る側の種類」が後から増えていくことはよくある
- 受け取るコンポーネントを足せばよく、投げる側が肥大化せずに済む
- メッセージを受け取る側は発信者を知らなくてよいので、発信者をすげ替えることが容易
- 「キーボードを押したら、画面のこのボタンを押したのと同じことにする」みたいな処理を実現しやすい
- キーボードを押したときに、ボタン押したときと同じメッセージを投げればよい
悪い
- デバッグは比較的しづらい
- 複雑度が上がってくると、誰が何を投げて何を受け取ってるかが追いづらくなる
- 柔軟性を得るために静的型付けによる安全性を捨てているので、IDE の機能で呼び出し元を辿る、みたいなことはできない
- 辿るときはメッセージの定数名などで grep かける感じになるね
抽象化の恩恵の例
- 例えば RPG だとパーティのキャラの装備つけかえたりとか、Model 操作する処理が色々ある
- 後々特殊なイベントとかマルチプレイモードとか入ると、それ用のパーティとか作りたくなる
- でも装備つけかえとかの処理とかは同じようなもの
- その処理レイヤーが「どのパーティか」を気にせず処理できるようになってれば流用しやすい
過去の自分へ
設計実装
- 設計実装で大事なのは
- 足し引き(付け外し)のしやすさ
- 他の場所でも使い回せるか
- データとシステムがきちんと分離されているか
- 概念のレイヤーを揃えよ
- 1 つの関心ごとを 1 つにまとめよ
たとえば
- プログラムを分割していくとき、「これ単にファイル分けてるだけなんじゃ?」と思うことがあっただろう
- static な関数群のモジュールなんかだと実際そんな感じになるが
- オブジェクト指向でクラスを分けるときの明確な指標は、「これのインスタンスを複数作って、複数の場所で使い回せるか?」
- (使い回して重複を避けることができるか?)
- クラス内で使うアルゴリズムを別クラスに切り出すときも、「切り出したこれを、別のものにもちゃんと差し替えられるか?」 みたいなことを意識するといい(Strategy パターン)
- 何か仕組みを作るとき、「こんな時はどうするの?」にどれだけ対応しておけるかはプログラマの力量
- そしてこういうところに経験の差が出る
- しかし全部は想定できないし、全部に対応しておく時間もないし、 対応したとしてもコードが無駄に複雑に大きくなってしまうよね
- できるプログラマは「これで大抵は事足りる」部分までを作って、 足りなくなりそうなところは「その場合はここを拡張・カスタムしてよ」といった形で対処する
- ちゃんとカスタムしやすい設計にしておけるかが大事
- 新しいコンテンツを足すときに、「新たにサブクラスのファイルを作って追加する」形になっているとうまくいきやすい
- うまくいくとは何か?
- 読むのに見通しがよく、書くのが心地よいということ
- 足し引きがしやすい
- ファイル単位で分かれているとか、関数を小さく切るとか、よく言うけど何が心地よいのか?
- スコープが小さく保たれるということ
- ローカルな処理を自分の中だけに閉じておける、周りのことを気にしなくてよい
- 名前とかぶつかるの気にしなくていい
- 内部実装変えたいときも影響が少ない
- レイヤーをちゃんと区切って設計するのも似たような話
- 「使う側はそこから先は知らなくていい」ってのが大事
- 使う側も楽だし、使われる側も内部ロジックを修正しやすい
- 同じインタフェースで状況に応じてすげ替え、みたいなのもやりやすいしね
- クラス変数に色々保持してメソッドまたいで使っちゃうのは
「クラスレベルだけどグローバル変数」使ってるようなものだからね
- 何にしても関数は必要な入力を明確にして(引数にとる)、 出力を明確にする(副作用を持たない)ようにした方がうまくいきやすい
- うまくいくとは何か?
- 変な事故が起こらない
- 単体でテストできる
- 関数型言語でよく見かける参照透過性ってやつだよ
- とはいっても、毎回全部渡すの面倒じゃない? 効率悪いときもあるよね?
- Yes. 完全性と処理効率・実装効率などはトレードオフになることがある
- 「引数毎回とるの面倒」なケースの落としどころとしておすすめは、 「引数指定が無ければクラス変数などに保持してあるものを使う」
- 使うの楽だしテスト可能な状態にもなる
小さいスコープに区切る意味
- 一連の処理シーケンスで関数が長くなるような時でも、ひとつひとつのタスクを関数に切る方がいい
- 「何をやっているか」が並んでいるレイヤーと、その個々の具体的な実装が分かれている方が見通しがよい
- コメントを書きたくなったら、それを関数化するチャンス
- 関数はできるだけ「何かを受け取って、副作用なく何かを返す」形にすると安全(参照透過性)
- 関数を小分けする(= スコープを小さく保つ)ことは、「そのスコープ内で生まれたものは、外には影響がない」 ことを言語レベルで保証しているということ。ここに意味がある
- 関数内が長いと、「上の方の変数が下の方でも参照されているかもしれない」ということが実装を読むまでわからない
命名
- 命名はプログラマの仕事の大半を占める
- public な関数を作り、引数の型を指定するたびに、少し引き返しにくい決断をしていることになる
- 他の人に使われうるものならなおさら重い決断になる
- だから命名センス、インタフェースを切るセンスは重要
- misc みたいなパッケージ名とかは甘え
- tmp とか work とかもつけるときはよく考えよう(大抵は使わなくていいケースが多い)
- 具体的な名前がつけられるのならば、つけた方がいい
- 人は楽な方に流れてしまうからね
インタフェースとデフォルト実装
- インタフェース定義してそれを扱うクラス作るのとかは行儀よいけど、デフォルト実装持てないの面倒じゃない?
- 最近の Java とかだとできるっぽいけどな
- そういう流れで LL だと mixin とかあるけどな
- ベースクラスを継承して使うのは楽だけど、継承ツリーに縛りが出てきちゃう
- まあインタフェース切っといて、それをデフォルト実装したベースクラスも作っておいて、 楽したければそれ継承して使う、とかでよいんじゃないか
開発の見積もり
- 見積もりは確率のかけ算なので、精度を高く出すことは難しいをことをまず前提に持つべき
- やりながらアップデートするのが無難
- 統計的に、最終的な見積もり結果の 1.4 倍はかかるだろう
- 「できた」には、とりあえず動く「できた」と、先のことまで考えた「できた」があることに注意
- プロジェクト後半の機能追加ほど、他の機能とのしがらみが増えて工数が増えやすいことに注意