1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ミノ駆動本の3章を読んだので、PythonでMoneyクラスを書いた

Posted at

はじめに

ミノ駆動本の第三章内でMoneyクラスを例に「値オブジェクト」「完全コンストラクタ」について説明されていました。
本書はJavaで書かれていたので、Pythonでも同じことを書いてみようと思い、記事を書くことにしました。

※例では通貨の概念がありましたが、簡単にするために通貨は省略しています。

本の内容を何も考えずにPythonで書く

以下のコードが本の内容をPythonに書き直したものになります。

money.py
class Money:
    def __init__(self, amount: int) -> None:
        if amount < 0:
            raise ValueError('金額が0以上ではありません。')
        
        self._amount: Final[int] = amount

    @property
    def amount(self) -> int:
        return self._amount
    
    def add(self, other: 'Money') -> 'Money':
        added: Final[int] = self._amount + other.amount

        return Money(added)
使いかた
usage.py
money1 = Money(100)
money2 = Money(200)
money3 = money1.add(money2)

特殊メソッドで書き直す

上の章の書き方でも正常に動きますが、Pythonの特殊メソッドを使用して書き直してみます。
こうすることで、Moneyクラスのインスタンス同士を足し算することができるようになります。

違いはaddメソッドを、Pythonの特殊メソッドの__add__に変更した部分です。

pythonicmoney.py
class Money:
    def __init__(self, amount: int) -> None:
        if amount < 0:
            raise ValueError('金額が0以上ではありません。')
        
        self._amount: Final[int] = amount
    
    @property
    def amount(self) -> int:
        return self._amount
    
    # 足し算の計算は特殊メソッドを利用する
    def __add__(self, other: 'Money') -> 'Money':
        # Moneyクラス以外とは足し算したくないので、Moneyクラス以外で足し算した場合はエラー
        if not isinstance(other, Money):
            return NotImplemented
        
        added: Final[int] = self._amount + other.amount

        return Money(added)
使いかた

使い方は以下のとおりです。

usage.py
money1 = Money(100)
money2 = Money(200)
money3 = money1 + money2 # <-この部分が異なる

発展させてみる

続いて、本の内容を写経するだけでは面白くないので、少し発展させてみます。

advancementmoney.py
class Money:
    def __init__(self, amount: int) -> None:
        if amount < 0:
            raise ValueError('金額が0以上ではありません。')

        self._amount: Final[int] = amount

    @property
    def amount(self) -> int:
        return self._amount

    def __add__(self, other: 'Money') -> 'Money':
        # Moneyクラス以外とは足し算したくないので、Moneyクラス以外と足し算した場合はエラー
        if not isinstance(other, Money):
            return NotImplemented

        added: Final[int] = self._amount + other.amount

        return Money(added)

    # 引き算のメソッドを追加
    def __sub__(self, other: 'Money') -> 'Money':
        # Moneyクラス以外とは引き算したくないので、Moneyクラス以外と足し算した場合はエラー
        if not isinstance(other, Money):
            return NotImplemented

        subtracted: Final[int] = self._amount - other.amount

        # 金額は0を下回ってほしくないので、マイナスになる場合はエラーとする
        if subtracted < 0:
            raise ValueError('金額が0以下になってしまいます。')

        return Money(subtracted)

    # お金は掛け合わせたり、割ったりしないので、掛け算/割り算は定義しない

    # printで出力したときに読みやすくするために__str__を定義
    def __str__(self) -> str:
        return f'{self.amount} JPY'
使いかた
usage.py
money = Money(100)
print(f'{money=:}') # -> money=100 JPY

added_money = money + Money(200)
print(f'{added_money=:}') # -> added_money=300 JPY

subtracted_money = money - Money(50)
print(f'{subtracted_money=:}') # -> subtracted_money=50 JPY

感想

今回は「値オブジェクト」と「完全コンストラクタ」について学びました。
この2つの概念を利用することによって、コードの可読性があがり、より堅牢なコードを書けることがわかりました。
(ほぼ本に書いてあるような内容そのままの感想ですね:sweat_smile:

この概念を覚えたことで、実際のプロジェクトで扱う概念を整理しやすくなりました。
また、単位の違う値をそれぞれ値オブジェクトで定義することで、計算ミスを減らす事ができることも大きなメリットだと感じました。

例えば、私のプロジェクトでは電力(W)と電力量(Wh)をfloat型で扱っていたのですが、計算するときに値が電力なのか、電力量なのかに注意をはらいながら実装しなければいけません。
それぞれを値オブジェクトにすることによって、IDEが間違いを指摘してくれたり、実行したときにエラーが出るようになるので、以前よりはバグが出にくくなった印象です。

特殊メソッドを使うことについて

特殊メソッドで書き直すの章発展させてみるの章で書いた内容は好みですかね。
addメソッドやsubメソッドで記述する方は、入力補完でどんなメソッドがあるのかわかりやすくなるメリットがあります。
↓こんな感じ
image.png

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?