yamamotototo のプロフィール画像@yamamotototo

投稿日
2022/12/20

リーダブルコードについて知ろう!

はじめに

社会人1年目の時に先輩エンジニアの方に勧められて読んだリーダブルコードで学んだ内容をまとめました。
ココに全部は書ききれなかったので、興味がある方は是非買って読んでみてください!

良いコードとは?

①Readable(読みやすい)+ Code(コード)
②コードは他の人が最短時間で理解できるように書かなければならない

  • 理解するまでにかかる時間を短縮する

 ┗「理解する」:変更を加えたりバグを見つけることができる

  • 遠い未来の自分が見た時にわかるように
  • プロジェクトに途中参加したメンバーがわかるように

③最初は大変かもしれない...それでもやる価値がある

  • 優秀なプログラマを目指して、バグの少ないコードを作り出そう

名前に情報を詰め込む

明確な単語を選ぶ

【Bad】
def GetPage(url) :    //インターネットからページを取得                        
……

→どこからページを取ってくる(Getしてくる)のかわからない(ローカルキャッシュ?データベース?)

【Good】  
def FetchPage(url) :  //もしくはDownloadPage                          
……

※「 explode()とsplit() 」
→explode()は”何かを分割する”を伝えれるが、split()との違いは名前からはわからない
2つの関数違い:区切り文字に正規表現を使えるか/使えないか
[PHP]splitとexplodeの違い

汎用的な名前を避ける

エンティティの値や目的を表した名前で選ぶ

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); 
}; 

⬇︎変数の目的や値を表すものがイイ(“vの2乗の合計”を表現する)

var euclidean_norm = function (v) {
      var sum_squares = 0.0;
      for (var i = 0; i < v.length; i += 1) 
            sum_squares+= v[i] * v[i]; 
      return Math.sqrt(retval); 
}; 

※これならバグも見つけやすい!
sum_squares += v[i];
↑合計する「square(2乗)」がない。バグだ!

具体的な名前を使う

┗「ServerCanStart(抽象的)」を「CanListenPort(具体的)」に
※任意のTCP/IPポートをサーバがリッスンできるか確認するメソッド

例. DISALLOW_EVIL_CONSTRUCTORS
(メモリリワークなどの問題につながる”悪”のコンストラクタを許可しない規約)

#define DISALLOW_EVIL_CONSTRUCTORS(ClassName) \ 
      ClassName(const ClassName&); \
      void operator=(const ClassName&); 
   
class ClassName { 
   private: 
      DISALLOW_EVIL_CONSTRUCTORS(ClassName); 
   public: ... 
}; 

※ 「EVIL(悪の)」 という言葉よりも、このマクロが「許可していないもの」を明確にするほうが大切(実際には operator=() メソッドも許可していない)
↓あまり刺激的ではなく、より具体的な名前に変更

#define DISALLOW_COPY_AND_ASSIGN(ClassName) ... 

名前に情報を追加する

名前は短いコメントのようなものだ!
┗16進数の文字列を持つ変数だったら?→「hex_id」とする (IDフォーマット優先)
値の単位・時間やバイト数のように計算できるものがあれば、変数名に単位を入れる

【Bad】
var start = (new Date()).getTime(); // ぺージの上部
...
var elapsed = (new Date()).getTime() - start; // ページの下部 
document.writeln(" 読み込み時間:" + elapsed + " 秒 "); 

※getTimeが秒ではなくミリ秒を返すため、このままだとうまく動かないので変数名に「 _ms 」を追加して、コードを修正

【Good】
var start_ms = (new Date()).getTime(); // ページの上部
...
var elapsed_ms = (new Date()).getTime() - start; // ページの下部 
document.writeln(" 読み込み時間:" + elapsed_ms / 1000 + " 秒 ");
関数の仮引数 単位を追加した仮引数
Start(int delay) delay→delay_secs
CerateCache(int size) size→size_mb
ThrottleDownload(float limit) limit→max_kbps
Rotate(float anple) angple→degrees_cm

重要な属性を追加する

状況 変数名 改善後
passwordはプレインテキストなので処理をする前に暗号化すべき password plaintext_passwoed
ユーザが入力したcommentは表示する前にスケープする必要がある comment unescaped_comment
htmlの文字コードをUTF-8に変えた html html_utf8
入力されたdataをURLエンコードした data data_urlenc

→セキュリティのバグのような深刻な被害が出るところに使おう!

名前の長さを決める

〜でも短ければ良いわけではない〜
スコープが小さければ短い名前でもOK

if (debug) { 
    map<string,int> m; 
    LookUpNamesNumbers(&m); 
    Print(m); 
} 

※コードを理解するための情報(変数の型・初期値・破棄方法)が近くにあるので、mでも大丈夫

頭文字と省略形

【Good】
evaluation → eval
document   → doc
string     → str
【Bad】
BackEndManager → BEManager

→ん?BE?知らない。これは暗号か?

不要な単語を捨てる

ConvertToString() → ToString()  
DoServeLoop()     → ServeLoop()

誤解されやすい名前

〜「他の意味と間違えられることはないか?」と自問自答する〜

例① filter()

results = Database.all_objects.filter("year <= 2011")

→filterは「選択する」or「除外する」のどちらかわからない
「選択する」:select()
「除外する」:exclude()

例② Clip(text, length)

【Bad】
#textの最後を切り落として、「...」をつける 
def Clip(text, length):
... 

「最後からlength文字を削除する(remove)」
「最大length文字まで切り詰める(truncate)」のどっち?

【Good】
def Truncate(text, max_chars):

ポイント1:関数名をTruncate(text, length)に変更
ポイント2:length を明確にする(今回は「文字列」を意味しているのでmax_chars)

ユーザの期待に合わせる

例. get*()
getで始まるメソッドは「軽量アクセサ」であるという規約

public double getMean() { 
         // 全てのサンプルをイテレートして、total / num_samplesを返す。
}  
   ... 
} 

もしget*()の規約を知らない場合は、コストが高いと知らずに呼び出してしまうかもしれない
→コストの高さが事前にわかるように computeMean() などの名前に変えるべき
(もしくは、コストの高くない実装に変えるべき)

美しさ

①適切な改行
②複数のコードブロックで同じようなことをしていたら、シルエットも同じようにする
③コードの「列」を整理すれば、概要が把握しやすくなる
④ある場所でA・B・Cのように並んでいたものを
他の場所でB・C・Aのように並べてはいけない!
→意味のある順番を選んで、常にその順番を守る
⑤空行を使って大きなブロックを論理的な「段落」に分ける

一貫性のある簡潔な改行位置とクラス

※適切な改行を入れ、繰り返されたコメントは簡潔にする(仮引数を一行で書く)

メソッドを使った整列

〜ヘルパーメソッドを使ってみる〜

【Bad】
DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, "Doug Adams", &error) == "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, " Jake Brown ", &error) == "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection, "No Such Guy", &error) == ""); 
assert(error == "no match found"); 
assert(ExpandFullName(database_connection, "John", &error) == ""); 
assert(error == "more than one result"); 

⬇︎

【Good】
CheckFullName("Doug Adams", "Mr. Douglas Adams", ""); 
CheckFullName(" Jake Brown ", "Mr. Jacob Brown III", ""); 
CheckFullName("No Such Guy", "", "no match found"); 
CheckFullName("John", "", "more than one result"); 
void CheckFullName(string partial_name, 
                   string expected_full_name, 
                   string expected_error) {
// database_connectionはクラスのメンバになっている。 
string error;
string full_name = ExpandFullName(database_connection, partial_name, &error); 
assert(error == expected_error);
assert(full_name == expected_full_name); 
} 

※これによって良い副産物が得られる
・重複を排除したことでコードが簡潔に!
・テストケースの大切な部分(名前やエラー文字列)が見やすくなった
・テストの追加が簡単になった

縦の線を真っ直ぐにする

【Bad】
CheckFullName("Doug Adams", "Mr. Douglas Adams", ""); 
CheckFullName(" Jake Brown ", "Mr. Jacob Brown III", ""); 
CheckFullName("No Such Guy", "", "no match found"); 
CheckFullName("John", "", "more than one result"); 
【Good】
CheckFullName("Doug Adams" , "Mr. Douglas Adams"  , ""); 
CheckFullName("Jake Brown" , "Mr. Jacob Brown III", ""); 
CheckFullName("No Such Guy", ""                   , "no match found"); 
CheckFullName("John"       , ""                   , "more than one result"); 

※縦の線を揃えることで流し読みが楽にできる(「似ているコードは似ているように見せる」)

一貫性と意味のある並び

コードの並びがコードの正しさに影響を及ぼすことは少ない
ただし.... できれば意味のある順番に並べる!

  • 対応するHTMLフォームの<inputフィールド>と同じ並び順にする
  • 「最重要」なものから重要度順に!
  • アルファベット順に並べる

コードを「段落」に分割する

・似ている考えをグループにまとめて、他の考えと分けるため
・視覚的な「踏み石」を提供できる
・段落単位で移動できるようになる

【Bad】
#ユーザのメール帳をインポートして、システムのユーザと照合する。 
#そして、まだ友達になっていないユーザの一覧を表示する。 
def suggest_new_friends(user, email_password):
    friends = user.friends()
    friend_emails = set(f.email for f in friends)
    contacts = import_contacts(user.email, email_password) contact_emails = set(c.email for c in contacts)
    non_friend_emails = contact_emails - friend_emails
    suggested_friends = User.objects.select(email__in=non_friend_emails) 
    display['user'] = user
    display['friends'] = friends
    display['suggested_friends'] = suggested_friends
    return render("suggested_friends.html", display) 
【Good】
def suggest_new_friends(user, email_password): 
    #ユーザの友達のメールアドレスを取得する。 
    friends = user.friends()
    friend_emails = set(f.email for f in friends) 
    
    #ユーザのメールアカウントからすべてのメールアドレスをインポートする。 
    contacts = import_contacts(user.email, email_password)
    contact_emails = set(c.email for c in contacts) 
    #まだ友達になっていないユーザを探す。
    non_friend_emails = contact_emails - friend_emails
    suggested_friends = User.objects.select(email__in=non_friend_emails) 
    #それをページに表示する。
    display['user'] = user
    display['friends'] = friends 
    display['suggested_friends'] = suggested_friends 
    return render("suggested_friends.html", display)

コメントすべきことを知る

〜コメントの目的は、書き手の意図を読み手に知らせることである〜

コメントすべきではないこと

  • コードからすぐに抽出できること
  • ひどいコードを補う「補助的コメント」

→コメントの前にコードを修正!

記録すべき自分の考え

  • なぜコードがこうなっているのか
  • コードの欠陥をTODO:やXXX:などの記法を使って示す
  • 定数の値にまつわる「背景」

読み手の立場になって考える

  • コードを読んだ人が「えっ!?」と思いそうな箇所を予想してコメントをつける
  • 平均的な読み手が驚くような動作は文書化しておく
  • ファイルやクラスには「全体像」のコメントを書く
  • 読み手が細部に囚われないようにコードブロックにコメントをつけて概要をまとめる

コメントのためのコメントをしない!

【Bad】
// 与えられた subtree に含まれる name と depth に合致した Node を見つける。 
Node* FindNodeInSubtree(Node* subtree, string name, int depth); 

※ これは「価値のないコメント」である(関数宣言と同じ)
ひどい名前の場合は、コメントではなく名前を変えよう!

自分の考えを記録する

// このデータだとハッシュテーブルよりもバイナリツールの方が40%速かった
// 左右の比較よりもハッシュの計算コストの方が高いようだ

コードの欠陥にコメントを付ける

// TODO: もっと高速なアルゴリズムを使う
// TODO: JPEG以外のフォーマットに対応する

定数にコメントを付ける

NUM_THREADS = 8  # 値は「>= 2 * num_processors」で十分

→これなら値の決め方がわかる(1 だと小さすぎて、50 だと大きすぎる)。

ハマりそうな罠を告知する

// メールを送信する外部サービスを呼び出している(1分でタイムアウト) 
void SendEmail(string to, string subject, string body); 

→「実装の詳細」についてコメントを書くことで不幸を未然に防ぐ
※この関数の実装では、外部のメールサービスに接続している。その接続には1秒以上かかり、このことを知らないウェブアプリケーション開発者が、HTTPリクエストの処理中に誤ってこの関数を呼び出してしまうかもしれない
(メールサービスが ダウンしていると、ウェブアプリケーションが「ハング」してしまう)

コメントは正確で解決に

〜小さな領域にできるだけ多くの情報を詰め込んだコメントを書く〜

  • 複数のものを指す可能性がある代名詞(それ,これ)を避ける
  • 関数の動作はできるだけ正確に説明する
  • コメントに含める入出力の実例を慎重に選ぶ
  • コードの意図は高レベルで記述する
  • よくわからない引数にはインラインコメントを使う
  • 多くの意味が詰め込まれた言葉や表現を使って、コメントを簡潔に保つ

コメントを簡潔に!

// intはCategoryType。                         
// pairの最初のfloatは’score’。    
// 2つ目は’weight’

↓もっと簡潔にできる!

//CategoryType -> (score, weight)

曖昧な代名詞を避ける

【But】
//データをキャッシュに入れる。ただし、先にそのサイズをチェックする

→ ん?「その」ってどれのこと?データorキャッシュ?

【Good】
//データをキャッシュに入れる。ただし、先にデータのサイズをチェックする
//データが十分に小さければ、それをキャッシュに入れる

歯切れのイイ文章にする

【But】
#これまでにクロールした URL かどうかによって優先度を変える。 
【Good】
#これまでにクロールしていない URL の優先度を高くする。 

→より単純で、短くて、直接的
「クロールしていないURLの優先度が高い」という情報も含まれている

入出力のコーナーケースに実例を使う

〜慎重に選んだ入出力の実例をコメントに書いておけば、それは千の言葉に等しい〜

【But】
// 'src' の先頭や末尾にある 'chars' を除去する。 
String Strip(String src, String chars) { ... } 

→あまり正確ではない
chars は、除去する文字列なのか、順序のない文字集合なのか?
src の末尾に複数の chars があったらどうなるのか?

【Good】
// 実例: Strip(“abba/a/ba”, ”ab”)は”/a/”を返す  
String Strip(String src, String chars) { ... } 

コードの意図を書く

〜コメントとコードとの矛盾や冗長検査の役割を担う〜

【Bad】
// listを逆順にイテレートする
for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend(); 
++it) 
DisplayPrice(it->price); 
...
【Good】
// 値段の高い順に表示する
for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend(); 
++it) 
DisplayPrice(it->price); 
...

→プログラムの動作を高いレベルから説明している

「名前付き引数」コメント

【Bad】
Connect(10, false); 

┗ 数値とブール値が渡されているけどコメントがないので、なんだかよくわからない

【Good】
// 引数にコメントをつけて関数を呼び出す
Connect(/* timeout_ms = */ 10, /* use_encryption = */ false);

→インラインコメントを使って、関数を呼び出す
┗本来は仮引数の名前を timeout_ms に変更すべきだがもし変更できないのであれば
このようにして名前を「改善」できる
【重要】ブール型の引数では、値の前に /* name = */ を置く

情報密度の高い言葉を使う

〜繰り返し登場する問題や解決策を”パターンやイディオムを説明するための言葉”を使うことでコメントを簡潔にする〜
例①

【But】
// このクラスには大量のメンバがある。同じ情報はデータベースにも保管されている。ただし、
// 速度の面からここにも保管しておく。このクラスを読み込むときには、メンバが存在してい 
// るかどうかを先に確認する。もし存在していれば、そのまま返す。存在しなければ、データベー
// スから読み込んで、次回のためにデータをフィールドに保管する。 

⬇︎

【Good】
// このクラスの役割は、データベースのキャッシ層である。

例②

【But】
// 所在地から余分な空白を除去する。それから「Avenue」を「Ave.」にするなどの整形を施す。 
// こうすれば、表記がわずかに違う所在地でも同じものであると判別できる

⬇︎

【Good】
// 所在地を正規化する (例: “Avenue” -> “Ave.”)。
0