1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リーダブルコードを読んでの備忘録

Last updated at Posted at 2024-03-30

はじめに

約3年前の新卒入社した際に本書を買ったが、ずっと埃をかぶっており流石にそろそろ読まねばと危機感を感じたので、重い腰を上げて読んだ証を残していきたいと思います。

私自身の備忘録つもりで書くので、あまり参考にならないかもしれないです。

image.png
https://amzn.asia/d/3oKdoF2

一気に書くとモチベーションが続かないので、少しずつこの記事に追記していきます。

第1章 理解しやすいコード

基本ではあるが、コードを短く書くことよりも「理解するまでにかかる時間を減らす」ことに力を入れることが大切。
自分が気持ちよくなるために書くコードは趣味だけにしておけ。(自戒)

悪い例
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());
良い例
bucket = FindBucket(key);
// 文中では{}を付けていないが、個人的にはなるべく付ける派なので...
if (bucket != NULL) {
    assert(!bucket->IsOccupied());
}

コメントもつけることで、コードは長くなるが「理解するまでにかかる時間」を減らすことができる。

良い例
// "hash = (65599 * hash) + c" の高速版
hash = (hash << 6) + (hash << 16) - hash + c;

第2章 名前に情報を詰め込む

  • 明確な単語を選ぶ
  • tmp や retval などの汎用的な名前を避ける
  • 具体的な名前を使って、物事を詳細に説明する
  • 変数名に大切な情報を追加する
  • スコープの大きな変数には長い名前を付ける
  • 大文字やアンダースコアなどに意味を含める

明確な単語を選ぶ

よく使いがちな「get」は便利でだが、あまり明確な単語ではないので安牌に逃げない。

代替案 使用目的 使用例
Fetch インターネット FetchPage()
Download ファイルなどの取得 DownloadFile()
Choose データを抽出 ChooseItem()
Select DBからデータを抽出 SelectItem()

BinaryTree クラスでの例。

悪い例
class BinaryTree {
    int Size(); // ← ツリーの高さ?ノード数?メモリ消費量?
    ...
}
良い例
class BinaryTree {
    int Height();      // ツリーの高さ
    int NumNodes();    // ノード数
    int MemoryBytes(); // メモリ消費量
    ...
}

かと言って、やりすぎには気を付ける。
本書で紹介されている「カラフル」な代替案の引用。

単語 代替案
send deliver, dispatch, announce, distribute, route
find search, extract, locate, recover
start launch, create, begin, open
make create, ser up, build, generate, compose, add, new

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

悪い例
var euclidean_norm = function(v) {
    var retval = 0.0; // ← 戻り値という情報以外何もない
    for (var i = 0; i < v.length; i++)  {
        retval += v[i] * v[i];
    }
    return retval;
}
良い例
var euclidean_norm = function(v) {
    var sum_squares = 0.0; // 2乗の合計と分かる
    for (var i = 0; i < v.length; i++)  {
        sum_squares += v[i] * v[i];
        // sum_squares += v[i]; ← 2乗されていないのでバグ!
    }
    return retval;
}

tmp

情報の一時的な保管や、生存期間が短い時にだけ使うようにする。

問題のない使い方
if (right < left) {
    tmp = right;
    right = left;
    left = tmp;
}

下記の tmp の使い方を見ると、tmp の意図が正確に読み取れないので怠慢。

ダメな例(1)
String tmp = user.name();
tmp += " " + user.phone_number();
tmp += " " + user.email();
...
template.set("user_info", tmp);
ダメな例(2)
tmp = tempfile.NamedTemporaryFile();
...
SaveData(tmp, ...);

tmp の変数名から「ユーザー情報」「tempファイル」であることが読み取りにくい。
なので、下記のように書き換えるとより理解しやすくなる。

書き換え例(1)
String user_info = user.name();
user_info += " " + user.phone_number();
user_info += " " + user.email();
...
template.set("user_info", tmp);
書き換え例(2)
tmp_file = tempfile.NamedTemporaryFile();
...
SaveData(tmp_file, ...);

ループイテレータ

i, j, k, iter などは、インデックスやループイテレータでよく使うが、より良く名前を付けることができる。

一般的なループイテレータ
for (let i = 0; i < clubs.size; i++) {
  for (let j = 0; j < clubs[i].members.size; j++) {
    for (let k = 0; k < users.size; k++) {
      if(clubs[i].members[k] === users[j]){
...

これだと、ネストが深くなった際に追いにくいので、

  • club_i, members_i, users_i
  • ci, mi, ui

とするとバグが目立ちやすくなる。
(個人的には後者の表現が好みかも)

より良い表現方法
if (clubs[ci].members[ui] == users[mi]) // ← ui と mi が逆になっている!
if (clubs[ci].members[mi] == users[ui]) // ← OK!

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

例:--run_locally

このコマンドオプションだと、具体性がないので「ローカルで実行する」ということしか分からない。
そのため、下記のようにもっと具体性を持たせると良くなる。

使用用途
--use_local_database ローカルのデータベースを使用する
--extra_logging ログを出力する
--dry_run お試し実行

特に「--dry_run」は、Jenkins で Jenkinsfile を更新した際に、一度このオプションを付けてジョブを実行することで、処理は実行せず設定だけ反映させる 用途としてよく使用している。

名前に情報を追加する

id などは、基本的に10進数表記が基本的であるので、16進数で指定したい場合には明示的にする必要がある。

16進数であることを明示する
string id;     // 誰も16進数であると分からない
string hex_id; // これなら誰でも理解できる

値の単位

時間やバイト数などのものは、明示的に単位を付けるとより分かりやすくなる。

ミリ秒を想定している場合
var start_ms = (new Data()).getTime();
...
var elapsed_ms = (new Data()).getTime() - start_ms;
document.writeln("読み込み時間:" + elapsed_ms / 1000 + "");

単位の例

一般的な名前 単位を追加した名前
delay delay_sec
size size_mb
limit max_kbps
angle degrees_cw

その他の重要な属性を追加する

単位以外にも、明示的に安全ではないデータなどを示すために「untrustedUrl」「unsafeMessageBody」などの変数名を使用すると良い。

変数名 改善後 状況
password plaintext_password password が平文なので、暗号化が必要である
comment unescaped_comment 入力された comment は表示する前にエスケープが必要である
html html_utf8 html の文字コードを utf-8 に変えた
data data_urlenc data を URLエンコードした

名前の長さを決める

d など短すぎる名前も良くないが、かと言って下記のような長すぎる名前も避けたい。

長すぎるクラス名
class NavigationControllerWrappingViewControllerForDataSourceOfExtendClassFromBaseClass {
}

スコープが小さければ短い名前でもいい

許される短い変数名
if (debug) {
    map<string, int> m;
    LookUpNamesNumbers(&m);
    Print(m);
}
許しがたい短い変数名
class XXX {
private:
    map<string, int> m;
...
============================
...
// 間に長い行数あると、m の型や目的が把握しずらく読みにくい。
    LookUpNamesNumbers(&m);
    Print(m);
}

第3章 誤解されない名前

例:filter()

filter() だと「選択」するか「除外」するかで解釈が分かれるので、下記のように使い分けるのが良さそう。

目的 関数名
選択する select()
除外する exclude()
選択するか除外するか解釈が分かれないようにする
results = Database.all_objects.select("year <= 2011");
results = Database.all_objects.exclude("year <= 2011");

例:Clip(text, length)

Clip() も「削除」するか「切り詰める」かで2通りの解釈がされてしまうので、下記のように使い分けるのが良さそう。

目的 関数名
削除する remove()
切り詰める truncate()
これも期待する動作の解釈が分かれる
def RemoveText(text, length):
    # 最後から length 文字削除する処理

def Truncate(text, length):
    # 最大 length 文字切り詰める処理

また、length の引数名も下記のように改善できる。

目的 関数名
最大バイト数 max_bytes
最大文字数 max_chars
最大単語数 max_words

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

限界値を含める場合には、接頭辞に max_min_ をつけるようにすると良さそう。

接頭辞に max_ や min_ をつける
MAX_ITEMS_IN_CART = 10

if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
    ...

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

範囲指定する際には first, last のようにすると解釈が分かれにくい。

first, lastを使う
print insteger_range(first=2, last=4)

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

first, last と似ているが、包含/排他的にされた範囲指定されたものには、begin, end を使うと良さそう。

begin, endを使う
PrintEventsInRange(beginDate="OCT 16 12:00AM", endDate="OCT 17 12:00AM")

ブール値の名前

  • 接頭辞には is, has, can, should などを付けた方が良い。
  • 変数の名前を 否定形 にするのは避けるべきである。
変数名に否定形を使うべきではない
bool disable_ssl = false;
bool use_ssl = true; // どちらも同じ意味だがこちらの方が分かりやすい

ユーザの期待に合わせる

例:get*()

get系の関数は値を返すだけの「軽量アクセサ」である認識が強い。
そのため、二乗平均平方根など計算負荷の高い関数に getRMS() などを付けると良くないので、computeRMS() などの名前に変更するべきである。

RMS = \sqrt{ \frac{1}{n} \sum_{i=0}^{n-1}x_i^2}
コストの高い関数にgetは付けない
public double computeRMS() {
    // コストのかかる計算処理をする
}

第4章 美しさ

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

横に長すぎるのは美しくないので、改行位置を統一させる。

public class PerformanceTester {
    public static final TcpConnectionSimulator wifi =
        new TcpConnectionSimulator(
            500,   /* Kbps */
            80,    /* millisecs latency */);

    public static final TcpConnectionSimulator t3_fiber =
        new TcpConnectionSimulator(
            45000, /* Kbps */
            10,    /* millisecs latency */);
    ...
}

メソッドを使った整列

1行が長くならないように、ヘルパー関数で分割する。

変更前
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"));
変更後
CheckFullName("Doug Adams", "Mr. Douglas Adams", "")
CheckFullName("Jake Brown", "Mr. Jacob Brown III", "")

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

縦の線をまっすぐにする

流し読みやタイポが見つけやすくなる。

CheckFullName("Doug Adams"  , "Mr. Douglas Adams" , "");
CheckFullName(" Jake Brown ", "Mr. Jake Brown III", "");
CheckFullName("John"        , ""                  , "more than one result");

location = request.POST.get("location");
phone    = equest.POST.get("phone"); // ← タイポしているのが見つけやすい
url      = request.POST.get("url");

command[] = {
    ...
    { "timestamping", &opt.timestamping, cmd_boolean },
    { "tries",        &opt.ntry,         cmd_number_inf },
    { "useragent",    NULL,              cmd_spec_useragent },
    ...
}

一貫性と意味のある並び

  • ある場所で「A, B, C」と並んでいたら、他の場所でも「A, B, C」の並びにする
  • 「最重要」なものから重要度順に並べる
  • アルファベット順に並べる

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

コメント行や空行をいれることで見やすくなる。

変更前
class FrontendServer {
    public:
        FrontendServer();
        void ViewProfile(HttpRequest* request);
        void CloseDatabase(string location);
        void SaveProfile(HttpRequest* request);
        ~FrontendServer();
        void OpenDatabase(string location, string user);
}
変更後

class FrontendServer {
    public:
        FrontendServer();
        ~FrontendServer();

        // ハンドラ
        void ViewProfile(HttpRequest* request);
        void SaveProfile(HttpRequest* request);

        // データベースのヘルパー
        void OpenDatabase(string location, string user);
        void CloseDatabase(string location);
}

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

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

コードからわかることをコメントに書かない。

// Account クラスの定義
class Account {
    public:
        // コンストラクタ
        Accout()
        
        // profitに新しい値を設定する
        void SetProfit(double profit);
...

ただ、一目見て分かりずらいものにコメントを付けるのはあり。

# 2番目の'*'以降をすべて削除する
name = '*'.join(line.split('*')[:2])

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

下手に最適化しようとして無駄に時間を使うなら、コメントを付けることで時間を使う必要がなくなる。

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

// ヒューリスティックだと単語が漏れることがあるが仕方ない。100%は難しい。

// このクラスは汚くなってきている。
// サブクラス 'ResourceNode' を作って整理したほうがいいかもしれない。

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

欠陥があるコードには、以下のような記法でコメントを付けると良い。

記法 典型的な意味
TODO: あとで手を付ける
FIXME: 既知の不具合があるコード
HACK: あまりキレイじゃない解決策
XXX: 危険!大きな問題がある
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?