Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Good Code

コード品質の柱

『Good Code,Bad Code』では次の6つをコードの品質を支える柱としている

抽象化レイヤー

コードを上手く構造化できれば、きれいな抽象化レイヤーができる。

良い抽象化の例

例えばHTTPでPOSTリクエストを送るとする。requestsパッケージを使えば次のように書くことができる。

import requests
requests.post('https://httpbin.org/post', data={'key': 'value'})

これは内部的には

  • データのシリアライズ

  • HTTPプロトコルに関する様々な問題の解決

  • TCP通信

  • データ送信時のエラーと訂正

など様々なことを行っているが、適切に抽象化されているためユーザーは内部実装を気にせず、やりたいことだけを考えて短い記述だけすればよくなっている。

きれいな抽象化レイヤーを作ることで、読みやすく、モジュール性があり、再利用性があり、テスタビリティの高いコードになる。

関連:SLAP原則

SLAP: Single Level of Abstraction Principle(抽象化レベルの統一)

コードを書くとき、機能の複雑さや抽象化のレベルに応じて多層に分離し、それぞれの層における抽象化のレベルは揃えるべき、というもの。

def 高水準():
    中水準1()
    中水準2()

def 中水準1():
    低水準1()
    低水準2()

def 低水準1():
    ...

...

コードのレイヤー

抽象化レイヤーを作るにはコードを関数やクラス、パッケージなどの異なる単位に分割することになる。

関数

例えば「車の所有者の住所を探し、見つかれば手紙を送る」という処理を考える

例:悪いコード
from typing import Optional


def send_owner_a_letter(
    vehicle: Vehicle,
    letter: Letter,
) -> Optional[Confirmation]:
    owners_address: Optional[Address] = None

    # 住所を探す基本的なロジック
    if vehicle.has_been_scrapped():
        owners_address = SCRAPYARD_ADDRESS
    else:
        most_recent_purchase = vehicle.get_most_recent_purchase()

        if most_recent_purchase is None:
            owners_address = SHOWROOM_ADDRESS
        else:
            owners_address = most_recent_purchase.get_buyers_address()

    # 条件付きで手紙を送るロジック
    if owners_address is None:
        return None

    return send_letter(owners_address, letter)

このコードでは1つの関数に「住所を探す」と「手紙を送るかどうかの判定」が混在しており、関数名と一致していない

例:良いコード

関数を小さくして分割し、関数名が内容と一致するようにすることでより読みやすくなる

from typing import Optional


def find_owners_address(vehicle: Vehicle) -> Optional[Address]:
    if vehicle.has_been_scrapped():
        return SCRAPYARD_ADDRESS

    most_recent_purchase = vehicle.get_most_recent_purchase()

    if most_recent_purchase is None:
        return SHOWROOM_ADDRESS

    return most_recent_purchase.get_buyers_address()


def send_owner_a_letter(
    vehicle: Vehicle,
    letter: Letter,
) -> Optional[Confirmation]:
    owners_address = find_owners_address(vehicle)

    if owners_address is None:
        return None

    return send_letter(owners_address, letter)

クラス

読みやすいコード

適切な命名

# ❌️極端に短い名前
class T:
    ...

# ⭕️わかりやすくする
class Team:
    ...

コードの行数が増えたとしても読みやすく書く

# ❌️短いが意図がわかりにくいコード
def calculate_price(price, quantity, is_member):
    return max(price * quantity * (0.9 if is_member else 1) - (500 if quantity >= 10 else 0), 0)
# ⭕️長いが意図が分かりやすいコード
MEMBER_DISCOUNT_RATE = 0.10
BULK_DISCOUNT_THRESHOLD = 10
BULK_DISCOUNT_AMOUNT = 500


def calculate_price(
    unit_price: int,
    quantity: int,
    is_member: bool,
) -> int:
    subtotal = unit_price * quantity

    member_discount = 0
    if is_member:
        member_discount = int(subtotal * MEMBER_DISCOUNT_RATE)

    bulk_discount = 0
    if quantity >= BULK_DISCOUNT_THRESHOLD:
        bulk_discount = BULK_DISCOUNT_AMOUNT

    total_discount = member_discount + bulk_discount
    final_price = subtotal - total_discount

    return max(final_price, 0)

✅️「行数を少なく」より「読みやすく」書く
✅️定数、係数は説明を入れる

ネストは最小限にする

if文は気をつけないと深いネストになりがちで、どんどん読みにくくなっていく

  • ✅️ネストは最小限にする(早期リターンを活用する)

  • ✅️別の関数に切り出せないか検討する

    • 長いif文は凝集度が低い可能性

# ❌️深くネストしたコード
def get_user_name(user):
    if user is not None:
        if user.is_active:
            return user.name

    return None
# ⭕️ネストを最小限にしたコード
def get_user_name(user):
    if user is None:
        return None

    if not user.is_active:
        return None

    return user.name
早期リターン(ガード節)

関数内の処理で早めにreturnできるケース(例:入力値が不正)は早めにreturnすることでネストが深くなるのを避けられる

# ❌️if-else
def function():
    if condition:
        normal_process()
    else:
        return error
# ⭕️早期リターン
def function():
    if not condition:
        return error

    normal_process()

コメントはWhyを書く

❌️howはコードにあるのでコメントに書くと冗長

# ❌️コメントでhowを書いたコード
def generate_id(data: list[str]) -> str:
    # data[0]はユーザーの名、data[1]は姓が含まれる
    # {first name}.{last name}の形式でIDを生成する
    return data[0] + "." + data[1]

上記の例は、howについてコメントを書かないといけないほどコード自体がわかりにくいのが根本原因でもある

# ⭕️コード自体が説明になるように書き直した場合
def generate_id(data: list[str]) -> str:
    return first_name(data) + "." + last_name(data)

def first_name(data: list[str]) -> str:
    return data[0]

def last_name(data: list[str]) -> str:
    return data[1]

契約プログラミング

契約プログラミング、または 契約による設計(Design by Contract: DbC)とは、関数やクラスの利用者と実装者の間に、明確な「契約」を定めてソフトウェアを設計する考え方。

契約の3要素

契約は主に次の3要素で構成される:

1. 事前条件

処理を呼び出す側が満たすべき条件。
例えば「引き出し金額は0より大きい」「配列は空でない」など。

def withdraw(balance: int, amount: int) -> int:
    assert amount > 0, "引き出し金額は正数でなければならない"
    assert amount <= balance, "残高を超えて引き出すことはできない"

    return balance - amount

2. 事後条件

処理が正常に終了したとき、実装側が保証する条件。

def withdraw(balance: int, amount: int) -> int:
    assert amount > 0
    assert amount <= balance

    new_balance = balance - amount

    assert new_balance == balance - amount
    assert new_balance >= 0

    return new_balance

この関数は、呼び出し後に以下を保証する。

  • 新しい残高は元の残高から引き出し額を引いた値である(new_balance == balance - amount

  • 残高は負にならない(new_balance >= 0

3. 不変条件

コードの呼び出し前後で変わらず常に成立していなければならない条件。

例えば次のクラスがあるとする:

class BankAccount:
    def __init__(self, balance: int = 0) -> None:
        assert balance >= 0
        self._balance = balance
        self._check_invariant()

    def withdraw(self, amount: int) -> None:
        assert amount > 0
        assert amount <= self._balance

        self._balance -= amount

        self._check_invariant()

    def deposit(self, amount: int) -> None:
        assert amount > 0

        self._balance += amount

        self._check_invariant()

    def _check_invariant(self) -> None:
        assert self._balance >= 0, "残高は常に0以上でなければならない"

このクラスでは、 self._balance >= 0 が不変条件

契約保証する側違反した場合
事前条件呼び出し側呼び出し方が誤っている
事後条件実装側関数の実装が誤っている
不変条件クラスの実装側オブジェクトの状態管理が誤っている

ただし、外部入力に対して assert を使うのは一般に不適切。Pythonの assert は、最適化オプションを付けて実行すると無効化される可能性があるため。

したがって、実務では次のように使い分ける。

  • ユーザー入力やAPI入力の検証:ValueError やバリデーションライブラリ

  • 内部実装上、必ず成立すべき前提:assert

  • ドメイン上のルール違反:専用の例外

予期せぬ副作用を避ける

副作用とは、関数が戻り値以外でシステムに影響を与えること。

例えば

  • ユーザーへの出力表示

  • ストレージへの保存

  • 別のシステムを呼び出すことで発生するネットワークトラフィック

など。

副作用は避けられないものであるがゆえに、わかりやすく書く必要がある。

⭕️意図が明白な副作用なら問題ない

import tkinter as tk

root = tk.Tk()
name_entry = tk.Entry(root)
message_label = tk.Label(root, text="")
...

def show_greeting():
    name = name_entry.get()
    message_label.config(text=f"こんにちは、{name}さん")
    # GUIの状態を書き換えているため副作用だが、関数名から予期できる
...

❌️予期せぬ副作用は問題となる可能性がある

例えば値の読み取りの関数なのに内部的には書き込みなどの副作用を行っていた場合、その関数の利用者が予期しない挙動となり、問題になる可能性がある。

def calculate_total(price, quantity):
    total = price * quantity
    save_result(total) # ストレージへの保存(副作用)
    return total

予期せぬ副作用は以下のような問題がある:

  1. ❌️呼び出し元の想定から外れる(→ バグを生む可能性)

  2. ❌️計算コストが高い可能性がある(→ 予期せぬ形でパフォーマンスが落ちる)

✅️副作用は明白にしておく

どうしても読み取りと書き込みを同時に行いたい場合、関数名からわかるようにしておく

def calculate_and_save_total(price, quantity):
    total = price * quantity
    save_result(total) # ストレージへの保存(副作用)
    return total

参考

  • Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考. (2023). 株式会社秀和システム.