A コーディング — 退化コメント・コメントと実装の不一致・死んだコード・TODO書式(カート合計計算 Bad→Good Ch12)

2026-08-03 (Day 121) 月曜 コーディング ★★★★☆ Python 3.12 / Decimal / dataclass(frozen, slots) 良いコード設計入門 Ch12(コメント)

概要

🫥

退化コメントの排除(Ch12)

# 小計# 送料を計算する# 合計を返す のような、コードを読めば1秒で分かることをそのまま日本語化しただけのコメントを削除し、変数名・定数名自体で意図を伝える。

⚠️

コメントと実装の同期(Ch12)

# 送料880円を加算 というコメントが、送料改定で実装が 990 に変わった後も取り残されていた。名前付き定数に置き換えることで、値そのものが「今の正しい値」であり続け、原理的にズレが起きなくする。

🕸️

死んだコードの削除(Ch12)

理由不明のままコメントアウトされたポイント還元ロジックを削除。Gitの履歴で復元可能なため、ソースコード上に残す理由がない。

🎯

Why型コメント・docstring・TODO書式(Ch12)

閾値・割引率が「なぜその値なのか」というビジネス根拠を docstring に明記し、# TODO: ...# TODO(担当者): 内容 (Issue: 番号, 対応予定: 時期) の追跡可能な形式に統一する。

問題

ECサイト MOps チームでは、カート合計金額(小計・送料・キャンペーン割引・総額)を計算する calculate_cart_total を Python で実装している。以下の「悪いコード」は、コードをそのまま日本語に翻訳しただけの「退化コメント」が随所にあり、さらに送料のコメント(880円を加算)が2025-10の送料改定で実装(990)と食い違ったまま放置されている。加えて、理由不明のままコメントアウトされたポイント還元ロジックの死んだコードや、担当者・期限・チケット番号のないTODOコメントも残っている。

制約・前提条件

  • Python 3.12+、金額計算には decimal.Decimal を使用すること(浮動小数点誤差を避ける)
  • 送料無料閾値・標準送料・キャンペーン割引閾値・割引率は名前付き定数にすること(0.9 のような乗数ではなく CAMPAIGN_DISCOUNT_RATE = Decimal("0.10") のように「引かれる側の率」を明示すること)
  • 戻り値は subtotal/shipping_fee/discount/total を保持する dataclass(frozen=True, slots=True) の値オブジェクト(CartTotal)にすること
  • 公開関数には Google スタイル docstring を付け、Args/Returns/Raises に加えて「なぜその閾値・割引率なのか」というビジネス上の根拠(Why)を1〜2文で書くこと(架空のチケット番号可)
  • コメントアウトされた死んだコード(ポイント還元ロジック)は削除すること(Gitの履歴に残るため復元は可能、という方針をどこかに一言残せばよい)
  • TODOコメントは # TODO(担当者): 内容 (Issue: チケット番号, 対応予定: 時期) の形式で書き直すこと
  • コードを読めば分かることを繰り返すだけの「退化コメント」(例:「小計」「送料を計算する」「合計を返す」)は残さないこと
期待する回答形式: 問題点の列挙(番号付き)+ 改善後コード(Google スタイル docstring・インラインコメント・名前付き定数含む)+ 実行例(input→output)+ 適用した設計パターン名と書籍対応章

悪いコード (Before)

このコードには 7つの設計上の問題 が隠れています。見つけてみてください。
bad_cart_pricing.py — 退化コメント × コメントと実装の不一致 × 死んだコード × 書式のないTODO
def calculate_cart_total(price, quantity, is_premium_member):
    """カート合計を計算する"""
    # 問題1: 退化コメント(読めば分かることの繰り返し)
    subtotal = price * quantity  # 小計

    # 送料を計算する
    # 問題2: 実装は990円なのにコメントは880円のまま(送料改定で不一致)
    shipping_fee = 990  # 送料880円を加算
    if subtotal >= 5000:
        shipping_fee = 0  # 5000円以上は送料無料にする

    # プレミアム会員は送料無料
    if is_premium_member:
        shipping_fee = 0

    # キャンペーン割引を適用する
    total = subtotal + shipping_fee
    # 問題3: マジックナンバー(10000, 0.9)の意味がコメント頼み
    if subtotal >= 10000:
        total = total * 0.9  # 0.9をかけて10%引く

    # 問題4: 理由不明のまま放置された死んだコード
    # 以下は旧ロジック(ポイント還元は別モジュールに移動済み)
    # points = int(subtotal * 0.01)
    # total -= points

    # 問題5: 担当者・期限・チケット番号のないTODO
    # TODO: 消費税の軽減税率対応

    # 合計を返す
    return total
問題点サマリー(7点)
1退化コメントの氾濫(Ch12)# 小計, # 送料を計算する, # 合計を返す はコードと同じ情報量しか持たず、読み手に新しい情報を与えない
2コメントと実装の不一致(Ch12)# 送料880円を加算 と書かれているが、実装は送料改定後の 990 のまま取り残され、読むと誤解する
3マジックナンバー(Ch10関連)5000, 990, 10000, 0.9 がコメントで補足されているだけで名前付き定数化されていない
4死んだコードの放置(Ch12) — コメントアウトされたポイント還元ロジックが「使われているのか無効化中なのか」判断できないまま残存
5Why(根拠)の欠如(Ch12) — なぜ5000円で送料無料か、なぜ10000円で10%引きかというビジネス上の意思決定根拠が一切書かれていない
6docstring未整備(Ch12) — 一行だけの """カート合計を計算する""" で、Args/Returns/Raisesの説明がない
7追跡不能なTODO(Ch12)# TODO: 消費税の軽減税率対応 に担当者・チケット番号・対応時期がなく、いつまでも放置されやすい

ヒント(段階的開示)

ヒント1 — 方向性
# 単価に数量をかけて小計を出す というコメントの直後に subtotal = price * quantity という、まさにその通りのコードが続いている。このコメントは何か新しい情報を読み手に与えているだろうか? コードを読めば1秒で分かることをもう一度日本語で言い直しているだけなら、それは第12章でいう「退化コメント」であり、むしろメンテナンスコスト(コードを直したのにコメントを直し忘れる)だけを増やす。実際に # 送料880円を加算 というコメントは、実装が 990 に変わった後も更新されず、今や嘘の情報源になっている。良いコメントは「コードを見れば分かるWhat」ではなく「コードを見ても分からないWhy(なぜその値・その判断なのか)」を書くものだと考えると、どこを削り、どこを足すべきかが見えてくる。
ヒント2 — アプローチ
  • 退化コメント(# 小計, # 送料を計算する, # 合計を返す など)→ すべて削除。その代わり subtotal, shipping_fee, discount, total という変数名・定数名自体で意図が伝わるようにする
  • 食い違ったコメント(# 送料880円を加算 vs 実装 990)→ 削除し、STANDARD_SHIPPING_FEE_YEN = Decimal("990") という名前付き定数+docstring内の「Why」記述に置き換える
  • # 以下は旧ロジック(...) のコメントアウトされた死んだコード → 削除する。Gitで復元可能なので保持する理由がない
  • 5000, 990, 10000, 0.9 のマジックナンバー → FREE_SHIPPING_THRESHOLD_YEN / STANDARD_SHIPPING_FEE_YEN / CAMPAIGN_DISCOUNT_THRESHOLD_YEN / CAMPAIGN_DISCOUNT_RATE に置き換え、なぜその値なのかは関数の docstring に1〜2文でまとめて書く(コード各所に散らばせない)
  • # TODO: 消費税の軽減税率対応# TODO(kainuma): 軽減税率(8%)対象商品の税区分判定を追加する (Issue: MOPS-1421, 対応予定: 2026-Q4) のように、担当者・内容・チケット・時期が揃った形式にする
  • 関数の一行docstring """カート合計を計算する""" → Args/Returns/Raises と、閾値・割引率の設定根拠(Why)を含む Google スタイル docstring に拡張する
ヒント3 — コードの骨格
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP

FREE_SHIPPING_THRESHOLD_YEN = Decimal("5000")
STANDARD_SHIPPING_FEE_YEN = Decimal("990")
CAMPAIGN_DISCOUNT_THRESHOLD_YEN = Decimal("10000")
CAMPAIGN_DISCOUNT_RATE = Decimal("0.10")


@dataclass(frozen=True, slots=True)
class CartTotal:
    subtotal: Decimal
    shipping_fee: Decimal
    discount: Decimal
    total: Decimal


def calculate_cart_total(
    unit_price: Decimal, quantity: int, is_premium_member: bool
) -> CartTotal:
    """...

    Why: ここに閾値・割引率の設定根拠を書く(Issue番号でも可)

    Args: ...
    Returns: ...
    Raises: ...
    """
    ...  # 退化コメントなし。Why以外のコメントは基本不要

問題点分析(7点)

#問題点分類改善方法
1退化コメント(小計/送料計算/合計を返す)退化コメント Ch12削除。変数名・定数名で意図を伝える
2「送料880円」コメントが実装990円と不一致実装との不一致 Ch12STANDARD_SHIPPING_FEE_YEN 定数化
35000/990/10000/0.9 のマジックナンバーマジックナンバー Ch10名前付き定数4種
4コメントアウトされたポイント還元ロジック死んだコード Ch12削除(Git履歴で代替)
5閾値・割引率の設定根拠(Why)が皆無Why欠如 Ch12docstringに根拠・チケット番号記載
6一行docstringでArgs/Returns/Raisesなしdocstring未整備 Ch12Googleスタイルdocstring
7担当者・チケット・時期のないTODOTODO書式 Ch12TODO(担当者): ... (Issue, 対応予定)

コメント分類図(SVG)— 消す/直す/足すの3分類

Before のコメント4種 ① 退化コメント 「小計」「合計を返す」 ② 実装と不一致 「880円」vs 実装990円 ③ 死んだコード コメントアウトされたpoints ④ 書式のないTODO 担当者・期限・Issueなし 共通点: コードと同じ情報 or 古い情報 or 追跡不能な 情報しか運んでいない Ch12 リファクタ 消す(Delete) 退化コメント・食い違ったコメント 死んだコード(points) → 読み手の認知負荷を減らす 直す(Fix) 5000/990/10000/0.9 → 名前付き定数(Ch10) 値そのものが正としてズレない 足す(Add)— Why型 なぜ5000円/10000円/10%か 施策名・チケット番号を docstring に Args/Returns/Raises 完備 半年後の担当者が調査せずに済む 足す(Add)— TODO書式 TODO(担当者): 内容 (Issue, 対応予定) CartTotal frozen dataclass(slots) subtotal / shipping_fee discount / total Ch3/Ch4: 値オブジェクト・不変 Before: 退化コメント・不一致コメント・死んだコード・書式なしTODOが混在 / After: 消す(退化・不一致・死んだコード)・直す(名前付き定数)・足す(Why型docstring・TODO書式)の3アクションに分解

模範解答

Before — 退化コメント × コメントと実装の不一致 × 死んだコード × 書式のないTODO
def calculate_cart_total(price, quantity, is_premium_member):
    """カート合計を計算する"""
    # 単価に数量をかけて小計を出す
    subtotal = price * quantity  # 小計

    # 送料を計算する
    shipping_fee = 990  # 送料880円を加算
    if subtotal >= 5000:
        shipping_fee = 0  # 5000円以上は送料無料にする

    # プレミアム会員は送料無料
    if is_premium_member:
        shipping_fee = 0

    # キャンペーン割引を適用する
    total = subtotal + shipping_fee
    if subtotal >= 10000:
        total = total * 0.9  # 0.9をかけて10%引く

    # 以下は旧ロジック(ポイント還元は別モジュールに移動済み)
    # points = int(subtotal * 0.01)
    # total -= points

    # TODO: 消費税の軽減税率対応

    # 合計を返す
    return total
After — 退化コメント排除 × Why型コメント × docstring整備 × TODO書式統一
"""cart_pricing.py — カート合計金額(小計・送料・割引・総額)の計算

良いコード・悪いコードで学ぶ設計入門(改訂新版)
  Ch12: コメント — 退化コメントの排除、コメントと実装の同期、目的(Why)の文書化
"""
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP

# 送料改定(2025-10)以降の基準値。この金額(円)以上の小計、または
# プレミアム会員(会員ランク特典 2025-Q4, PROMO-2025-42)の場合に送料無料となる。
FREE_SHIPPING_THRESHOLD_YEN = Decimal("5000")

# 国内標準送料(2025-10 改定後の金額、円)。
STANDARD_SHIPPING_FEE_YEN = Decimal("990")

# まとめ買い促進キャンペーンの適用条件(販促企画チーム承認: PROMO-2025-88)。
# 小計がこの金額(円)以上の注文に CAMPAIGN_DISCOUNT_RATE を適用する。
CAMPAIGN_DISCOUNT_THRESHOLD_YEN = Decimal("10000")
CAMPAIGN_DISCOUNT_RATE = Decimal("0.10")


@dataclass(frozen=True, slots=True)
class CartTotal:
    """カート合計金額の内訳を保持する値オブジェクト。

    Attributes:
        subtotal: 小計(単価 × 数量、円)。
        shipping_fee: 送料(円)。無料条件を満たす場合は0。
        discount: キャンペーン割引額(円)。
        total: 小計 + 送料 - 割引の総額(円)。
    """

    subtotal: Decimal
    shipping_fee: Decimal
    discount: Decimal
    total: Decimal


def calculate_cart_total(
    unit_price: Decimal, quantity: int, is_premium_member: bool
) -> CartTotal:
    """カート合計金額(小計・送料・割引・総額)を計算する。

    送料はプレミアム会員のLTV向上施策(会員ランク特典 2025-Q4, PROMO-2025-42)
    として、小計が FREE_SHIPPING_THRESHOLD_YEN 以上、またはプレミアム会員の
    場合に無料となる。キャンペーン割引(CAMPAIGN_DISCOUNT_RATE)は、まとめ買い
    促進を目的として小計 CAMPAIGN_DISCOUNT_THRESHOLD_YEN 以上の注文に適用される
    (販促企画チーム承認: PROMO-2025-88)。

    Args:
        unit_price: 商品単価(円)。
        quantity: 数量。
        is_premium_member: プレミアム会員かどうか。

    Returns:
        小計・送料・割引額・総額を保持する CartTotal。

    Raises:
        ValueError: unit_price または quantity が0以下の場合。
    """
    if unit_price <= 0 or quantity <= 0:
        raise ValueError("unit_price と quantity は正の値である必要があります")

    subtotal = unit_price * quantity

    shipping_fee = STANDARD_SHIPPING_FEE_YEN
    if subtotal >= FREE_SHIPPING_THRESHOLD_YEN or is_premium_member:
        shipping_fee = Decimal("0")

    discount = Decimal("0")
    if subtotal >= CAMPAIGN_DISCOUNT_THRESHOLD_YEN:
        discount = (subtotal * CAMPAIGN_DISCOUNT_RATE).quantize(
            Decimal("1"), rounding=ROUND_HALF_UP
        )

    total = subtotal + shipping_fee - discount

    # TODO(kainuma): 軽減税率(8%)対象商品の税区分判定を追加する。
    # 現在は全商品を標準税率10%として扱っており、食品等が混在するカートの
    # 税額按分は未対応。 (Issue: MOPS-1421, 対応予定: 2026-Q4)

    return CartTotal(
        subtotal=subtotal, shipping_fee=shipping_fee, discount=discount, total=total
    )
# 通常会員・少額(送料あり・割引なし)
result = calculate_cart_total(Decimal("1200"), 2, is_premium_member=False)
print(result)
# CartTotal(subtotal=Decimal('2400'), shipping_fee=Decimal('990'), discount=Decimal('0'), total=Decimal('3390'))

# 通常会員・送料無料閾値超(割引なし)
result = calculate_cart_total(Decimal("2500"), 3, is_premium_member=False)
print(result)
# CartTotal(subtotal=Decimal('7500'), shipping_fee=Decimal('0'), discount=Decimal('0'), total=Decimal('7500'))

# 通常会員・キャンペーン割引適用
result = calculate_cart_total(Decimal("5000"), 3, is_premium_member=False)
print(result)
# CartTotal(subtotal=Decimal('15000'), shipping_fee=Decimal('0'), discount=Decimal('1500'), total=Decimal('13500'))

# プレミアム会員・少額でも送料無料
result = calculate_cart_total(Decimal("500"), 2, is_premium_member=True)
print(result)
# CartTotal(subtotal=Decimal('1000'), shipping_fee=Decimal('0'), discount=Decimal('0'), total=Decimal('1000'))

# 異常系: 数量0
calculate_cart_total(Decimal("1000"), 0, is_premium_member=False)
# ValueError: unit_price と quantity は正の値である必要があります
ポイント適用した設計原則書籍対応章
# 小計, # 送料を計算する, # 合計を返す の削除退化コメントの排除Ch12
# 送料880円を加算(実装990と不一致)の削除コメントと実装の同期Ch12
# 以下は旧ロジック(...) の死んだコードの削除死んだコードの排除(Git履歴で代替)Ch12
docstring に「なぜ5000円/10000円/10%なのか」を明記目的(Why)の文書化Ch12
"""カート合計を計算する""" → Args/Returns/Raises付き docstringdocstring整備(Googleスタイル)Ch12
# TODO: ...# TODO(kainuma): ... (Issue, 対応予定)TODOコメントの書式統一Ch12
5000/990/10000/0.9 → 名前付き定数マジックナンバーの排除Ch10
CartTotaldataclass(frozen=True, slots=True)値オブジェクト・不変の活用Ch3/Ch4
# tests/test_cart_pricing.py
from decimal import Decimal

import pytest
from cart_pricing import calculate_cart_total


class TestCalculateCartTotal:
    def test_standard_shipping_fee_applies_below_threshold(self):
        result = calculate_cart_total(Decimal("1200"), 2, is_premium_member=False)
        assert result.shipping_fee == Decimal("990")
        assert result.total == Decimal("3390")

    def test_free_shipping_at_threshold(self):
        result = calculate_cart_total(Decimal("2500"), 2, is_premium_member=False)
        assert result.subtotal == Decimal("5000")
        assert result.shipping_fee == Decimal("0")

    def test_premium_member_always_free_shipping(self):
        result = calculate_cart_total(Decimal("500"), 1, is_premium_member=True)
        assert result.shipping_fee == Decimal("0")

    def test_campaign_discount_applies_at_threshold(self):
        result = calculate_cart_total(Decimal("5000"), 3, is_premium_member=False)
        assert result.subtotal == Decimal("15000")
        assert result.discount == Decimal("1500")
        assert result.total == Decimal("13500")

    def test_no_discount_below_campaign_threshold(self):
        result = calculate_cart_total(Decimal("3000"), 3, is_premium_member=False)
        assert result.discount == Decimal("0")

    def test_raises_value_error_for_zero_quantity(self):
        with pytest.raises(ValueError):
            calculate_cart_total(Decimal("1000"), 0, is_premium_member=False)

    def test_raises_value_error_for_non_positive_price(self):
        with pytest.raises(ValueError):
            calculate_cart_total(Decimal("-100"), 1, is_premium_member=False)

ポイント解説

1退化コメントはコードより先に嘘をつく(Ch12)# 単価に数量をかけて小計を出す の直後に subtotal = price * quantity があるように、コードを読めば1秒で分かることをもう一度日本語で言い直すコメントは、読み手にとって新しい情報を何も追加しない。それどころか、コードだけを修正してコメントを直し忘れるリスクを常に抱える。実際にこのコード内の # 送料880円を加算 は、送料が 990 に変わった後もコメントだけが取り残され、今や「読むと誤解する」有害なコメントになっている。コメントを書く前に「このコメントを消してもコードから同じ情報が読み取れるか」を自問するとよい。
2コメントは実装と同期させるか、そもそも書かない(Ch12) — 金額・閾値のような変わりやすい値をコメントの自然文で表現すると、値が変わるたびにコメントも直す必要があり、実務ではまず追従されない。名前付き定数(STANDARD_SHIPPING_FEE_YEN = Decimal("990"))にすれば、値そのものが「今の正しい値」であり続け、コメントとのズレが原理的に起きなくなる。
3死んだコードはGitに任せて消す(Ch12) — コメントアウトされた # points = int(subtotal * 0.01) は、「使われていない」のか「一時的に無効化しているだけ」なのか読み手には区別できず、判断コストだけを生む。バージョン管理システムに履歴が残っている以上、コードとして復元したければ git log を辿ればよく、ソースコード上に残しておく理由はない。
4良いコメントは「Why」を書く(Ch12) — 「5000円以上で送料無料」「10000円以上で10%引き」という条件自体はコードを読めば分かるが、「なぜ5000円なのか」「なぜ10%なのか」というビジネス上の意思決定の根拠はコードには現れない。docstring にこの根拠(施策名・チケット番号)を書いておくことで、半年後に閾値を変更しようとした担当者が「これは何かの根拠があって決まった値か、それとも適当な初期値か」を無駄に調査せずに済む。
5TODOコメントは追跡可能な形式で書く(Ch12)# TODO: 消費税の軽減税率対応 は、誰がいつ対応するのか分からないまま放置されやすく、実際に多くのコードベースで「何年も残り続けるTODO」の典型パターンになる。担当者・チケット番号・対応予定時期をセットで書くことで、TODOが「単なる願望」ではなく「追跡可能なタスク」として扱われるようになる。

実務への応用

MOps チームのようにキャンペーン施策の閾値・割引率が頻繁に変わるドメインでは、「なぜその値なのか」というWhyの記録が失われると、次に閾値を変更する担当者(自分自身かもしれない)が過去の販促企画の意図を推測するところから始めることになる。docstring にチケット番号や施策名を残しておけば、Confluence やチケット管理システムを検索せずとも、コードの中に「この定数がなぜ存在するか」の手がかりが残る。

コメントの陳腐化を構造的に防ぐ: コメントと実装の不一致(今回の「880円」vs「990円」)は、コードレビューで見逃されると本番稼働後も長期間放置されやすく、新しく参加したメンバーがコメントを信じて誤った前提でコードを読んでしまう事故につながる。名前付き定数への置き換えは、この種の「コメントの陳腐化」自体を構造的に防ぐ手段になる。

今日のまとめ

良いコメントは「コードを読めば分かるWhat」を繰り返すのではなく、「コードだけでは分からないWhy(なぜその値・その判断なのか)」を書くものであり、退化コメント・食い違ったコメント・死んだコードを削り、Why型コメントとGoogleスタイルdocstring、追跡可能なTODO書式に置き換えることで、コメントは「コードと一緒に腐る負債」から「コードを補強する資産」に変わる。

次のステップ

  • 発展問題: CAMPAIGN_DISCOUNT_RATE のようなキャンペーン割引率が「常時開催の1種類」から「複数キャンペーンの重ね掛け可否ルールを持つ複数種類」に増えた場合を想定し、Why型コメント・docstring の書き方をどう発展させるべきか(例: CampaignPolicy のような値オブジェクトに施策名・根拠チケットをフィールドとして持たせ、コメントではなくデータとして表現する設計)を検討してください。
  • 参考: 「良いコード・悪いコードで学ぶ設計入門(改訂新版)」Ch12(コメント)/ Ch10(設計の悪魔・マジックナンバー)/ Google Python Style Guide(Comments and Docstrings)

自己評価(あとで記入)