Loading...
snrk tech blog.

【要約】リーダブルコード 表面上の改善

はじめに

初心に戻ってリーダブルコードを読んでみました。
https://www.oreilly.co.jp/books/9784873115658/

本シリーズのゴール

  • よいコード(読みやすいコード)を書けるようになる
  • レビューの質を上げる

自分のための備忘録的な目的がメインですが、皆さんの参考にもなれば幸いです。
この記事では「I. 表面上の改善」についてまとめます。


I. 表面上の改善

2. 名前に情報を詰め込む

2.1 明確な単語を選ぶ

  • 複数の意味に取れる単語は使わない
    • 例)getは取ってくる場所に合わせてfetchdownloadなどにする

2.2 tmpやretValなどの汎用的な名前を避ける

  • 基本的には汎用的な名前は使わず、エンティティの値や目的を表した名前を選ぶ
  • 一次的な保管かつ生存期間がわずか数行の変数であれば、tmpなども利用可
  • ループイテレータにijkを利用するのは、イテレータであることを表せるので有効
    • イテレータが複数ある場合は、members_iusers_iのように接頭辞をつけるとベター

2.3 抽象的な名前よりも具体的な名前を使う

  • 全ての機能を表す具体的な名前が思いつかない場合、機能を分割することを検討する

2.4 名前に条件を追加する

  • 値の単位を名前に含める
    • 例)start_time_ms, size_mb
  • その他の重要な属性を追加する
    • バグ(セキュリティに関するもの)になりそうなところだけでOK
      • 例)unescaped_comment, plaintext_password, html_utf8

2.5 名前の長さを決める

  • 長い名前を避ける
    • 例)newNavigationControllerWrappingViewControllerForDataSourceOfClass
  • スコープが小さければ短い名前でもいい
  • 長い名前を入力するのは問題じゃない
    • テキストエディタには補完機能があるので、「入力しにくい」は長い名前を避ける理由にはならない
  • 頭文字と省略形
    • プロジェクト固有の省略形は禁止
    • 一般的に広く使われる省略形はOK
      • 例)string -> str
  • 不要な単語を投げ捨てる
    • 削除しても情報が全く損なわれない場合は削除OK
      • 例)ConvertToString() -> ToString()

2.6 名前のフォーマットで情報を伝える

  • 大文字やアンダースコアなどに意味を含める
    • 例)
    • クラス名:CamelCase(キャメルケース)
    • 変数名:lower_separated(小文字をアンダースコアで区切ったもの)

3. 誤解されない名前

3.1 例: filter()

  • filterの結果、「選択する」のか「除外する」のか不明瞭
    • 「選択する」ならselect()
    • 「除外する」ならexclude()

3.2 例: Clip(text, length)

Bad Example
# textの最後を切り落として、「...」をつける
def Clip(text, length):
  ...
  • 動作としてどちらを行うのか関数名からは不明瞭
    • 最後からlength文字を削除する(Remove
    • 最大length文字まで切り詰める(Trancate)←今回はこっち
  • lengthという引数名もmax_lengthにした方が明確
    • さらに長さは「文字数」を表していることがわかるようにmax_charsだとベター

3.3 限界値を含めるときはminとmaxを使う

Bad Example
# ショッピングカードには商品が10点までしか入らない
CART_TOO_BIG_LIMIT = 10

if shopping_cart.num_itms() >= CART_TOO_BIG_LIMIT:
  Error("カートにある商品数が多すぎます。")
  • CART_TOO_BIG_LIMIT
    • 条件にこの値を含めるべきか(「未満」か「以下」か)わからない
    • 結果、上記コードでは「off-by-oneエラー[1]」のバグが発生している
    • MAX_ITEMS_IN_CARTならその値を含めるべきことが明確

3.4 範囲を指定するときはfirstとlastを使う

Bad Example
print integer_range(start=2, stop=4)
  • stop
    • 範囲の対象なのか、非対象なのかわからない
    • lastを使えば対象であることが明確

3.5 包含/排他的範囲にはbeginとendを使う

包含/排他的な範囲とは

例)10/16に開催されたイベントを全て出力したい

Bad Example
PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.999pm")

↓ 包含/排他的範囲を使うと簡潔に書ける

# 第2引数の時刻は「含まない」→ 排他的
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")
  • 包含/排他的範囲にはbeginendを使うことが多い
    • endは曖昧な気もするが、英語に「ちょうど最後の値を超えたところ」を意味する単語がない・・・
    • ただプログラミングでは排他的範囲の終端をendと表すことが慣習となっている

3.6 ブール値の名前

Bad Example
bool read_password = true
  • パスワードを「これから読み取る必要がある」のか「すでに読み取っている」のか不明瞭
    • ブール値の変数名にはishascanshouldなどをつけることが多い
Bad Example
bool disable_ssl = true
  • 名前を否定系にするのも理解を難しくするので避ける
    • 上記の例だと値としてはtrueだが、sslは無効を表す
    • 変数の値と表す状態がになっている

3.7 ユーザの期待に合わせる

  • 例: get*()
    • getで始まるメソッドはメンバの値を返すだけの「軽量アクセッサ」であるという規約に慣れ親しんでいる
    • 「負けを認めて」getを他の単語に変えた方がいい

3.8 例: 複数の名前を検討する

  • 他の人がどう誤解する可能性があるかを検討の軸にする
  • その単語だけでは誤解を招く可能性がある場合は、補足の単語を追加する
    • 例)inherit_fromという名前は何かを継承することはわかるが、実際何を値として与えれば良いか不明瞭
    • inherit_from_experiment_idとすれば、継承したいexperiment_idを与えれば良いとわかる

4. 美しさ

3つの原則

  • 読み手が慣れているパターンと一貫性のあるレイアウトを使う
  • 似ているコードは似ているように見せる
  • 関連するコードをまとめてブロックにする

4.1 なぜ美しさが大切なのか?

  • プログラミングの時間のほとんどはコードを読む時間

4.2 一貫性のある簡潔な改行位置

  • 内容が似ているコードでも「シルエット」が異なるとそればかり目立ってしまう

4.3 メソッドを使った整列

  • 何度も同じ処理が出てきたらメソッドにする
    • 「面倒な仕事」が全てメソッドに隠蔽される
    • = 読みやすくなる

4.4 縦の線をまっすぐにする

Good Example
CheckFullName("Doug Adams"  , "Mr. Douglas Adams" , "");
CheckFullName(" Jake Brown ", "Mr. Jake Brown III", "");
CheckFullName("No Such Guy" , ""                  , "no match found");
CheckFullName("John"        , ""                  , "more then one result");
  • メリット
    • 関係性を理解しやすい
  • デメリット
    • 手間がかかる
    • 1行だけ変更したいのに、他の行も変更しなければならない(差分が増える)

4.5 一貫性と意味のある並び

  • 本来順番に意味のない複数のコードは、意味のある順番に並べるといい
  • どんな並び順でも良いが、一連のコード内では同じ並び順を使うべき
    • 例)対応するHTMLフォームの<input>フィールドと同じ並び順にする
    • 例)「最重要」なものから重要度順に並べる
    • 例)アルファベット順に並べる

4.6 宣言をブロックにまとめる

  • 複数の宣言文を役割ごとにブロックにする(空白行を間に入れる)
    • 例)役割
      • ハンドラ
      • リクエストとリプライのユーティリティ
      • データベースのヘルパー

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

  • 文章を段落に分割するメリット
    • 似ている考えをグループにまとめて、他の考えと分ける
    • 視覚的な「踏み石」を提供できる
    • 段落単位で移動できるようになる
  • 基本的にはコードも文章と同じ

4.8 個人的な好みと一貫性

  • 最終的には個人の好み
    • 例)クラス定義の開き括弧の位置

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

5.1 コメントするべきでは「ない」こと

  • コードから「すぐに」わかることをコメントしない
    • 新しい情報を提供していなくても、コードを読むよりコメントを読んだ方が理解が早くなるならOK
  • ひどい名前はコメントをつける前に名前を変える

5.2 自分の考えを記録する

「監督のコメンタリー」を入れる

Good Example
// このデータだとハッシュテーブルよりもバイナリツリーの方が40%速かった。
// 左右の比較よりもハッシュの計算コストの方が高いようだ。
  • 下手に最適化しようとして無駄に時間を使う必要がなくなる
Good Example
// ヒューリスティックだと単語が漏れることがあるが仕方ない。100%は難しい。
  • 失敗するテストケースに無駄な時間を使う必要がなくなる
Good Example
// このクラスは汚くなってきている。
// サブクラス'ResourceNode'を作って整理した方がいいかもしれない。
  • コードが汚いことを認め、誰かに修正を促している

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

プログラマがよく使う記法
// TODO: あとで手をつけることを記述
// FIXME: 既知の不具合があるコードに記述
// HACK: あまりキレイじゃない解決策に記述
// XXX: 危険!大きな問題がある場合に記述

定数にコメントをつける

  • 定数を設定するとき、なぜその値を設定したのか背景があればコメントで記述する

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

質問されそうなことを想像する

  • 単純な方法以外で実装している場合に有効

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

Good Example
# 実行時間はO(タグの数*タグの深さの平均)なので、ネストの深さに気を付ける
def FixBrokenHtml(html): ...
  • ネストの深いHTMLに利用して、パフォーマンスが低下するリスクを使う「前」に告知する

「全体像」のコメント

Good Example
// これはビジネスロジックとデータベースをつなぐグルーコードです。アプリケーションから直接使ってはいけません
  • 新しくチームに参加した人に向けて書く(意識)
  • 短い文章でもOK

要約コメント

  • 関数の内部でも「全体像」についてコメントするのは有効
  • 関数内の大きな「塊」に要約コメントをつけるのもいい
    • 塊を関数に分割できるならその方がいい

5.4 ライターズブロックを乗り越える

  1. 頭の中にあるコメントをとにかく書き出す
  2. コメントを読んで(どちらかといえば)改善が必要なものを見つける
  3. 改善する

6. コメントは正確で簡潔に

6.1 コメントを簡潔にしておく

  • 例)3行のコメントを1行にまとめられるなら1行に

6.2 あいまいな代名詞を避ける

  • 代名詞は物事を複雑にする
  • 下の例で「その」が指しているのは、「データ」?「キャッシュ」?
Bad Example
// データをキャッシュに入れる。ただし、先にそのサイズをチェックする。

6.3 歯切れの悪い文章を磨く

Bad Example
// これまでにクロールしたURLかどうかによって優先度を変える。
  • クロールした/しない場合にそれぞれどう優先度が変わるのか不明瞭
Good Example
// これまでにクロールしていないURLの優先度を高くする。

6.4 関数の動作を正確に記述する

Bad Example
// このファイルに含まれる行数を返す
int CountLines(string filename) {...}
  • 「行」には複数の意味がある
    • 空のファイルは0行なのか1行なのか
    • "hello"は0行なのか1行なのか
    • "hello\n"は1行なのか2行なのか
    • などなど
  • 「行」の意味を特定する
Good Example
// このファイルに含まれる改行文字('\n')を数える
int CountLines(string filename) {...}

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

  • 役に立つ実例
    • 複雑なケース
    • エッジ(境界)ケースを示す
    • 結果がソートされるのか/されないのかわかる入力を使う

6.6 コードの意図を書く

Bad Example
// listを逆順にイテレートする
  • 上のコメントより下のコメントの方が優れている
Good Example
// 値段の高い順に表示する

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

Bad Example
Connect(10, false)
  • 引数が何を表しているのかわからない
  • 引数を名前付きで渡せる言語
    • 例)Python
Good Example(Python)
Connect(timeout = 10, use_encryption = False)
  • 引数を名前付きで渡せない言語
    • 引数にインラインコメントを使えば同じことが可能
    • 例)C++/Java
Good Example(Java)
Connect(/* timeout = */ 10, /* use_encryption = */ False)

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

  • 業界に馴染み深い言葉を積極的に使えばコメントが簡潔になる
      • キャッシュ層
      • 正規化
      • ヒューリスティック
      • ブルートフォース
      • ナイーブソリューション

つづく

この記事では「I. 表面上の改善」についてまとめました。
表面上の改善は難易度が低いですが、取り入れればすぐに効果を実感できると思います。

脚注
  1. 境界条件の判定に関するエラー ↩︎

snrk
snrk
美容系メディアを運営する会社でエンジニアをしています。コーヒーと猫が好きです。