🪄

【GoodCode,BadCode 5章 】コードを読みやすくするチェックリスト

2024/06/09に公開

概要

読みやすいコードを書くことは大切です。それは、ほとんどのエンジニアが理解していることだと思います。しかし、コードの読みやすさは主観的で明確に定義するのは困難です。
読みやすさの本質はコードを見た時、何をしているのか素早く、正確に理解できることです。

本記事では、読みやすいコードのテクニックをまとめます。
なお、コード例はpythonで書きますが、他のどの言語でも通じる内容です。

GoodCode,BadCodeはどんな本?

「よいコード」と「悪いコード」を見極めるための概念やテクニックを紹介している本です。
プロフェッショナルなエンジニアとしてコーディングスキルを高めたい人に役立ちます。
各章それぞれテーマに沿って、コードの事例を交えながら説明してくれるため、わかりやすくとてもおすすめな本です!
https://www.shuwasystem.co.jp/book/b620733.html

1.わかりやすい名前を使う

わかりやすい名前を使うことは、コードを読みやすくする上で重要な要素です。
それは、エンジニアなら誰もが周知の事実だと思います。

名前の書き方は流派があるので詳しいテクニックは割愛しますが、とにかくわかりやすい名前を心がけましょう!

また、少しでも迷ったらチームに相談しましょう。命名くらいで質問するのは迷惑かな...なんて思わないでください!わかりづらい名前を使う方が後々チームの負担になるので!

2.コメントを適切に使う

コード内のコメントは様々な役割を果たします。

  • コードがをしているかの説明
  • コードがなぜその処理をしてるかの説明

コードの大きな塊を要約したコメントは、多くの場面で役に立ちます。
しかし、下位レイヤーの行単位の処理をコメントで説明しても、コード理解の効率化にはなりません。
コメントを書くより、よりわかりやすい名前で、自明な処理を書く方がコードは読みやすいからです。

2-BadCode. 冗長なコメント

コードが自明である場合、コメントは冗長である上、むしろ悪い場合があります。

def get_full_name(firstName, lastName):
    # 苗字と名前を結合して返す
    return firstName + " " + lastName

上記のコードでは、getFullName()が名前を返すことは自明です。
しかしコメントを追加することで、コードを乱雑にしています。全ての行にこのようにコメントを追加すると、他のエンジニアは無駄にコメントを読まなければならず、さらにメンテナンスの負担がかかります。

2-GoodCode. コメントは、コードが存在する理由を説明する

コードはなぜその処理を行なっているのか説明しづらい時があります。
何らかの文脈や前提知識が必要な場合、それらをコードで表すことは難しいです。

このようなコンテキストがコード理解に必要な場合、コメントは有用な手段です。
以下のサンプルコードはgetUserName()がなぜ条件分岐をしているのかを説明しています。

class User:
    def __init__(self, firstName, lastName, nickName=None, loginVersion="v2"):
        self.firstName = firstName
        self.lastName = lastName
        self.nickName = nickName
        # 旧サイトからのログインと新規サイトからのログインが存在する
        self.loginVersion = loginVersion

    def get_user_name(self):
        # 新規サイトからログインするユーザーはニックネームを表示する仕様
        if self.loginVersion == "v2":
            return self.nickName
        else:
            return self.firstName + " " + self.lastName

ただし、コメントはエンジニアが必ず読んでくれるとは限りません。
あくまで、コメントはコードを補完する役割ということを意識し、読みやすいコードにすることを心がけましょう。

3.コードの行数にこだわらない

一般的にコードの行数は少なければ少ないほどいいとされています。これは、コード量が増えることはメンテナンス量が増えることを意味し、他のエンジニアがコードを読む分量が増えるためです。

しかし、簡潔なコードは理解しやすいコードとは限りません。

3-BadCode 簡潔ではあるものの理解しづらいコード

次のコードはnumbersの値が偶数ならその平方、奇数ならその立方を計算し、result配列に格納するコードです。1行に処理が全て格納されていますが、どんな処理をしているのか理解することは難しいです。

numbers = [1, 2, 3, 4, 5]

result = list(map(lambda x: x ** 2 if x % 2 == 0 else x ** 3, numbers))
print(result)

3-GoodCode よりコード長くても、理解しやすいコード

より多くのコードが必要でも、読む人にとって前提を共通することが大切です。適切な名前の関数を作成し、コードを再利用できる形にすることで理解しやすいコードにしましょう。

numbers = [1, 2, 3, 4, 5]

def calculate_square(number):
    return number ** 2

def calculate_cube(number):
    return number ** 3

result = []
for number in numbers:
    if number % 2 == 0:
        # 偶数なら平方を計算
        result.append(calculate_square(number))
    else:
        # 奇数なら立方を計算
        result.append(calculate_cube(number))

print(result)

4.一貫したコーディングスタイルにこだわる

文法的に正しいかどうかは、コンパイラがチェックしてくれますが、コーディンススタイル規約はほどんどの言語で自由に選択できます。
統一感のないコーディングスタイルはバグや勘違いを助長するため、避けるべきです。

  • 特定の言語機能の使い方
  • インデントの数
  • ディレクトリ構造
  • ドキュメントの書き方

など、コーディングスタイルは名前の付け方だけでなく様々な場面をサポートします。

チームでスタイルが決まっていない場合、Googleが出しているスタイルガイドが参考になります。
https://google.github.io/styleguide/

5.ネストの深いコードを避ける

ネストが深くなるとコードの読みやすさが低下するため、ネストの量を最小限になるようにコードを構造化する必要があります。

5-BadCode. ネストの深い処理

今年が閏年か計算する処理です。初めから読む気がなくなるようなコードですね。

def is_leap_year(year):
    # 4で割り切れる年は閏年の候補
    if year % 4 == 0:
        # 100で割り切れる年は平年の候補
        if year % 100 == 0:
            # 400で割り切れる年は閏年
            if year % 400 == 0:
                return True
            else:
                return False
        else:
            return True
    else:
        return False

# 今年の年を取得
from datetime import datetime
current_year = datetime.now().year

# 今年が閏年かどうかを判別
if is_leap_year(current_year):
    print(f"{current_year}年は閏年です。")
else:
    print(f"{current_year}年は閏年ではありません。")

ネストが深くなる原因は一つのコードで処理が多すぎる結果です。
ネストを小さくするには、早期リターンをするか、処理をより小さい関数へ小さく分離しましょう。

5-GoodCode. 早期リターン、小さい関数へ分離

閏年の判別、現在の年を取得、結果の出力をそれぞれ小さい関数へ分離することで読みやすいコードになりました。

from datetime import datetime

def is_leap_year(year):
    # 400で割り切れる年は閏年
    if year % 400 == 0:
        return True
    # 100で割り切れる年は平年
    if year % 100 == 0:
        return False
    # 4で割り切れる年は閏年
    if year % 4 == 0:
        return True
    # それ以外の年は平年
    return False

def get_current_year():
    return datetime.now().year

def check_leap_year():
    current_year = get_current_year()
    if is_leap_year(current_year):
        print(f"{current_year}年は閏年です。")
    else:
        print(f"{current_year}年は閏年ではありません。")

# 今年が閏年かどうかを判別
check_leap_year()

6.関数の呼び出しを読みやすくする

関数に適切な名前がつけられていても、引数の目的や機能が明確でなければ読んでも理解できないコードになります。

6-BadCode. 関数の引数を理解できないコード

次のコードは、メッセージを送信することは理解できますが、引数「1」と「True」が何を示すのか関数の定義を見るまで理解できません。

sendMessage("hello", 1, True)

関数の定義が別ファイルや数百行離れた場所にあることも多いため、参照することは面倒な作業です。
この問題を解決するには、名前付き引数を使用することが望ましいです。
これにより、関数の定義を見なくてもとても読みやすくなります。

6-GoodCode. 名前付き引数使って関数を呼び出す

# 名前付き引数を使って、関数を呼び出す
sendMessage(message="Hello, World!", priority=1, isRetry=True)

TypeScriptなど名前付き引数に対応していない言語では、インターフェースやオブジェクトを使って表現することができます。

interface SendMessageProps {
  message: string;
  priority: number;
  isRetry: boolean;
}

function sendMessage({ message, priority, isRetry }: SendMessageProps) {
  // send message
}

// TypeScriptでの擬似名前付き引数を使用した関数定義
sendMessage({ message: "Hello", priority: 1, isRetry: true });

7.説明のない値を使用しない

マジックナンバー、ハードコーディングは、値の存在理由や機能を説明していないため、読みにくいコードになってしまいます。

適切な定数、ヘルパー関数を使用しましょう。

【定数を使用した場合】

CAL_TO_JOULE = 4.184

# カロリーをジュールに変換する
joule = cal * CAL_TO_JOULE

【ヘルパー関数の場合】

# カロリーをジュールに変換する
def cal_to_joule(cal: float):
    return cal * 4.184

定数やヘルパー関数を使う際は他のエンジニアが再利用できるかを検討しましょう。
再利用できる可能性がある場合、パブリックなユーティリティクラスに定数やヘルパー関数を配置しましょう。

8.無名関数を適切に利用する

小さくて自明な処理は無名関数を利用すると読みやすくなります。
たった1行のコードしか必要とせず、解決する問題もわずかなもののため、次のコードは読みやすくコンパクトです。

# コメントがあるフィードバック配列を返す
def filter_feedback_with_comments(feedback_list: list[Feedback]) -> list[Feedback]:
    return [feedback for feedback in feedback_list if feedback.comment]
無名関数 動作するコード
class Feedback:
    def __init__(self, comment: str):
        self.comment = comment

def filter_feedback_with_comments(feedback_list: list[Feedback]) -> list[Feedback]:
    return [feedback for feedback in feedback_list if feedback.comment]

feedbacks = [Feedback(""), Feedback("コメント"), Feedback(None)]
filtered_feedbacks = filter_feedback_with_comments(feedbacks)

# "1" が出力される
print(len(filtered_feedbacks))

このシナリオでは、無名関数を使用しても問題ありません。
なぜなら、中のロジックが小さく、単純なためです。

8-BadCode. 説明が必要な処理に無名関数を使う

無名関数は文字通り名前がないため、関数名のような内容の要約を読み手に伝えることができません。
どんなに小さくても、関数の内容が自明でない限り、無名関数を使ってもコードは読みにくくなります。

# パリティピットを計算するBadCode
calculate_parity_bit = lambda binary_string: 0 if binary_string.count('1') % 2 == 0 else 1

このようなロジックは、確かに簡潔ですが、何をしているかほとんどのエンジニアが理解できないため、説明が必要です。
ロジックが複雑、説明が必要な場合は名前付き関数を使うようにしましょう。

9.すばらしい新しい言語機能を適切に使用する

エンジニアは新機能を使いたがる傾向にありますが、それが本当に目の前の処理に最適なツールかは、立ち止まって考える必要があります。

  • 新機能のメリット
    • より完結で機能的なスタイルのコードが書ける
    • 冗長性を省き、バグの少ないコードが書ける
  • 新機能のデメリット
    • 他のエンジニアが知らない機能の場合、混乱を招く
    • 新機能が常に問題を解決してくれるわけではなく、効率を落とす場合がある

新しい言語機能を使う際は、言語機能が真新しいからという理由ではなく、目の前の処理に適したツールだから利用するということを心がけましょう。

まとめ

コードが読みにくいと、次のような問題が起こる可能性があります。

  • 他のエンジニアが読んで理解するまでの時間を浪費する
  • 思わぬバグを発生させる

自戒を込めて、読みやすいコードの章をまとめました。
参考になれば幸いです。

Discussion