これまでのメモと感想文のまとめ↓
7章 制御フローを読みやすくする
7.1 条件式の引数の並び順
左に調べたい値、右に比較の基準を置く。 length >= 10 と書き、 10 <= length のように逆にしない。逆に書くヨーダ記法は、 == を = と書き間違えるミスを防ぐためのものだ。今はlintやコンパイラが警告してくれるので、読みにくくしてまで守る理由はない。
// 良い例:左は変化する値、右は基準
if (length >= 10) {}
while (bytesReceived < bytesExpected) {}
// 悪い例:ヨーダ記法で、読んでいて違和感がある
if (10 <= length) {}
while (bytesExpected > bytesReceived) {}
7.2 if/else ブロックの並び順
並べる順番には三つの目安がある。肯定形の条件を先に書く。単純な分岐を先に書く。目を引く分岐を先に書く。三つがぶつかるときは自分で判断する。否定形のほうが先に見せるべき場合(nullチェックやエラー処理)は例外になる。
// 悪い例:先頭が否定形で、読み手が一度考え直すことになる
if (!urlParams.has("expand_all")) {
renderItems(items);
} else {
items.forEach((item) => item.expand());
}
// 良い例:肯定形で、特殊なほうの分岐を先に置く
if (urlParams.has("expand_all")) {
items.forEach((item) => item.expand());
} else {
renderItems(items);
}
// 例外:エラーの分岐を先に見せたい場合
if (!file) {
logError("ファイルが存在しない");
} else {
// 通常の処理
}
7.3 三項演算子
三項演算子は、単純な二択のときだけ使う。目指すのは行数を減らすことではなく、他の人が理解するまでの時間を短くすることだ。
// 良い例:単純で、一目で分かる
const timeStr = hour >= 12 ? "pm" : "am";
// 悪い例:両方の分岐に計算があり、読み手が立ち止まる
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);
// 良い例:if/else に分ける。長くなるが読みやすい
if (exponent >= 0) {
return mantissa * (1 << exponent);
} else {
return mantissa / (1 << -exponent);
}
7.4 do/while ループを避ける
do/while は条件が本体の後ろにあるため、読み手は本体を読み終えるまで、いつ繰り返すのかが分からない。本体が必ず1回は実行されるので、入力が空のときにも壊れやすい。たいていの do/while は while に書き換えられるし、条件が上にあるほうが、上から下へ読む流れに合う。
// 悪い例:node が null でも本体が先に1回動き、エラーになる
do {
if (node.name === name) return true;
node = node.next;
} while (node !== null);
// 良い例:先に条件を確かめる
while (node !== null) {
if (node.name === name) return true;
node = node.next;
}
7.5 関数から早く返す
関数の出口は一つだけにすべき、という昔からのルールに、本は賛成していない。答えが決まった時点で返してしまえば、その後ろのコードは条件を一つ減らして読める。後片付けが心配なら、 try/finally のような仕組みに任せる。
function contains(str, substr) {
// 続ける必要のないケースを先に片付ける
if (str === null || substr === null) return false;
if (substr === "") return true;
// 本来の検索処理
}
7.6 悪名高きgoto
goto 自体が悪なのではなく、あちこちに飛ぶ使い方が問題になる。最近の言語ではほとんど出番がなく、Cでも使いどころは関数末尾でのまとめた後片付けくらいだ。それも、ジャンプが何本も交差しない形に限る。JS にgotoはなく、一番近いのはラベル付きの break と continue だが、考え方は同じで、ジャンプは少なく単純なほどよい。
7.7 ネストを浅くする
ネストは少しずつ深くなる。既存のコードに条件を一つ足すたびにインデントが一段増え、直す人は変更箇所しか見ないので、全体は見直されない。コードを足すときは一歩引き、関数全体を新しく書くつもりで眺める。方法は二つあって、早めにreturnすることと、ループの中で continue を使うことだ。
// 悪い例:一段ずつ奥に潜っていく
function handle(userResult, permissionResult, reply) {
if (userResult === "SUCCESS") {
if (permissionResult === "SUCCESS") {
reply.writeErrors("");
reply.done();
return;
} else {
reply.writeErrors(permissionResult);
}
} else {
reply.writeErrors(userResult);
}
reply.done();
}
// 良い例:失敗のケースを先に片付け、本筋はインデントしない
function handle(userResult, permissionResult, reply) {
if (userResult !== "SUCCESS") {
reply.writeErrors(userResult);
reply.done();
return;
}
if (permissionResult !== "SUCCESS") {
reply.writeErrors(permissionResult);
reply.done();
return;
}
reply.writeErrors("");
reply.done();
}
ループの中でも同じ考え方が使える。各回の処理が互いに独立しているなら、 continue は「この要素は飛ばす」という意味になり、読みやすい。
// 悪い例
for (const user of users) {
if (user.active) {
if (user.age > 18) {
sendMail(user);
}
}
}
// 良い例
for (const user of users) {
if (!user.active) continue;
if (user.age <= 18) continue;
sendMail(user);
}
7.8 実行の流れを追えるかい?
スレッド、シグナルハンドラ、例外、関数ポインタや無名関数、仮想メソッドといった仕組みは、次にどこが実行されるのかを見えにくくする。便利だが、使うのは控えめにして、読み手が実行の流れを見失わないようにする。JS ならコールバック、イベントハンドラ、Promise が近い。
// 悪い例:コールバックの入れ子で、次の処理を探して潜っていく
getUser(id, (user) => {
getOrders(user, (orders) => {
render(orders, () => done());
});
});
// 良い例:async/await なら上から下へ流れる
const user = await getUser(id);
const orders = await getOrders(user);
await render(orders);
done();
8章 巨大な式を分割する
8.1 説明変数
複雑な式の一部を変数に取り出し、その名前で何なのかを説明する。
// 悪い例:頭の中で式を最後まで追わないと、何を比べているのか分からない
if (line.split(":")[0].trim() === "root") {}
// 良い例:変数名で、この部分がユーザー名だと分かる
const username = line.split(":")[0].trim();
if (username === "root") {}
8.2 要約変数
説明変数に似ているが、目的が違う。式は複雑でなくても、長くて何度も出てくるなら、一つの概念として変数にまとめてしまう。
// 悪い例:毎回この比較を読み直すことになる
if (request.user.id === document.ownerId) {
// ユーザーはこのドキュメントを編集できる
}
if (request.user.id !== document.ownerId) {
// ドキュメントは読み取り専用
}
// 良い例:変数名がそのまま概念になる
const userOwnsDocument = request.user.id === document.ownerId;
if (userOwnsDocument) {
// ユーザーはこのドキュメントを編集できる
}
if (!userOwnsDocument) {
// ドキュメントは読み取り専用
}
8.3 ド・モルガンの法則を使う
!(a || b || c) は !a && !b && !c と等しく、 !(a && b && c) は !a || !b || !c と等しい。全体にかかった否定を内側に入れると、条件が素直に読めることが多い。ただし、書き換えて読みやすくなる場合だけ使う。
// 悪い例:全体が否定されていて、頭の中で反転させる必要がある
if (!(fileExists && !isProtected)) {
error("申し訳ありません。ファイルを読み込めません");
}
// 良い例:否定を内側に入れる
if (!fileExists || isProtected) {
error("申し訳ありません。ファイルを読み込めません");
}
8.4 短絡評価の悪用
&& や || は短絡評価されるので、これをif文の代わりにして、処理を一行に詰め込む人がいる。ifで書くべきところはifで書く。
// 悪い例:&& をifの代わりに使い、副作用が式の中に隠れている
user && user.isAdmin && grantAccess(user);
// 良い例
if (user && user.isAdmin) {
grantAccess(user);
}
// 問題ない例:デフォルト値を決める定番の書き方
const name = options.name || "default";
補足として、これは本に書いてあることではなく自分で足した話だが、JS の || は 0 や空文字も「ない」と見なす。デフォルト値を決めるときは ?? のほうが正確なことが多い。
8.5 複雑なロジックと格闘する
二つの範囲(半開区間 [begin, end) )が重なるかを判定する。最初は重なるパターンを全部書き出したくなるが、長くなって間違えやすい。見方を変えると、重ならないのは二つの場合だけだ。
// 悪い例:重なるパターンを全部並べている
overlapsWith(other) {
return (
(this.begin >= other.begin && this.begin < other.end) ||
(this.end > other.begin && this.end <= other.end) ||
(this.begin <= other.begin && this.end >= other.end)
);
}
// 良い例:逆から考えて、重ならない場合を先に除く
overlapsWith(other) {
if (other.end <= this.begin) return false; // 相手は自分が始まる前に終わっている
if (other.begin >= this.end) return false; // 相手は自分が終わった後に始まる
return true; // 残るのは重なる場合だけ
}
8.6 巨大な文を分割する
同じような巨大な式が何度も出てくるときは、重複する部分を変数に取り出す。
// 悪い例:ほぼ同じ長い式が何度も書かれている
const updateHighlight = (messageNum) => {
if ($("#vote_value" + messageNum).html() === "Up") {
$("#thumbs_up" + messageNum).addClass("highlighted");
$("#thumbs_down" + messageNum).removeClass("highlighted");
} else if ($("#vote_value" + messageNum).html() === "Down") {
$("#thumbs_up" + messageNum).removeClass("highlighted");
$("#thumbs_down" + messageNum).addClass("highlighted");
} else {
$("#thumbs_up" + messageNum).removeClass("highlighted");
$("#thumbs_down" + messageNum).removeClass("highlighted");
}
};
// 良い例:重複する部分を変数に取り出す
const updateHighlight = (messageNum) => {
const thumbsUp = $("#thumbs_up" + messageNum);
const thumbsDown = $("#thumbs_down" + messageNum);
const voteValue = $("#vote_value" + messageNum).html();
const hi = "highlighted";
if (voteValue === "Up") {
thumbsUp.addClass(hi);
thumbsDown.removeClass(hi);
} else if (voteValue === "Down") {
thumbsUp.removeClass(hi);
thumbsDown.addClass(hi);
} else {
thumbsUp.removeClass(hi);
thumbsDown.removeClass(hi);
}
};
8.7 式を簡潔にするもう1つの創造的な方法
同じ書き方の繰り返しは、それだけで複雑さになる。本ではC++のマクロを使って、繰り返しのパターンを一箇所にまとめている。JS にマクロはないので、ここではループに置き換えた。ただし、元のコードより読みにくくなっては意味がない。
// 悪い例:同じパターンの繰り返しで、どこか一行を書き間違えやすい
function addStats(from: Stats, to: Stats) {
to.totalMemory = from.totalMemory + to.totalMemory;
to.freeMemory = from.freeMemory + to.freeMemory;
to.swapMemory = from.swapMemory + to.swapMemory;
}
// 良い例:パターンを一度だけ書く
const STAT_FIELDS = ["totalMemory", "freeMemory", "swapMemory"] as const;
function addStats(from: Stats, to: Stats) {
for (const field of STAT_FIELDS) to[field] += from[field];
}
7章と8章を読んで
この2章は、感想を分けて書くほどの違いがなかった。どちらも、読み手が頭の中で同時に覚えておくこと、つまり今どの分岐にいて、どの条件が成り立っていて、どこまで実行されたのかを減らす話だからだ。
6章と違って、この2章の規則は機械的なものが多い。ESLintにも yoda 、 no-nested-ternary 、 max-depth 、 no-else-return のような対応するルールがある。AIにコードを書かせるときも、SkillやlintでAIに守らせれば済む。
例外は8.1と8.2の変数名だと思う。 userOwnsDocument = request.user.id === document.ownerId では、名前が意図で、式が実装にあたる。二つがずれていれば気づけるのは、6.6のコメントと同じ仕組みだ。名前と式を同じAIが書くと、このチェックはまた働かなくなる。規則はAIに任せられるが、その変数が業務上何を意味するかは、自分で決めたい。