要約を全編に渡って書こうと思ったけど序盤で力尽きた…続きはあなたの目で!
=================================================
■ ルール1
できるだけ単純であるべきだが、単純化してはいけないあまりに汎用的な処理を書こうとしてプログラムが複雑化していないか?
解決すべき問題を単純化したり制限をかけることでプログラムを単純に保つことができないか?本書ではとある計算処理において桁数の限界を取っ払うために補助クラスを作成する例を挙げた。
しかし、そもそも桁数の限界を取っ払う必要ある?
桁数に限界を設けてASSERTしちゃえばコードをシンプルに保てるでしょ?
という提言。単純なコードは読み易い。本のように最初から最後までまっすぐ読み通せる。
=================================================
■ ルール2
バグは伝染する開発者Aが作成したclass Hoge;を開発者Bが使用してclass Foo;を実装したとする。
しかし実は class Hoge にはバグが 含まれていた。
ところが、開発者Bはその挙動が開発者Aの意図したものだと思い込んでバグったまま class Foo を実装してしまった。
更に開発者Cがclass Fooを使って別の機能を実装し……という感じで、 class Hoge; のバグはどんどん伝染して絡まる。
class Hoge; のバグを直したとたんに開発者B,Cが作成したコードは正しく動かなくなる可能性がある。さて、class Hoge; のバグを早期に発見してバグの伝搬を防ぐには???
・バグったコードを放置すると、どんどんコードが絡まるからバグを早期発見できるようにすべき
・バグを早期発見するためにテスト駆動しようよ!
・じゃぁ、テストしやすいコードって?
・ステートレスな関数は自動テストしやすい。
・テストしやすくしようと考えると、必然的に単純なコードを書く羽目になる。
・ステートを保持する必要があるクラスには監査関数(Hoge::audit())を用意する。
friend で内部変数をテストに公開してテストコード側でチェックを行うより保守が楽。
// いい例ではないかもしれないが…
void Hoge::audit() {
// ステートがIdleの時は _data にはデータが入っていないはずである etc...
Assert(_state == State::Idle && _data == nullptr);
}
・状態の不正、使われ方の不正をテストやASSERTで検知しやすいような設計を心がけよう。
=================================================
■ ルール3
優れた名前こそ最高のドキュメントである・打鍵数をケチって短すぎる変数名を付けるのを避けよう。ちゃんと意味の分かる変数名にしよう
・規則を混ぜない
例えば、STL式のコンテナと独自のコンテナを混在して使う時、メンバ関数名と意味を揃えないと混乱が生じる
・厳格で機械的な命名規則を徹底させよう
誰が書いても必然的に同じ命名になるはずだ。
他の人が書いたコードを読み易くなる
ハンガリアン記法も徹底すればそんなに悪いものじゃないぜ?(他の命名規則と混じると途端にアレだけど)=================================================
■ ルール4
一般化には3つの例が必要・YAGNI
https://ja.wikipedia.org/wiki/YAGNI
ある1つ目の「赤い標識を見つける」という要件に対して Sign* findSign(const vecotr<Sign*>& signs, Color color);という関数を書くより
Sign* findRedSign(const vecotr<Sign*>& signs);と書くべきだというような話。
将来的に「任意の色を指定して標識を見つける」という処理が必要になるかを現時点から想像しながら書くのは無駄になる確率が高い。
2つ目の「position の近くの標識を見つける」という要件が出てきた場合も一般化するのはまだ早い
Sign* findSignNearPosition(const vecotr<Sign*>& signs, const Location& location, float maxDistance); を用意するだけにする。
3つ目の「赤くて、かつposition近くの標識を見つける」という要件が来て初めて一般化を考える
Sign* findSignByColorAndPosition(const vecotr<Sign*>& signs,
const Color& color = Color::Invalid,
const Location& locaton = Location::Invalid, float maxDistance = FLT_MAX);
を用意する。
こうした場合でも元の関数を消す必要はない。
相変わらず位置だけを条件にしたい場合は findSignNearPosition を使えばよい。・YAGNIよりひどい。。。
汎用化をさせすぎると機能を拡張するたびにどんどん複雑になり、呼び出し側のコードも複雑になる。
後者の方がシンプルで拡張もしやすくね??って話。
// 青か、メインストリートに近い標識を取得する
vector<Sign*> signs;
FindQuery* colorQuery = new FindQuery();
query->colors = { Color::Blue, };FindQuery* areaQuery = new FindQuery();
query->areas = { mainStreetr, };FindQuery* query = new FindQuery();
query->queries = {colorQuery, areaQuery, };
query->boolean = Boolean::Or;
vector<Sign*> results = query->DoQuery(signs);
or
// これでええやん?
vector<Sign*> signs, results;
for(Sign* sign : signs) {
if(sign->color == Color::Blue || matchArea(sign, mainStreet)) {
results->push_back(sign);
}
}
=================================================
■ ルール5
最適化に関する教訓その1は、「最適化するな」
・とにかく最適化すしない!を肝に銘じる
素直で読み易いコードを単純明快なコードを書こう
ロジックが単純でメモリアクセスが素直な方が速い場合も多い
単純なコードをは最適化しやすいから単純なコードを書け。
・最適化手順
1) 計測する
2) バグがないか確認する
そもそも想定と違う関数の呼び出し方をしていないか?など
3) データを計測、観測する
関数の引数、扱ってるデータがどんなものか観測する
場合によっては何回も行っている計算やメモリ確保を減らすことができる可能性がある
4) プロトタイプを作る
最適化対象の関数が複雑な計算をしているとして、その処理を全部取っ払って単純な結果を返す関数に置き換える。
最適化対象の関数のコストを0に近づけた時に、高速化の結果が期待通りになるかどうかを先に確認する。
メモリアクセスのキャッシュ状況などが変わって、結局別の場所の処理負荷が高まるかもしれない。
5) 最適化をする
「同じ処理を高速にする」より「やることを減らす」方向で。
関数を呼び出すたびに同じ結果になる処理を繰り返してるかもしれない...etc