🥪

python dataclass で __post_init__ を使って完全コンストラクタ

2022/10/15に公開

問題

  • pythonのコードベース
  • dataclass使って値オブジェクトを表現している
  • 完全コンストラクタを使いたい
    • 完全コンストラクタとは、不正状態から防護するための設計パターンです。(中略)インスタンス変数をすべて初期化できるだけの引数を持ったコンストラクタを用意します。そしてコンストラクタ内では、ガード節で不正値を弾きます。このように設計することで、生成された段階で正常値だけをもつ完全なインスタンスが生成されます。(『良いコード/悪いコードで学ぶ設計入門』仙塲大也 著)

    • 値オブジェクトの利用時にそのオブジェクトが不正かどうか意識したくないので
      • 意識するとコストがかかるので
  • どうする?

対応

__post_init__ を使う。

以下、価格classを例に取り上げる。


from src.domains.currency.unit import Unit as CurrencyUnit
from src.domains.price.value_object import ValueObject as Price


def test_貨幣量を取得できる():
    p = Price(
        amount=2000,
        unit=CurrencyUnit.JPY,
    )
    assert p.amount == 2000
from dataclasses import dataclass

from src.domains.currency.unit import Unit as CurrencyUnit


@dataclass(frozen=True)
class ValueObject:
    amount: int
    unit: CurrencyUnit

このテストは期待通り通る。

============================= test session starts =============================
collecting ... collected 1 item

test_value_object.py::test_貨幣量を取得できる PASSED                     [100%]

amountが負の場合は例外を出したい。
ので、

def test_貨幣量が負の場合は例外を飛ばす():
    with pytest.raises(ValueError) as e:
        Price(
            amount=-1,
            unit=CurrencyUnit.JPY,
        )
    assert str(e.value) == "amount is negative number."

このテストを通したい。

============================= test session starts =============================
collecting ... collected 2 items

test_value_object.py::test_貨幣量を取得できる PASSED                     [ 50%]
test_value_object.py::test_貨幣量が負の場合は例外を飛ばす FAILED         [100%]

現時点ではテストが通らない。


from dataclasses import dataclass

from src.domains.currency.unit import Unit as CurrencyUnit


@dataclass(frozen=True)
class ValueObject:
    amount: int
    unit: CurrencyUnit

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("amount is negative number.")

__post_init__ 内でバリデーションして例外を飛ばすことでテストが通る。

============================= test session starts =============================
collecting ... collected 2 items

test_value_object.py::test_貨幣量を取得できる PASSED                     [ 50%]
test_value_object.py::test_貨幣量が負の場合は例外を飛ばす PASSED         [100%]

Discussion