クラス設計基本の基
初めに
コード設計について「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」を手に取り、学んでいます。本記事はこちらを参考に書いています。
今回はクラス設計について取り上げます。
本題
あるべきクラス設計の基本
クラス設計をする際の大前提として、「クラス単体で動作するよう設計する」 ということが挙げられます。
単体で動作させるために、クラスを構成する要素としては以下の二つになります。
・インスタンス変数
・インスタンス変数の不正状態から守るメソッド
上記二つを意識し、実際にコードを見ながら堅牢なクラス設計について触れていきます。
完全コンストラクタ
まずインスタンス化した時に、インスタンス変数に正常な値が設定されている状態にします。
引数なしで初期化できてしまうデフォルトコンストラクタでは、呼び出し側で必要な引数を意識して渡す必要が出てくるため、不正なインスタンスが出来がちです。
また凝集度の観点でも、初期化ロジックが分散されていると後々の仕様変更の際、保守に時間がかかるなどの問題が出てくると思います。
class Money:
"""金額を表す
Attributes:
amount: 金額値
currency: 通貨単位
"""
def __init__(self, amount: int, currency: Currency):
# ガード節
if amount < 0:
raise ValueError("金額が0円以上ではありません")
if currency == None:
raise ValueError("通過を指定してください")
# privateにしアクセスを制御
self.__amount = amount
self.__currency = currency
上記では完全コンストラクタとしてamount, currencyの値を必ず必要としています。また、このコードでは他にいくつかのテクニックも併せて用いられています。
-
ガード節
不正値を持った値ではインスタンス化できないようにしています。 -
インスタンス変数を不変に
インスタンス変数が上書きできる状態だと、仕様変更で処理を変える際、現在の値に何が入っているのかいちいち気にしなければならなくなります。
変更の場合は新しいインスタンスを防ぐ
外部からの代入は受け入れない形を取りましたが、かといって変更が必要な場合も出てくるかと思います。その際は、インスタンス変数の値を変えるのではなく、その変更値を持った新しいインスタンスを生成するようにします。
class Money:
...
def add(self, other: 'Money') -> 'Money':
"""金額を加算し、新しいMoneyインスタンスを返す"""
added = self.__amount + other
return Money(amount=added, currency=self.__currency)
またotherにはint型ではなく、Money型を使用しています。標準用意されているint型やstring型といった基本データ型(プリミティブ型)を使用すると、意図が異なる値を代入してしまう可能性があり、その際エラーも起きず原因の特定が困難になります。
これまでで一通りのクラス設計は出来たため実際に金額値を取得するgetメソッドを定義し、どのように動作するか検証してみます。
Money
class Money:
"""金額を表す
Attributes:
amount: 金額値
currency: 通貨単位
"""
def __init__(self, amount: int, currency: Currency):
if amount < 0:
raise ValueError("金額が0円以上ではありません")
if currency == None:
raise ValueError("通過を指定してください")
self.__amount = amount
self.__currency = currency
def add(self, other: 'Money') -> 'Money':
"""金額を加算し、新しいMoneyインスタンスを返す"""
added = self.__amount + other
return Money(amount=added, currency=self.__currency)
def get(self) -> int:
"""金額値(amount)を取得する"""
return self.__amount
検証
money = Money(amount=-200, currency='円')
# => ValueError: 金額が0円以上ではありません
money = Money(amount=300, currency=None)
#=> ValueError: 通過を指定してください
money = Money(amount=300, currency='円')
money.__amount = money.__amount + 500
#=> AttributeError: 'Money' object has no attribute '__amount'
money = money.add(500)
print(money.get())
#=> 800
不正値の混入とインスタンス変数の変更を防ぎ、単体で動作が可能なクラス設計ができたかと思います。
Discussion