条件分岐
6-3

switch 重複を Enum+多態で一元化

「種類ごとに値や振る舞いが変わる」を関数ごとの if-elif で書くと、同じ分岐が何か所にもコピーされる。新しい種類を追加するとき全箇所を漏れなく直す必要があり、1か所でも忘れると静かに間違った値を返す。種類を Enum にし、種類ごとの値と振る舞いを1か所にまとめれば、追加・変更の影響範囲が一点に閉じる。

01 消費税区分ごとの税率・請求書表記

Bad

税区分の分岐が関数ごとに重複。請求書表記の関数だけ非課税の追加を忘れている

同じ分岐があちこちにコピーされる
def tax_rate(category: str) -> Decimal:
    if category == "standard":
        return Decimal("0.10")
    elif category == "reduced":
        return Decimal("0.08")
    elif category == "exempt":
        return Decimal("0")
    return Decimal("0")

def invoice_label(category: str) -> str:
    if category == "standard":
        return "10%対象"
    elif category == "reduced":
        return "8%対象(軽減税率)"
    # "exempt" の追加を忘れても、空文字が返るだけで気づけない
    return ""
Good

区分は Enum、区分ごとの値は不変データとして1つの対応表に集約する

Enum+対応表で一元化
class TaxCategory(Enum):
    STANDARD = auto()
    REDUCED = auto()
    EXEMPT = auto()

@dataclass(frozen=True)
class TaxRule:
    rate: Decimal
    invoice_label: str

TAX_RULES: dict[TaxCategory, TaxRule] = {
    TaxCategory.STANDARD: TaxRule(Decimal("0.10"), "10%対象"),
    TaxCategory.REDUCED: TaxRule(Decimal("0.08"), "8%対象(軽減税率)"),
    TaxCategory.EXEMPT: TaxRule(Decimal("0"), "非課税"),
}

def tax_rule(category: TaxCategory) -> TaxRule:
    return TAX_RULES[category]

区分を str ではなく Enum にすると、タイポは即エラーになり、登録漏れも assert set(TAX_RULES) == set(TaxCategory) のテスト1本で検出できる。

02 区分ごとに計算ロジックが違うなら多態に

Good

値の差では収まらず計算手順まで変わるなら、区分ごとのクラスに振る舞いを持たせる

抽象基底クラスで振る舞いごと一元化
class WithholdingTax(ABC):
    @abstractmethod
    def amount(self, fee: Decimal) -> Decimal: ...

class LectureFeeTax(WithholdingTax):
    def amount(self, fee: Decimal) -> Decimal:
        # 講演料: 100万円以下は10.21%、超過分は20.42%
        if fee <= 1_000_000:
            return (fee * Decimal("0.1021")).quantize(Decimal("1"))
        over = fee - 1_000_000
        return (Decimal("102100") + over * Decimal("0.2042")).quantize(Decimal("1"))

class DesignFeeTax(WithholdingTax):
    def amount(self, fee: Decimal) -> Decimal:
        return (fee * Decimal("0.1021")).quantize(Decimal("1"))

Java の interface に相当するものは Python では ABC(または Protocol)。呼び出し側は tax.amount(fee) と書くだけで、報酬の種類ごとの計算式はそれぞれのクラスの中に閉じる。新しい報酬種別の追加は「クラスを1つ書く」ことであり、既存の分岐をいじる作業ではなくなる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第6章。コード例は原則を自分の題材で表現し直したオリジナル。
6-3

switch 重複を Enum+多態で一元化

「種類ごとに値や振る舞いが変わる」を関数ごとの if-elif で書くと、同じ分岐が何か所にもコピーされる。新しい種類を追加するとき全箇所を漏れなく直す必要があり、1か所でも忘れると静かに間違った値を返す。種類を Enum にし、種類ごとの値と振る舞いを1か所にまとめれば、追加・変更の影響範囲が一点に閉じる。

01 消費税区分ごとの税率・請求書表記

Bad

税区分の分岐が関数ごとに重複。請求書表記の関数だけ非課税の追加を忘れている

同じ分岐があちこちにコピーされる
def tax_rate(category: str) -> Decimal:
    if category == "standard":
        return Decimal("0.10")
    elif category == "reduced":
        return Decimal("0.08")
    elif category == "exempt":
        return Decimal("0")
    return Decimal("0")

def invoice_label(category: str) -> str:
    if category == "standard":
        return "10%対象"
    elif category == "reduced":
        return "8%対象(軽減税率)"
    # "exempt" の追加を忘れても、空文字が返るだけで気づけない
    return ""
Good

区分は Enum、区分ごとの値は不変データとして1つの対応表に集約する

Enum+対応表で一元化
class TaxCategory(Enum):
    STANDARD = auto()
    REDUCED = auto()
    EXEMPT = auto()

@dataclass(frozen=True)
class TaxRule:
    rate: Decimal
    invoice_label: str

TAX_RULES: dict[TaxCategory, TaxRule] = {
    TaxCategory.STANDARD: TaxRule(Decimal("0.10"), "10%対象"),
    TaxCategory.REDUCED: TaxRule(Decimal("0.08"), "8%対象(軽減税率)"),
    TaxCategory.EXEMPT: TaxRule(Decimal("0"), "非課税"),
}

def tax_rule(category: TaxCategory) -> TaxRule:
    return TAX_RULES[category]

区分を str ではなく Enum にすると、タイポは即エラーになり、登録漏れも assert set(TAX_RULES) == set(TaxCategory) のテスト1本で検出できる。

02 区分ごとに計算ロジックが違うなら多態に

Good

値の差では収まらず計算手順まで変わるなら、区分ごとのクラスに振る舞いを持たせる

抽象基底クラスで振る舞いごと一元化
class WithholdingTax(ABC):
    @abstractmethod
    def amount(self, fee: Decimal) -> Decimal: ...

class LectureFeeTax(WithholdingTax):
    def amount(self, fee: Decimal) -> Decimal:
        # 講演料: 100万円以下は10.21%、超過分は20.42%
        if fee <= 1_000_000:
            return (fee * Decimal("0.1021")).quantize(Decimal("1"))
        over = fee - 1_000_000
        return (Decimal("102100") + over * Decimal("0.2042")).quantize(Decimal("1"))

class DesignFeeTax(WithholdingTax):
    def amount(self, fee: Decimal) -> Decimal:
        return (fee * Decimal("0.1021")).quantize(Decimal("1"))

Java の interface に相当するものは Python では ABC(または Protocol)。呼び出し側は tax.amount(fee) と書くだけで、報酬の種類ごとの計算式はそれぞれのクラスの中に閉じる。新しい報酬種別の追加は「クラスを1つ書く」ことであり、既存の分岐をいじる作業ではなくなる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第6章。コード例は原則を自分の題材で表現し直したオリジナル。