リーダブルコード読んだ+我流のリーダブルコードを考えた

Edit

はじめに

リーダブルコードを読んだ上で自分なりにリーダブルなコードを再定義して、そのコードを書くためのTipsをこの記事で紹介しようと思います。

リーダブルコードを読んだ

リーダコードの目次

オライリーのリーダブルコードの紹介サイト から目次を引用して、見易く加工しました。


  • はじめに「理解しやすいコード」
    • 「優れた」コードって何?
    • 読みやすさの基本定理
    • 小さなことは絶対にいいこと?
    • 「理解するまでにかかる時間」は競合する?
    • でもやるんだよ
  • 第1部 表面上の改善
    • 名前に情報を詰め込む
    • 誤解されない名前
    • 美しさ
    • コメントすべきことを知る
    • コメントは正確で簡潔に
  • 第2部 ループとロジックの単純化
    • 制御フローを読みやすくする
    • 巨大な式を分割する
    • 変数と読みやすさ
  • 第3部 コードの再構成
    • 無関係の下位問題を抽出する
    • 一度に1つのことを
    • コードに思いを込める
    • 短いコードを書く
  • 第4部 選抜テーマ
    • テストと読みやすさ
    • 「分/時間カウンタ」を設計・実装する

感想

風刺画が面白かった上に、的をついていたと思います。一人が数年程度のプログラミングの経験で得られるものではないことが本に書いてあるという印象を受けました(詳しくは実際に読んでください)。

ツイッター上で、「リーダブルコードは必ず読むべき本というものではない」と主張している人を何人か見かけたことがありましたが、理解できる気がします。結局は我流のリーダブルコードに走るのがオチということが多いです。200ページ以上ある本の内容をコード書くときに、いちいち思い出したりするのは人間の脳に合わないので、その本の内容に似ていてかつ、規模が縮小化したマイルールで書いていくしかないです。我流のリーダブルなコードを書いた上でLinterやPrettierを入れておくのがいい気がします。

ということで我流のリーダブルコードを定義します。

自分なりのリーダブルコードを考える

コードのリーダビリティ(読みやすさ)の定義をする

自分(Yoshisaur)の中では「(未来の自分を含む)誰かが初めてコードを読むとき、クラス・関数の構造や機能を理解するまでの平均的な時間が短ければ短いほど、そのコードはリーダブルである(=読みやすさの度合いが高い)」と定めています。

また、自分の中では「コードのリーダビリティを決める要因のスコープはコード本体と、ドキュメント、テストコード、ディレクトリ構造*1、コミットメッセージである」と定めています。

*1 今回の記事では、ディレクトリ構造についての詳細な内容は記述はしないです。


「初見でコードを完読するまでの時間が短くなる要素」を考える

自分は以下の要素が、読み手の能力以外の「初見でコードを完読するまでの時間が短くなる要素」であると定義しています。

  • 命名規則が統一されている
  • コメントしなくていいところはしない
  • 単語の具体度が高い
  • 変数名の単語数が、その変数の出現行数に比例している
  • if文の条件式で真偽値以外の演算が行われていない、かつ、比較する2つの対象の並びが統一している
  • for文の(i, j, kで知られる)イテレータが、多重しない場合を除いて繰り返し回数を定義する配列の名前の頭文字を含んでいる
  • 関数名が動詞で始まっている
  • 関数に引数としてリテラルを渡さない
  • クラス名が名詞で終わっている
  • ドキュメントの中で関数の引数と戻り値の型が記述されている
  • テストコードのエラー文に、変数に格納されている具体的な値が出力されている
  • 行数が少ない
  • コミットメッセージが統一されている

後述する内容は各要素に関しての説明です。


命名規則が統一されている

読みやすいコードは命名規則に従って変数、関数、クラスを命名しています。

わかる範囲で「対象」の内容を記述しています。

命名規則(Case) 対象
スネークケース foo_bar Python, Rubyの変数や関数
キャメルケース fooBar Java, JavaScriptの変数や関数, PHPの関数
パスカルケース FooBar Java, Python, Ruby, PHPのクラス
ケバブケース foo-bar HTML, CSS
アッパーケース FOO_BAR グローバル変数、定数

コメントしなくていいところはしない

以下のようにコメントを書いてはいけません。

/* calculate the area */
int area = width * height;

演算内容がコードを見てわかるならコメントをしないようにしましょう。


単語の具体度が高い

変数、関数、クラスなどの命名には抽象的な単語ではなく具体的な単語を使うといいです。

変数に関して具体的な命名の仕方の例では、「格納する値にpxなどという単位が存在するとき、~_pxのように命名する」などが挙げられます。


変数名の単語数が、その変数の出現行数に比例している

変数名はスコープが一画面内に収まるのであれば短くていいですが、数画面の範囲に広がるのであれば変数名を具体的であるべきです。


if文の条件式で真偽値以外の演算が行われていない、かつ、比較する2つの対象の並びが統一している

以下のようにif文の条件式の中で真偽値以外の演算を行わないでください。

if(sum(distance_km_list) > 100){
    printf("The total distances is over 100km");
}

以下のように先に変数に演算結果を代入した後に比較してください。

int total_distance_km = sum(distance_km_list);

if(total_distance_km > 100){
    printf("The total distances is over 100km");
}

if文の条件式で比較する2つの対象は左が変数、右が定数、リテラルになるように統一してください。

if(total_distance_km > 100){
    printf("The total distances is over 100km");
}

if文の条件式の比較対象の並び

変数 定数、リテラル

for文の(i, j, kで知られる)イテレータが、多重しない場合を除いて繰り返し回数を定義する配列の名前の頭文字を含んでいる

好みの問題でもありますが、多重for文を書くときに、イテレータを<配列/リストの頭文字>_iのようにすると読みやすくなることもあります。

for (int g_i = 0; g_i < sizeof(group_array); g_i += 1){
    for(int m_i = 0; m_i < sizeof(group_array[g_i].member_array); m_i += 1){
        sprintf(group_index_str, "%d", g_i + 1);
        sprintf(member_index_str, "%d", m_i + 1);
        printf("The " + member_index_str + "th member of the " + group_index_str + "th group is " + group_array[g_i].member_array[m_i]).name;
    }
}

※ 例に上げたコードは読みづらいかもしれないです…。


関数名が動詞で始まっている

関数名は動詞で始まります。

関数名に使う英語の動詞は具体的であるほうが好ましいですが、類義語の使い分けが難しいので表を用意しました。


  • 作成する
  • 変更する
  • 取得する
  • 保存する
  • 追加する
  • 削除する
  • 開始/実行する
  • 停止する
  • 検索/選択する
  • 検証する

作成する

単語 意味 用途
make (一時的に)作成する 一時的に使用する変数を作るときなど
generate (プログラムに基づいて)生成する 文字列、画像、パスワードを生成するときなど
create (人為的に)新しく作成する ユーザを作成するとき、データベースのテーブルまたはデータベース自体を作成するときなど
build (変換などをしてファイルなどを新しく)作成する コンパイル、パッケージ化、インストーラ/データベースのスキーマの作成をするときなど

変更する

単語 意味 用途
update 更新する 時間、バージョンなどの値を変更するとき、データーベースのレコードを変更するときなど
edit 編集する データベースのカラム単位の変更をするときなど
change 変更する (古い値を破棄して)値を全体的に変更するときなど
modify 変更する 値を一時的に変更するときなど

取得する

単語 意味 用途
get 取得する 計算量O(1)で値を取得するとき
load 読み込む メモリ、レジスタ、フォームなどの、読み込んだデータを保存する先が明示されているときなど
read 読み込む ファイル、Streamなどのデータを読み込む元の存在が明示されているときなど
retrieve 取り出す インデックスやURLを辿ってデータを取得するときなど
fetch 取得する サーバ、データベースからデータを取得するときなど

保存する

単語 意味 用途
save 保存する オブジェクトやファイルにデータを保存するときなど
store 保存する ストレージやデータベースに保存するときなど
write 書き込む ファイル、ストリームに一行単位で書き込みを行うときなど

追加する

単語 意味 用途
put 追加する 連想配列にキーを追加するとき、ストリームへ文字列を出力するときなど
insert 挿入する 配列/リストの特定の位置にデータを追加するとき、データベースにレコードを追加するときなど
append 追加する 配列/リストの末尾にデータを追加するときなど
prepend 追加する 配列/リストの先頭にデータを追加するときなど
register 登録する イベントハンドラを追加するときなど

削除する

単語 意味 用途
delete 削除する 復元できないようにデータを削除するときなど
remove 取り除く 閲覧/アクセスができる場所からデータを取り除くときなど
erase 消す メモリの内容を消すときなど
clear 空にする オブジェクトや変数の中身を空にしたいときなど

実行する

単語 意味 用途
execute 実行する 短期的なスクリプト/アプリケーションを実行するときなど
run 走らせる 一定期間継続して実行されるスクリプト/アプリケーションを実行するときなど

停止する

単語 意味 用途
abort 中断する 異常、障害によってスクリプト/アプリケーションを中断するときなど
pause 一時停止する 再開することを前提にスクリプト/アプリケーションを一時停止するときなど
suspend 保留する 条件が揃うまでスクリプト/アプリケーションの実験を中断して再開を延期するときなど
kill 強制終了する プロセスを強制終了するときなど

検索/選択する

単語 意味 用途
find 見つける 存在することが前提されているデータを探し出すときなど
search 検索する 存在しないかもしれないデータを検索するときなど
select 選択する 条件を満たすものを選択してフィルターするときなど
exclude 除外する 条件を満たすものを除外してフィルターするときなど

検証する

単語 意味 用途
is ~である オブジェクト、変数がある状態であるか調べるときなど
has ~を持っている オブジェクトが特定のプロパティを持っているか調べるときなど

関数に引数としてリテラルを渡さない

関数に引数としてリテラルを渡してはいけません。

↓ Pythonの悪い例

with open('~/sandbox/python_script/hoge.py') as f:
    print(f.read())

↓ 改善例

file_path = '~/sandbox/python_script/hoge.py'
with open(file_path) as f:
    print(f.read())

クラス名が名詞で終わっている

クラス名が名詞で終わっていなければいけません。

例は割愛します。


ドキュメントの中で関数の引数と戻り値の型が記述されている

動的型付け言語の場合は特にドキュメントの引数と戻り値の型を記述している必要があります。


テストコードのエラー文に、変数に格納されている具体的な値が出力されている

テストコードのエラーメッセージはテストに失敗する理由を説明するために、変数に格納されている値を出力する必要があります。

↓ Golangの悪い例

if result != want {
    t.Errorf("The actual result is not the expected result.")
}

↓ 改善例

if result != want {
    t.Errorf("fileWrite.Hello() = %q want %q", result, want)
}

行数が少ない

上記の要素を守っていている限りコードはなるべく短くべきです。


コミットメッセージが統一されている

コミットメッセージは規約に則って統一されていると、コードのリーダビリティが上がります。

コミットメッセージの規約について、参考になる Conventional Commits に記述されている内容を紹介します。

コミットメッセージは本文とフッターを含まない場合、以下のような構成にするといいです。

<type>(optional scope): <description>

日本語に訳すと以下のようになります。

<型>(任意 スコープ): <説明>

ちなみに<説明>は英語の動詞(原形)から始まると良いとされています。


type(型)のテーブルは以下の通りです。

説明
fix バグの修正に関する変更
feat 新しい機能の追加
build ビルド
chore 雑事(カテゴライズする必要がないもの)
ci CI
docs ドキュメントの追加、変更
style コードスタイルの整形に関する変更
refactor リファクタリングに関する変更
perf パフォーマンスの向上に関する変更
test テストに関する変更

コミットメッセージの例は以下のようになります。

feat(api): respond with a json object when a http request is received

コミットする変更内容が以前のバージョンとの互換性を破るようでしたら、メッセージに「!」を付けるといいとされています。

feat(api)!: respond with a json object when a http request is received
comments powered by Disqus