#ごあいさつ#
当記事に興味を持っていただき、ありがとうございます。
バックエンドメインのエンジニアになってから間もなく3年という節目に、1年ぶり4度目の「リーダブルコード」を手に取りました。3年目ともなると自分が書いてきたコードがある程度ストックされ、「正しく動くか」はもちろん、「最適な書き方をしているか」も念頭に置き、後輩のソースレビューをする機会も増えてきました。「最適な書き方」の定義はさまざまですが、今回は書籍名の通り「読みやすい」を一つの指標に、名著「リーダブルコード」を読み進め、まとめていきたいと思います。
本書は3部構成になっており、本記事では1,2部の中から比較的分かりやすく、かつ大切だと感じるテーマをピックアップしています。本記事で取り上げているテーマは書籍のうちのごく僅かです。是非本書を購入し、隅々まで何度も読んでみてください!
#当記事について#
###記事を書いた人###
2020/3に情報系四年制大学を卒業後、2020/4からバックエンドエンジニアとして採用。主にPHP(Laravel)を用いた開発を業務で行う傍ら、「AWS」を通してインフラの学習を進め「AWS SAProfessional」を取得。2021/12に退職し、学んだことをまとめつつ、腰を据えて転職活動中。
###取り扱う書籍について###
「リーダブルコード より良いコードを書くためのシンプルで実践的なテクニック」 (日本語版)
著:Dustin Boswell,Trevor Foucher 訳:角 征典
発行所:株式会社オライリー・ジャパン
発行元:株式会社オーム社
初版第23刷
#はじめに#
xページ~
本書の目的は、君のコードをよくすることだ
「リーダブルコード」の目的です。つまり、プロジェクトの全体アーキテクチャやデザインパターンの話はありません。例えば、変数に名前を付けたり、ループの処理を書いたり、問題を関数に細分化したりといった、基本的な部分のお話をしています。
###本書について###
本書の目的は、読みやすいコードを書くことである。その中心となるのは、コードは理解しやすくなければいけないという考えだ。具体的に言えば、誰かが君のコードを読んで理解する時間を最短にすることだ。
プロジェクトリーダーや先輩に見てもらうことはもちろんですが、**明日の自分も「誰か」**に含まれますね。
#1章 理解しやすいコード#
2ページ~
###優れたコードって何?###
例えば、このようなコードのほうが、
for(Node* node = list->head; node != NULL; node = node->next)
print(node->data);
以下のコードよりも優れているとみんな思っている。
Node* node = list->head;
if(node == NULL) return;
while(node->next != NULL){
Print(node->data);
node = node->next;
}
if(node != NULL) Print(node->data);
(どちらも動作は全く同じなのに)
上記の2つのコードを見比べると、明らかに前者が優れていると分かります(少なくともリーダブル)。短い行にまとまっているのはもちろん、慣れ親しんだfor文なので、直観的にどの引数が何の役割を担っているかが理解できそうです。
でも、どちらが優れているかわからないことも多い。例えば、このようなコードは、
return exponent >=0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);以下のコードよりも優れているのだろうか。
if(exponent >= 0){
return mantissa * (1 << exponent);
}else{
return mantissa / (1 << -exponent);
}
前者のほうが簡素だ。でも、後者のほうが安心できる。
前者は短い行にまとまってはいますが、処理の内容を言葉で説明するには少し頭を使いそうです。対して後者は、慣れ親しんだif文を用いており、「もしexponentが >=0だったら、、」というように上から下へ声に出して読み上げていけば、どういった処理なのか理解しやすそうです。**「誰かが君のコードを読んで理解する時間を最短に」**というテーマで考えると、複数行で書かれた後者のほうが優れていると言えます。
###小さいことは絶対にいいこと?###
コメントをつけると「コードが長く」なるけど、そのほうが理解しやすいことになる。
//"hash = (65599 * hash) + c"の高速版
hash = (hash << 6) + (hash << 16) - hash + c;
私の実体験として、開発会社オリジナル(他社)のFWで書かれた案件を引き継いだ時は、本当に地獄でした。LaravelやFuelで書かれたコードしか馴染みがなかった私にとってはやたら長文でとても理解できませんでしたが、コメントはしっかりと差し込まれていたので何とかなりました。個人的にはIDEで全体検索をした際に引っかかるように「キーワード」になるような言葉をコメントに含めるとgoodだと思います。
###「理解するまでにかかる時間」は競合する?###
「それじゃあ、そのほかの条件は?コードを効率化するとか、設計をうまくやるとか、テストしやすいとか、いろいろあるじゃん?そういうのは理解しやすさと競合しないわけ?」そんなことを考えるかもしれないね。
でも、そのほかの目標とは全く競合しないんだ。高度に最適化されたコードであっても、もっと理解しやすくできるはずだ。それに、理解しやすいコードというのは、優れた設計やテストのしやすさにつながることが多い。
「理解しやすさ」だけを考えてコーディングしてよいのか、という疑問は私も感じていました。著者曰く、「理解しやすさ」はそのほかの要素の底上げにも繋がる観点のようです。
#2章 名前に情報を埋め込む#
10ページ~
###明確な単語を選ぶ###
例えば、「get」はあまり明確な単語ではない。
def GetPage(url);
...「get」という単語からは何も伝わってこない。このメソッドはページをどこから取ってくるのだろうか?ローカルキャッシュから?データベースから?インターネットから?インターネットから取ってくるのであれば、FetchPage()やDownloadPage()のほうが明確だ
私もメソッド名にget○○()を多用していたので、どきっとしました。特に経験の浅いエンジニアの場合、データを取ってくる=DBから取得、という意識が強いかと思うので、より明確な単語を調べるようにしましょう。データの取得先はDBだけでなく、キャッシュ、API、外部ストレージなど、様々な可能性があります。
###tmpやretvalなどの汎用的な名前を避ける###
var euclidean_norm = function(v){
var retval = 0.0;
for(var i = 0; i < v.length; i += 1)
retval += v[i] * v[i];
return Math.sqrt(retval);
};いい名前が思いつかなかったら、戻り値にretvalとつけたくなる。でも、retvalには「これは戻り値です」以外の情報はない(戻り値なのは当たり前だ)。
戻り値は何だろう?と思ったとき、「sum_squares」と書いてあれば「ああ、squareの合計ね」となりますが、「retval」と書いてあると「で、そのretvalには何が入っているの?」となりますね。今後そのコードを読む自分以外の誰かのことも考えると、汎用的な名前は避けたほうが良いようです。
ただし、以下のように場合には汎用的な名前を使っても問題ないようです。
if(right < left){
tmp = right;
right = left;
left = tmp;
}
このような場合は、tmpという名前で全く問題ない。この変数の目的は、情報の一時的な保存だ。しかも、生存期間はわずか数行である。
以上のように第2章では、名前の付け方についてのススメがまとめられています。
・ループイテレータ
・抽象的な名前よりも具体的な名前を使う
・値の単位
・その他の重要な属性を追加する
・スコープが小さければ短い名前でもいい
・名前のフォーマットで情報を伝える
などなど、、。ここらへんは各社のコーディングルールでも定められているので心配ないとは思いますが、2年目以降の方もたまには振り返ってみてよいかもしれません。
#3章 誤解されない名前#
29ページ~
###例:filter()###
データベースの問い合わせ結果を処理するコードを書いているとしよう。
results = Database.all_objects.filter("year <= 2011")
このresultsには何が含まれているだろうか?
・「year <= 2011」のオブジェクト
・「year <= 2011」ではないオブジェクト
本書では、「選択する」のであればselect()、「除外する」のであればexclude()にしたほうがいいと書いてありました。個人的にはそもそも変数名「results」が良くないと思います。「selectedDate」や「excludedDate」などが良いでしょうか?
###限界値を含めるときはminとmaxを使う###
ショッピングカートには商品が10点までしか入らないとしよう。
CART_TOO_BIG_LIMIT = 10
if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
Error("カートにある商品が多すぎます。")
(...)
ここでの根本的な問題は、CART_TOO_BIG_LIMITという名前があいまいなことだ。これでは「未満(限界値を含まない)」なのか「以下(限界値を含む)」なのかがわからない。
(...)
この場合は、MAX_ITEMS_IN_CARTという名前にするべきだ。
最大、最小などを示す場合はmax,minを使えば間違いなさそうです。
昔、変数名に「fewest」を使ったところ、ソースレビューで「minのほうが良いよ」とご指導いただいたことがあります。「最小 英語」で検索すると「min(minimum)」が出るのですが、「少ない 英語」だと「few」や「least」が出たりします。経験浅の場合は注意が必要です。
本書ではほかに、「範囲を指定するときはfirstとlastを使う」、「包含/排他的範囲にはbeginとendを使う」ことが推奨されています。
#4章 美しさ#
41ページ~
※引用が長いですがコードの美しさの話ですので、コードは流し見で大丈夫です。
###一貫性のある簡素な改行位置###
(...)
任意の速度のネットワークに接続したときに、プログラムがどのように動くかを評価するコードだ。
(...)
public class PerformanceTester {
public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator(
500,/*Kbps*/
80,/*millisecs latency*/
200,/*jitter*/
1/*packet loss %*/);public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
45000,/*Kbps*/
10,/*millisecs latency*/
0,/*jitter*/
0/*packet loss %*/);public static final TcpConnectionSimulator cell = new TcpConnectionSimulator(
100,/*Kbps*/
400,/*millisecs latency*/
250,/*jitter*/
5/*packet loss %*/);
横幅80文字に合わせるために(これは会社のコーディング標準なのだ)余計な改行が入っている。その結果、t3_fiberの見た目が他と違って残念なことになっている。コードのシルエットが変なので、自然とt3_fiberに目が向いてしまう。それに「似ているコードは似ているように見せる」の原則も守られていない。
コードの見た目を一貫性のあるものにするには、適切な改行を入れるようにしよう(それからコメントも整列させよう)。
(...)
このクラスを簡潔に書いたら以下のようになる。
public class PerformanceTester {
// TcpConnectionSimulator(throughput,latency,jitter,packet_loss)
// [kbps] [ms] [ms] [percent]public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(500, 80, 200, 1);public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(45000, 10, 0, 0);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(100, 400, 250, 5);コメントを最上部に移動して、仮引数を一行で書くようにした。数値の右隣からコメントが少なくなったけど、より簡素な表組みに「データ」が並ぶようになった。
「美しさ」=「読みやすさ」がとても分かりやすいですね。まるで表題と表体の関係のようで、直観的に引数の値と順番が理解できます。
#5章 コメントすべきこと知る#
55ページ~
###コメントするべきでは「ない」こと###
// Accountクラスの定義
class Account{
public:
//コンストラクタ
Account();
//profitに新しい値を設定する
void SetProfit(double profit);
//このAccountからprofitを返す
double GetProfit();
};新しい情報を提供するわけでもなく、読み手がコードを理解しやすくなるわけでもない。まったく価値がない。
コードを見たらすぐ分かることは、わざわざ書く必要がありません(変数名や関数名がまともな前提)。これも初学者がやりがちなことだと思います。ただ、コードを見たらすぐ分かる、の線引きは曖昧で、コードを読むエンジニアのレベルによって異なります。一例として、本書では以下のコメントは「あり」とのことです。
# 2番目の'*'以降をすべて削除する
name = '*'.join(line.split('*')[:2])厳密に言えばこのコメントも「新しい情報」を提供していない。コードを見ればどのように動くかわかる。でも、コードを理解するよりも、コメントを読んだほうが早く理解できる。
###読み手の立場になって考える###
void Clear() {
vector().swap(data); //えっ? どうしてdata.clear()じゃないの?
}
(...)
このようにしているのは、ベクタのメモリを開放してメモリアロケータに戻す方法がこれしかないからだ。これはあまり知られていないことである。つまり、ここにコメントをつけるべきなのだ。
//ベクタのメモリを開放する(「STL swap技法」)で検索してみよう)
vector().swap(data);
表題の通り、自分以外の誰かが見たときに「えっ?なにこれ?」となる場所にコメントを書きましょう。ほかに、タイムアウトなどハマりそうな罠を告知したり、「全体像」(処理の抽象的な説明)についてのコメントや要約コメントを書いておくと、読み手の助けになるとのことです。
#6章 コメントは正確で簡単に#
71ページ~
###あいまいな代名詞を避ける###
//データのキャッシュを入れる。ただし、先にそのサイズをチェックする。
「その」が指しているのは、「データ」かもしれないし「キャッシュ」かもしれない。
(...)//デートをキャッシュに入れる。ただし、先にデータのサイズをチェックする。
これが最も簡単な改善だ。あるいは、文章全体を書き換えて「それ」を明確にすることもできる。
//データが十分に小さければ、それをキャッシュに入れる。
あれこれそれ等のあいまいな代名詞は避けるか、対象が明確になるように書き換えましょう。コードを読み進めれば「それ」の正体がわかるかもしれませんが、それではコメントする意味がないですね。
###入出力のコーナーケースに実例を使う###
//'src'の先頭や末尾にある'chars'を除去する。
String Strip(String src, String chars){...}
このコメントはあまり正確ではない。以下のような質問に答えることができないからだ。
・charsは、除去する文字列なのか、除去する文字列なのか、順序のない文字集合なのか?
・srcの末尾に複数のcharsがあったらどうなるのか?
以上の質問に答えられる適切な実例はこうだ。
//...
//実例:Strip("abba/a/ba","ab")は"/a/"を返す
String Strip(String src, String chars){...}
言葉で説明するより、疑問点に対する答えをすべて含んだ実例を書いておけば、万事解決することもあるようです。
#7章 制御フローを読みやすくする#
83ページ~
###条件式の引数の並び順###
以下の二つのコードはどちらが読みやすいだろうか。
if(length >= 10)
または、
if(10 <= length)ほとんどのプログラマは最初のほうが読みやすい。
(...)
「もし君が18歳以上ならば」というの自然だ。でも、「もし18年が君の年齢以下ならば」というのは不自然だ。
おっしゃるとおり。
###関数から早く返す###
関数で複数のreturn分を使ってはいけないと思っている人がいる。アホくさ。関数から早く返すことはいいことだ。むしろ望ましいときもある。例えば、
public boolean Contains(String str, String substr){
if(str == null || substr == null) return false;
if(substring.equals("")) return true;
...
}
このような「ガード節」を使わずに実装するとすごく不自然な実装になる。
関数の出口を1つにしたいというのは、何らかのクリーンアップコードを確実に実行したいからだろう。
ガード節、つまりreturnで抜けないとelseの中にさらにif/elseの処理が入ることになり、ネストが深くなってしまいます。言語によって、「try ... finally」「with」「using」というように、こうした仕組みがより洗礼された形で提供されいるそうなので、一度調べておくとよいかもしれません。
#8章 巨大な式を分割する#
99ページ~
###説明変数###
例えば、以下のようなコードがあったとする。
if line.split(':')[0].strip() == "root":
...説明変数を使えば、以下のようになる。
username = line.split(':')[0].strip()
if username == "root":
...
前者のような複雑っぽいをことをしているようでただ読みづらいだけのコードは、初学者がやりがちかと思います。この処理内で今後「line.split(':')[0].strip()」を使うことになるかもしれないですし、わざわざコメントに「ユーザーネーム」と書いてあげるよりも、端的な変数名にしてあげればより読みやすく美しいはずです。
###要約変数###
if(request.user.id == document.owner_id){
//ユーザーはこの文書を編集できる
}...
if(request.user.id != document.owner_id){
//文書は読み取り専用
}request.user.id == document.owner_idはそれほど大きな式ではない。でも、変数が5つも入っているから、考えるのにちょっと時間がかかる。
このコードが言いたいのは「ユーザーは文書を所持しているか?」だ。ようやく変数を追加すれば、この概念をもっと明確にできる。
final boolean user_owns_document = (request.user.id == document.woner_id);
if(user_owns_document){
//ユーザーはこの文書を編集できる
}...
if(user_owns_document){
//文書は読み取り専用
}
正直なところ、「request.user.id == document.woner_id」は見たらすぐ理解できるのでここまでする?と思いましたが、見やすさ以外にも以下の利点があるようです。
また、user_owns_documentwo最上部に定義したことで、「この関数で参照する概念」を事前に伝えることができるようになった。
ここまでの考えには至りませんでした。
#9章 巨大な式を分割する#
111ページ~
###役に立たない一時変数###
now = datetime.datetime.now()
root_message.last_view_time = nowこのnowを使う意味はあるだろうか?意味がない理由を以下に挙げよう。
・複雑な式を分割していない。
・より明確になっていない。datetime.datetime.now()のままでも十分に明確だ。
・一度しか使っていないので、重複コードの削除になっていない。
前章では「説明変数」と「要約変数」を用いて、巨大な式を分割し、説明文のようにすることで読みやすくしていました。今回はその逆で、コードの読みやすさが向上しないは削除しよう、というお話です。何度もnowを使う機会があるのであれば変数に入れてあげて良いですが、一度だけであれば不要ですね。
###JavaScriptのグローバルスコープ###
JavaScriptでは、変数の定義にvarをつけないと(例えば、var x = 1じゃなくて x = 1)、その変数はグローバルスコープに入ってしまう。グローバルスコープに入ると、すべてのJavaScriptファイルや<script>ブロックからアクセスできてしまう。以下に例を挙げよう。
<script>
var f = function(){
//危険:'i'は'var'で宣言されていない!
for(i = 0; i < 10; i += 1) ...
};
f();
</script>このコードでは、意図せずに変数iをグローバルスコープに入れている。したがって、ほかのブロックからも変数が見えてしまう。
<script>
alert(i); // '10'が表示される。'i'はグローバル変数なのだ!
</script>
このスコープの規則のことを知らないプログラマが多い。
普通の変数に対してはvar,let,constを当たり前のように使っていても、うっかりif文の条件などにvarを付け忘れてしまわないように注意が必要ですね。2つの関数でvarのない同じ名前のローカル変数を作ると、2つの関数で同じ変数が参照され、めちゃくちゃになってしまいます。本書では2つの関数が「混線」してしまうと表現しています。
#さいごに#
ここまでご覧いただき、ありがとうございます。
本書は「こんな感じでコーディングしたらいいんじゃない?」と優しくアドバイスしてくれる内容になっています。歌詞は、チーム、言語によっては「その書き方だと困る」となってしまうかもしれませんので、参考までにとどめていただければと思います。
冒頭にも書いてある通り、本記事は本書のごく一部をピックアップしているにすぎませんので、ぜひ購入し、隅々まで読んでみてください!