PythonでTDD入門(4) - インスタンス変数のプライベート化

これまでのサイクルでDollarをバリューオブジェクトとして扱うようになりました。また、Dollarオブジェクトの同値関係が定義され、最初のテストも奇麗に書き換える事ができます。

    def test_multiplication(self):
        five = Dollar(5)
        product = five.times(2)
        self.assertEqual(Dollar(10), product)
        product = five.times(3)
        self.assertEqual(Dollar(15), product)

さらに変数のインライン化を行います。

    def test_multiplication(self):
        five = Dollar(5)
        self.assertEqual(Dollar(10), five.times(2))
        self.assertEqual(Dollar(15), five.times(3))

テストも問題ありません。

テスト駆動を行う事で実装者が最初のユーザになる

これで随分とやりたいことが見える読みやすいコードになってきました。この事は非常に重要です。これこそがTDDの一番の効果とも言えます。
つまり、テストコードを見て解りやすいかどうかは、テスト対象クラスのインターフェイスが使いやすいかどうかに直結するわけです。言い換えれば自らでそのクラスやメソッドの最初の使用者となりレビューをしています。そして、テストとリファクタリングを通して、改善しているのです。
このようにテスト駆動を行うことで、内部的なリファクタリングもそうですが、インターフェイスも解りやすいように改善するという意識が生まれます。メンテナンスをする中ではコードの中身よりもむしろAPIの解りやすさと使いやすさを重視しましょう。

フィールドのカプセル化

テストコードのリファクタリングを行った結果、amountがテストコード、すなわち外部から参照されなくなりました。したがって、amountをプライベート変数とすることができます。
まずはテストコードを書きます。

    def test_amount_private(self):
        try:
           Dollar(5).amount
        except AttributeError:
            pass
        else:
            self.fail()

実行します。

1) Failure:
test_amount_private(tdd.tests.money_test.Money_Test):

amountを呼び出したならばAttributeErrorが発生しなくてはなりませんが、プライベート化していないのでテストが失敗する事を確認です。

Pythonではプライベートなインスタンス変数を定義するには2つのアプローチがあります。1つ目は変数の頭にアンダースコアを1つ付ける方法、2つ目のアプローチはアンダースコアを2つつける方法です。
1つ目(アンダースコアが1つ)の場合、インスタンス変数はシステム的に隠蔽されません。あくまでアンダースコアではじまる変数はプライベート属性なので外から修正しないようにしてくださいというお約束(慣習)です。これに対してアンダースコアを2つ付けた場合は外部からのアクセスが原則としてできないようになります。
最初にアンダースコアを1つ付けてプライベート化してみましょう。

    def test_amount_private(self):
        try:
           Dollar(5)._amount
        except AttributeError:
            pass
        else:
            self.fail()
class Dollar():
    def __init__(self, amount):
        self._amount = amount

    def times(self, multipaier):
        return Dollar(self._amount * multipaier)

    def __eq__(self, other):
        return self._amount == other._amount

_amountにアクセス可能です。これは使う側の問題なので、単体テストではチェックできない部分かと思います。

次に2つにして実行してみます。

    def test_amount_private(self):
        try:
           Dollar(5). __amount
        except AttributeError:
            pass
        else:
            self.fail()
class Dollar():
    def __init__(self, amount):
        self.__amount = amount

    def times(self, multipaier):
        return Dollar(self.__amount * multipaier)

    def __eq__(self, other):
        return self.__amount == other.__amount

今度は成功です。無事にプライベート変数にすることができました。