はじめに
こんにちは、新卒2年目エンジニアの細川です!第2回目となった社内輪読会!
前回までの記事も含めてこちらにありますのでぜひ確認してみてください!
引き続き「リーダブルコード」を読み進めていきます。僕の担当は第2部の始めとなる第7章「制御フローを読みやすくする」となります。初回に千奈さんが全体像をまとめてくださってるのでそちらを参考にしていただきたいのですが、これまでの第1部とは異なり、第2部ではプログラムのロジック面について触れていきます。その中でも第7章では分岐やループなどの制御フローについて結構具体的なアドバイスとともに書かれていました。
7章を一言でまとめると、「直感的でわかりやすい流れでコーディングしよう!」ということだと思います。「でも具体的に、直感的な流れって??」という問いに簡単な例を示しながら書いてくれています。
7.1条件式の引数の並び順
まずはif文などの引数の順番についてです。以下のコードはどちらが読みやすいでしょうか?
// if文
if (age >= 10)
if (10 <= age)
// while文
while (value_received < value_expected)
while (value_expected > value_received)
if文でもwhile文でも、上の方が読みやすいと思った方は多いと思います。
読みやすくするための原則としては「左:調査対象、右:比較対象」というものがありそうです。
この原則は普段話したり書いたりする言葉の用法とあっているため、自然で受け入れやすいみたいです。
「もし君の年齢が18才以上ならば」とは言うけど、「もし18年が君の年齢以下ならば」というのは不自然に感じるのと同じです。
ただテストで使うassert文の場合、「左:比較対象、右:調査対象」の並びになっているのでちょっと注意が必要です。テストの際にこの並びになっているのにも理由があるようで、こちらの記事でいい感じにまとめてくださってます。
// assert文
assert.Equal(value_expected, value_received)
7.2if/elseブロックの並び順
続いてはif/elseブロックの並び順についてです。これは僕もいまだに迷いながら書いてしまってます。
// 1例目
if (a == b) {
//第1の場合の処理
} else {
//第2の場合の処理
}
// 2例目
if (a != b) {
//第2の場合の処理
} else {
//第1の場合の処理
}
上の2つのコードは同じ意味になりますが、基本的には1例目の方がよさそうです。
if/elseブロックの並び順については以下のようなルールが示されてます。
- 条件は基本的には否定ではなく肯定を使う
- 単純な条件を先に書く
- 目立つ条件を先に書く
ただ、これらは衝突する場合もあるので結局時と場合によって変わるということらしいです。
否定でも以下のような場合は理解しやすかったりします。
if !file {
//エラーログ等
} else {
//通常時の処理
}
基本的なルールを頭に入れておいて都度都度考えながらコードを書くのがよさそうです。
7.3三項演算子
続いては三項演算子についてです。
三項演算子とは以下のように、「条件 ? a : b」という書き方で、「if (条件) {a} else {b}」を短く書いたものです。
// 三項演算子
ampm = (hour >= 12) ? "pm" : "am"
// if文
if (hour >= 12) {
ampm = "pm"
} else {
ampm = "am"
}
上の場合は、三項演算子を使った方がわかりやすくなる例です。
ただ基本的には、三項演算子は使わないほうがいいということです。上記のように明らかにわかりやすくなる場合のみ使って、あとはきちんとif文を書きましょう!
行数を短くするよりも、他の人が理解する時間を短くすることの方が重要です。
7.4do/whileループを避ける
続いてはdo/while文についてです。
do/while文は「do {式} while (条件)」のように条件が後にくる書き方です。以下のような例が示されています。
// "name"に合致するものを"node"のリストから探索する。
// "max_length"を超えたノードは考えない
const ListHasNode: boolean(node: Node, name: string, max_length: int) {
do {
if (node.name() === name)
return true;
node.next();
} while (node !== null && max_length > 0);
}
do/while文は条件文が下にある、doの中の式は必ず1回実行されるなどの特徴があります。
基本的に他のif文やwhile文、for文などは条件が上にくるので慣れてないと若干読みづらさがあるかと思います。そのため基本的にはdo/while文は避けたほうがいいとのことでした。
ただ、1回は必ず実行させたいという場合にコードを重複させてまでdo/whileを削除することはしなくていいよとも言ってくれてます。以下のような場合はおとなしくdo/while文を使いましょう
// 処理
...
while (condition) {
//処理(2度目以降)
...
}
7.5関数から早く返す
次は関数内のreturn文についてです。
この節を読んで初めて知ったのですが、関数内で複数のreturn文を使うことを良しとしない方もいるみたいです。この節では特に強い言葉でそのような考え方が批判されていました。ここで書くのは気が引けるのでぜひ読んで確認してみてください(笑)
return文を複数書くのが望ましい例として以下のようなものが挙げられています。
const Contains: boolean(str: string, substr: string) {
if (str === null || substr === null) {
return false;
}
if (substr === "") {
return true;
}
...
}
上記のようにガード節などでも、return文が複数ある場合はよくあります。関数から早く返せる場合には早く返してしまった方が処理全体の時間も短くなるので、早く返した方がよさそうですね。
7.6悪名高きgoto
すごいタイトルですね(笑)続いてはgotoです。最近ほとんど見なくなって僕もアセンブラとかで使ったくらいでそれ以外の言語を書いてるときに基本的に使ったことはありません。
ちなみにgotoを知らない方に説明すると、以下のようにラベルを付けた場所に同じ関数内であればどこからでも飛ぶことができる命令です。
...
if (a == NULL) {
goto END; // goto命令
}
// その他の処理
...
END: // gotoのラベル
return;
gotoはかなり毛嫌いしている方が多いらしくネット上でも使うなという記事が多い印象です。その理由としてはgotoがあると、すぐにコードの進行が複雑になってしまってスパゲティコードになってしまうからです。ただ現在でもC言語では様々な場所でgotoが使われていて、リーダブルコードにも、「gotoは神への冒涜だ!とはねのけるよりも、なぜgotoを使っているのか?を分析する方がよい」と書かれています。基本的に害のない使い方としては、上のように関数の最下部に置いたENDに飛ぶというような使い方です。
gotoのとび先が複数あったり、上に飛んだり交差したりするといよいよ読めなくなってしまいますので、基本的には使わないようにしましょう。
7.7ネストを浅くする
普通にコードを書いていたらネストって深くなってしまいがちですよね。ただ、ネストが深いコードは読み手の「精神的スタック」に条件を置いておいてもらう必要があり、理解しづらくなってしまいがちです。
以下にネストが深くなっている場合と浅くした場合を示します。
// ネストが深くなっている場合
if (user_result === SUCCESS) {
if (permission_result !== SUCCESS) {
reply.WriteErrors("error reading permissions");
reply.Done();
return;
}
reply.WriteErrors("")
} else {
reply.WriteErrors(user_result);
}
reply.Done();
return;
// ネストを浅くした場合
if (user_result !== SUCCESS) {
reply.WriteErrors(user_result);
reply.Done();
return;
}
if (permission_result !== SUCCESS) {
reply.WriteErrors(permission_result);
reply.Done();
return;
}
reply.WriteErrors("");
reply.Done();
ネストを浅くするコツとしては「失敗の場合」に早くreturnすればいいみたいです。
エラーケースを早くreturnすることで、ネストを浅くしやすくできます。ループの中では早くreturnするというのが使えない場合もあるのでcontinueを使いましょう。ただcontinueを使うことで理解しづらくなる場合もあるので、これも場合によるといったところでしょう。
また、ネストが深くなる原因として、「コードを後から追加する」ことが挙げられていました。
最初はクリーンなコードであっても都度都度、変更を加えていくことで見づらくなってしまうことがあります。それぞれの変更は簡潔なもので見やすいのですが、トータルでコードを見てみると複雑なコードになってしまいがちなのはよくあることです。業務の都合上なかなか難しいこともあるかもしれませんが負債を解消するためにも定期的にリファクタしていきたいですよね。リファクタの際にためになる書籍もベテランエンジニアの方に教えていただきました!ファウラーのリファクタリング・カタログ「Replace Nested Conditional with Guard Clauses」です。こちらもぜひ読んでみたいと思っています。
7.8実行の流れを終えるかい?
この節は今まで触れてきた低レベルの制御フローではなく、高レベルの「流れ」つまりプログラム全体での流れについて触れられています。スレッドや関数ポインタと無名関数など裏側でコードを実行する構成要素についてです。
具体的なアドバイスは述べられていませんでしたが、「理解しづらくなることもあるのであまり使いすぎないようにしようね」ということでした。
7.9まとめ
ここまでで述べたことをまとめると以下のようになります
- 比較を書くときは調査対象(変化する値)を左に、比較対象(安定した値)を右に。
- if/elseブロックは肯定形・単純・目立つものを先に書く。衝突する場合は、状況に応じて。
- 三項演算子・do/whileループ・gotoなどはできるだけ使わないように。
- ネストは深くしすぎない。ガード節を多用しよう!
終わりに
7章では、これまで以上に、より具体的なアドバイスをくれています。場合によっては、これらの原則に則らないほうが良い場合もあるかもしれませんが、このように基本的に良い書き方を知っておくことで普段のコーディングの際に迷うことが減ってより良いコードが書けるようになると思うので、もしまだ読んでない方がいらっしゃったら、一度読んでみるのをお勧めします!