リーダブルコードを読んだメモ
※箇条書きです
メモです。15章だけ詳しく?書いてます。
[リーダブルコード] (https://www.oreilly.co.jp/books/9784873115658/)
やらんもんは勝たれへん
コードは理解しやすくないといけない
名前に情報をつめこむ
名前が間違った情報になってないか考える
範囲を指定するときは、first - last
包含/排他的範囲にはbegin - end
コードを見たらすぐにわかるコメントはかかない
コードの欠陥にコメントをつける
定数にコメントをする (想定してる範囲の数値とか, なんのための定数なのかとか)
歯切れの悪い文章を磨く
行数を短くするよりも、他の人が理解するのにかかる時間を短くする。
ネストを浅くする
早めに返してネストを削除する(失敗ケースを早めに返す)
三項演算子(: ?)・do/while ループ・gotoは使いどころを考える
ド・モルガンの法則を使う 「not を分配してand/or を反転する」(逆方向は「not をくくりだす」)
「頭がいい」コードに気を付ける。あとで他の人がコードを読むときにわかりにくくなる。
役に立たない一時的な変数を消す
変数のスコープを縮める
大きなクラスを小さなクラスに分割する
(分割後に相互にメンバを参照し合うようならやっても意味はない)
邪魔な変数を削除する
変数のスコープをできるだけ小さくする
1度だけ書き込む変数を使う
エンジニアリングとは、大きな問題を小さな問題に分割して、それぞれの解決策を組み立てることに他ならない
「高レベルの目標は何か?」と自問する
// before
// 与えられた緯度軽度に最も近い'array'
// 地球が完全な球体であることを前提としている
var findClosetLocation = function (lat, lng, array) {
var closet;
var closet_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1 ) {
// 2つの地点をラジアンに変換する。
var lat_rad = radians(lat);
var lng_rad = radians(lng);
var lat2_rad = radians(array[i].latitude);
var lng2_rad = radians(array[i].longitude);
// 「球面三角法の第二余弦定理」の公式を使う。
var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
if (dist < closet_dist) {
closet = array[i];
closet_dist = dist;
}
}
return closet;
};
// after
var spherical_distance = function (lat1, lng1, lat2, lng2) {
var lat1_rad = radians(lat1);
var lng1_rad = radians(lng1);
var lat2_rad = radians(lat2);
var lng2_rad = radians(lng2);
// 「球面三角法の第二余弦定理」の公式を使う。
return Math.acos(Math.sin(lat11_rad) * Math.sin(lat2_rad) +
Math.cos(lat1_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng1_rad));
};
var findClosetLocation = function (lat, lng, arry) {
var closet;
var closet_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) {
var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
if (dist < closet_dist) {
closet = array[i];
closet_dist = dist;
}
}
return closet;
};
プロジェクト固有のコードから汎用コードを分離する
一度に1つのタスクを行う
おばあちゃんがわかるように説明できなければ、本当に理解したとは言えない。
LRU 方式(参照されていない時間が最も長い項目を削除する方式)
コードは小さくする
4xx や5xx のレスポンスコードを返しているurl-path を見つけるコマンド
cat access.log | awk '{ print $5 " " $7 }' | egrep "[45]..$" | sort | uniq -c | sort -nr
不必要な機能をプロダクトから削除する。余計なことはしない。
最も簡単に問題を解決できるような要求を考える
定期的にすべてのAPIを読んで、標準ライブラリに慣れ親しんでおく。
エラーメッセージを読みやすくする
1つの完璧なテストを作るよりは複数の小さなテストを作って検証する
テストのために本物のコードの読みやすさを犠牲にするのはダメ
テストがプロダクトの邪魔になるのもダメ
15 分/時間カウンタを設計•実装する
インターフェース
- before
class MinuteHourCounter {
public:
// カウントを追加する
void Count(int num_bytes);
// 直近1分間のカウントを返す
int MinuteCount();
// 直近1時間のカウントを返す
int HourCount();
};
- after
// 直近1分間および直近1時間の累積カウント記憶する
// 例えば、帯域幅の使用状況を確認するのに使える
class MinuteHourCounter {
// 新しいデータ点を追加する (count >= 0)
// それから、1分間は、MinuteCount()の返す値が+countだけ増える
// それから1時間は、HourCount()の返す値が+countだけ増える
void Add(int count);
// 直近60秒間の累積カウントを返す
int MinuteCount();
// 直近3600秒間の累積カウントを返す
int HourCount();
};
試案1 素朴な解決策
class MinuteHourCounter { struct Event {
Event(int count, time_t time) : count(count), time(time) {}
int count;
time_t time;
};
list<Event> events;
public:
void Add(int count) {
events.push_back(Event(count, time()));
}
...
};
class MinuteHourCounter {
...
int MinuteCount() {
int count = 0;
const time_t now_secs = time();
for (list<Event>::reverse_iterator i = events.rbegin();
i != events.rend() && i->time > now_secs - 60; ++i) {
count += i->count;
}
return count;
}
int HourCount() {
int count = 0;
const time_t now_secs = time();
for (list<Event>::reverse_iterator i = events.rbegin();
i != events.rend() && i->time > now_secs - 3600; ++i) {
count += i->count;
}
return count;
}
};
for ループが少しうるさい。̶̶この部分を読むときに速度が著しく落ちる
MinuteCount() と HourCount() がほぼ同じ
重複コードを共有化すれば コードを小さくできる
読みやすいバージョン
class MinuteHourCounter {
list<Event> events;
int CountSince(time_t cutoff) {
int count = 0;
for (list<Event>::reverse_iterator rit = events.rbegin();
rit != events.rend(); ++rit) {
if (rit->time <= cutoff) {
break;
}
count += rit->count;
}
return count;
}
public:
void Add(int count) {
events.push_back(Event(count, time())); }
int MinuteCount() {
return CountSince(time() - 60);
}
int HourCount() {
return CountSince(time() - 3600);
}
};
パフォーマンスの問題
- これからも大きくなっていく。
- MinuteCount() と HourCount() が遅すぎる。
試案2 ベルトコンベヤー設計
- 不要なデータを削除する。
- 事前に minute_count と hour_count の値を最新のものにしておく。
二段階ベルトコンベヤーの実装
class MinuteHourCounter {
list<Event> minute_events;
list<Event> hour_events; // 直近 1 分間のイベントは含まれて「いない」
int minute_count;
int hour_count; // 直近 1 時間の「すべて」のイベントをカウントしている
};
このベルトコンベヤー設計で最も大切なのは、時間経過に伴うイベントの「移 動」だ。
イベントは、minute_eventsからhour_eventsへと移動する。
void Add(int count) {
const time_t now_secs = time();
ShiftOldEvents(now_secs);
// 1 分間のリストに流し込む(1 時間のリストはあとから)
minute_events.push_back(Event(count, now_secs));
minute_count += count;
hour_count += count;
}
int MinuteCount() {
ShiftOldEvents(time());
return minute_count;
}
int HourCount() {
ShiftOldEvents(time());
return hour_count;
}
// 古いイベントを見つけて削除して、hour_count と minute_count を減らす。
void ShiftOldEvents(time_t now_secs) {
const int minute_ago = now_secs - 60;
const int hour_ago = now_secs - 3600;
// 1 分以上経過したイベントを 'minute_events' から 'hour_events' へと移動する。
//(1 時間以上経過した古いイベントは次のループで削除する)
while (!minute_events.empty() && minute_events.front().time <= minute_ago) {
hour_events.push_back(minute_events.front());
minute_count -= minute_events.front().count;
minute_events.pop_front();
}
// 1 時間以上経過した古いイベントを 'hour_events' から削除する
while (!hour_events.empty() && hour_events.front().time <= hour_ago) {
hour_count -= hour_events.front().count;
hour_events.pop_front();
}
}
試案3 時間バケツの設計
タイムスタンプを保持するのに使った time_t は秒数を保持しているので、 ここで数値が丸められてしまう。
時間バケツの実装
この設計をクラス 1 つで実装すると、理解できない複雑なコードになる。「11 章 一度に 1 つのことを」のアドバイスにしたがって、複数のクラスで異なる部分を処 理していきたい。
期間(直近 1 時間など)のカウントを追跡するクラスを作る。
// 時間バケツ N 個のカウントを保持するクラス。
class TrailingBucketCounter {
public:
// 例:TrailingBucketCounter(30, 60) は、直近 30 分の時間バケツを追跡する。
TrailingBucketCounter(int num_buckets, int secs_per_bucket);
void Add(int count, time_t now);
// 最新の num_buckets の時間に含まれる合計カウントを返す
int TrailingCount(time_t now);
};
class MinuteHourCounter {
TrailingBucketCounter minute_counts;
TrailingBucketCounter hour_counts;
public:
MinuteHourCounter() :
minute_counts(/* num_buckets = */ 60, /* secs_per_bucket = */ 1),
hour_counts( /* num_buckets = */ 60, /* secs_per_bucket = */ 60) { }
void Add(int count) {
time_t now = time();
minute_counts.Add(count, now);
 hour_counts.Add(count, now);
}
int MinuteCount() {
time_t now = time();
return minute_counts.TrailingCount(now);
}
int HourCount() {
time_t now = time();
return hour_counts.TrailingCount(now);
}
};
TrailingBucketCounter を実装する
// 最大数を持ったキュー。古いデータは端から「落ちる」。
class ConveyorQueue {
ConveyorQueue(int max_items); // キューの最後の値を増加する。
void AddToBack(int count);
// キューの値を 'num_shifted' の分だけシフトする。
// 新しい項目は 0 で初期化する。
// 最古の項目は max_items 以下なら削除する。
void Shift(int num_shifted);
// 現在のキューに含まれる項目の合計値を返す。
int TotalSum();
};
class TrailingBucketCounter {
ConveyorQueue buckets;
const int secs_per_bucket;
time_t last_update_time; // Update() が最後に呼び出された時刻
// 通過した時間バケツの数を計算して Shift() する。
void Update(time_t now) {
int current_bucket = now / secs_per_bucket;
int last_update_bucket = last_update_time / secs_per_bucket;
buckets.Shift(current_bucket - last_update_bucket);
last_update_time = now;
}
public:
TrailingBucketCounter(int num_buckets, int secs_per_bucket) :
buckets(num_buckets),
secs_per_bucket(secs_per_bucket) { }
void Add(int count, time_t now) {
Update(now);
buckets.AddToBack(count);
}
int TrailingCount(time_t now) {
Update(now);
return buckets.TotalSum();
}
};
class TrailingBucketCounter {
ConveyorQueue buckets;
const int secs_per_bucket;
time_t last_update_time; // Update() が最後に呼び出された時刻
// 通過した時間バケツの数を計算して Shift() する。
void Update(time_t now) {
int current_bucket = now / secs_per_bucket;
int last_update_bucket = last_update_time / secs_per_bucket;
buckets.Shift(current_bucket - last_update_bucket);
last_update_time = now;
}
public:
TrailingBucketCounter(int num_buckets, int secs_per_bucket) :
buckets(num_buckets),
secs_per_bucket(secs_per_bucket) { }
void Add(int count, time_t now) {
Update(now);
buckets.AddToBack(count);
}
int TrailingCount(time_t now) { Update(now);
return buckets.TotalSum(); }
};
// 最大数を持ったキュー。古いデータは端から「落ちる」。
class ConveyorQueue {
queue<int> q;
int max_items;
int total_sum; // q に含まれるすべての項目の合計
public:
ConveyorQueue(int max_items) : max_items(max_items), total_sum(0) { }
int TotalSum() {
return total_sum;
}
void Shift(int num_shifted) {
// 項目がシフトされすぎた場合に、キューをクリアする。
if (num_shifted >= max_items) {
q = queue<int>(); // clear the queue total_sum = 0;
return;
}
// 必要な分だけ 0 をプッシュする。
while (num_shifted > 0) {
q.push(0);
num_shifted--; }
// 超過した項目はすべて落とす。 while (q.size() > max_items) { total_sum -= q.front();
q.pop();
}
}
void AddToBack(int count) {
if (q.empty()) Shift(1); q.back() += count;
total_sum += count;
}
};
3つの解決策を比較する
50 行 の読みにくいコードよりも、100 行の読みやすいコードのほうが優れているのだ。
まとめ
自然に読みやすいコードを書けるようになるため の 3 つのステップを紹介しよう。
● 実際にやる
● 当たり前にする
● コードで伝える
誰かが読み にくいコードを書いたら、「ここ、少し読みにくくない?」と指摘する人がいるのが 自然な状態だ。
続ける事が大事
「自分が書いたコードってどのくらい覚えているんですか?」
「ほとんど覚えていないですよ。」
「直すときどうするんですか? わからなくなってるじゃないですか。」
「忘れても見たら簡単にわかるように書いておくんですよ。」