1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

『リーダブルコード』7章・8章を読んで制御フローと式の分割をわかる

1
Posted at

これまでのメモと感想文のまとめ↓

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に任せられるが、その変数が業務上何を意味するかは、自分で決めたい。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?