📌

リーダブルコードを読んで実践してること

に公開

リーダブルコード最高!

プロジェクトの長期的な成功には、単に機能するコードを書くだけでなく、他の開発者が容易に理解し、修正できるコードを書く能力が不可欠です。

リーダブルコードの全ての原則を完全に実践するには、まだまだ努力が必要ですが、自分が日々意識して取り組んでいる具体的な方法をいくつか紹介します。

変数に単位を加える

変数名に単位を含めることで、その数値が具体的に何を意味するのかが一目瞭然になります。
間違った単位での計算や不適切なAPI引数の使用を防ぐことができます。

特に、時間のように秒、ミリ秒、マイクロ秒など複数の単位が存在するケースでは、単位の明示が混乱を避け、バグの発生リスクを低減する上で非常に役立ちます。

Before

# Before: 単位がないと、time.sleep()の引数が何秒か分かりづらい
start_time = time.time()

time.sleep(1) # 1秒? 1ミリ秒?

end_time = time.time()

print(f"経過時間: {end_time - start_time} 秒")

After

# After: 変数名に単位(second)をつけると、time.sleep()の引数が1秒だとすぐにわかる
start_time_sec = time.time()

SLEEP_TIME_SEC = 1
time.sleep(SLEEP_TIME_SEC)

end_time_sec = time.time()

print(f"経過時間: {end_time_sec - start_time_sec} 秒")

位置をそろえる

変数の定義において代入演算子の位置を揃えることで、視覚的な整列を生み出し、コードの構造を理解しやすくしてくれます。

特に、関連する変数を一つのグループとしてまとめ、その中で整理することで、各変数の識別が容易になり、素早くコードを読むことが可能になります。

Before

first_name = "Taro"
last_name = "Yamada"
age = 30
is_student = True

user_id = "user123"
user_name = "Taro Yamada"
email = "taro.yamada@example.com"
registration_date = "2023-01-01"
last_login = "2023-09-18"

After

first_name = "Taro"
last_name  = "Yamada"
age        = 30
is_student = True

user_id           = "user123"
user_name         = "Taro Yamada"
email             = "taro.yamada@example.com"
registration_date = "2023-01-01"
last_login        = "2023-09-18"

コードの欠点を指摘しておく

コードの実装において、現時点で認識されている欠点や将来的な改善点がある場合、それらをコメントとして明示的に残すと便利です。

これにより、後からそのコードを読み返す開発者(あるいは未来の自分)は、実装の背景にある制約や意図を理解しやすくなります。

事前に問題を指摘しておくことで、予期せぬ挙動や設計上のトレードオフに関する疑問が解消され、無駄な調査時間を削減し、保守作業の効率を高めることができます。

def process_data(data):
    # この関数は、データ処理ロジックを実装していますが、
    # 欠損値の扱いやエラーハンドリングが不十分な可能性があります。
    # 例外処理が足りてないかも
    ...

処理の要約をコメントしておく

複数のステップからなる複雑な関数や長い関数においては、主要な処理ブロックごとにその目的を要約したコメントを付加すること見やすいです。

これにより、コードの全体像を素早く把握できるようになり、特定の機能を探す際に詳細な実装を飛ばし読みすることが可能になります。

結果として、関数の意図が明確になり、保守やデバッグの際にコードを理解するための認知負荷を大幅に軽減できます。

Before

def process_data(data):
    raw_data = load_data(data)
    ...

    cleaned_data = clean_data(raw_data)
    ...

    analysis_result = analyze_data(cleaned_data)
    ...

    save_result(analysis_result)
    ...

    return analysis_result

After

def process_data(data):
    # データを読み込む
    raw_data = load_data(data)
    ...
    
    # データを整形する
    cleaned_data = clean_data(raw_data)
    ...
    
    # データを分析する
    analysis_result = analyze_data(cleaned_data)
    ...
    
    # 結果を保存する
    save_result(analysis_result)
    ...
    
    return analysis_result

IFの条件式を丁寧に書く

条件式を短縮して記述することは可能ですが、それをあえて冗長に、しかし明確に記述することで可読性が向上する場合があります。

特に複数の条件が組み合わさる場合や、否定形を含む場合には、変数に意味のある名前を付けて中間的な条件を表現することで、そのロジックが直感的に理解しやすくなります。

// Before (!isDeletedが読みにくい)
if (isActive && !isDeleted) {
  console.log("ユーザーはアクティブです。");
}

// After (可読性を高めた条件式)
const userIsActive = isActive;
const userIsNotDeleted = !isDeleted;
if (userIsActive && userIsNotDeleted) {
  console.log("ユーザーはアクティブです。");
}

早めに切る

関数内で早期に結果が確定する場合や、特定の前提条件を満たさない場合には、早期リターン(ガード節)を使用することでコードの可読性を高めることができます。

ネストが深くなるのを避け、関数の主要なロジックをよりフラットな構造で表現することを可能にします。

Before

def check_value(value):
    if value is not None:
        if value > 10:
            return "Value is greater than 10."
        else:
            return "Value is 10 or less."
    else:
        return "Value is None."

After

def check_value(value):
    if value is None:
        return "Value is None."

    if value > 10:
        return "Value is greater than 10."

    return "Value is 10 or less."

ヘルパー関数を作る

コードベースで共通する処理や繰り返し現れるロジックは、ヘルパー関数として独立させることで、保守性と再利用性を大幅に向上させることができます。

ヘルパー関数を用いることで、メインロジックが簡潔になり、機能の変更やバグ修正が必要になった際に一箇所を修正するだけで全体に反映されるため、開発効率とコード品質の向上が期待できます。

Before

# Before: 重複した処理が複数箇所に存在する

def process_data_before(data):
    # データの前処理
    processed_data = []
    for item in data:
        # 重複する前処理ロジック1
        cleaned_item = item.strip().lower()
        processed_data.append(cleaned_item)

    # データの処理
    result = []
    for item in processed_data:
        # 重複する処理ロジック2
        if len(item) > 5:
            result.append(item.capitalize())
        else:
            result.append(item.upper())
            
    return result

def another_function_before(more_data):
    # ここでも同様の前処理が必要
    processed_data = []
    for item in more_data:
        # 重複する前処理ロジック1
        cleaned_item = item.strip().lower()
        processed_data.append(cleaned_item)

    # 別の処理ロジック
    return processed_data

After

# After: ヘルパー関数で共通の処理をまとめる

def _preprocess_item(item):
    """共通の前処理を行うヘルパー関数"""
    return item.strip().lower()

def _transform_item(item):
    """共通の変換処理を行うヘルパー関数"""
    if len(item) > 5:
        return item.capitalize()
    else:
        return item.upper()

def process_data_after(data):
    # ヘルパー関数を使って前処理
    processed_data = [_preprocess_item(item) for item in data]
    
    # ヘルパー関数を使って変換処理
    result = [_transform_item(item) for item in processed_data]
            
    return result

def another_function_after(more_data):
    # ここでもヘルパー関数を利用
    processed_data = [_preprocess_item(item) for item in more_data]
    
    # 別の処理ロジック
    return processed_data

ループの変数名をこだわる(必要なとき)

入れ子になったループを使用する場合、ijのような一般的な短い変数名ではなく、そのループが何を表しているのかを具体的に示す変数名を用いると便利なときがあります。

例えば、row_indexcol_indexのように意味のある名前を付けることで、コードを読んだときに各ループの役割がすぐに理解できるようになります。

これにより、特に複雑なデータ構造を扱う場合やデバッグ時に、どの変数がどの次元や要素を指しているのかが明確になり、混乱を避け、問題解決の時間を短縮できます。

Before

for i in range(5):
    for j in range(5):
        print(f'i: {i}, j: {j}')

After

# After
for row_index in range(5):
    for col_index in range(5):
        print(f'row_index: {row_index}, col_index: {col_index}')

Discussion