見出し画像

今日や現在時刻は副作用

例題

りんごの価格は通常100円ですが、月曜日は特価で2割引きだとします。このメソッドを次のように実装したとしましょう。

そのまま書くとこうなります

Option Explicit On
Option Strict On
Option Infer On

Module Program
    Function AppleSellingPrice() As Integer
        Dim regularPrice = 100
        ' 月曜特価
        If Now.DayOfWeek = DayOfWeek.Monday Then
            Return CInt(regularPrice * 0.8)
        End If
        Return regularPrice
    End Function
End Module

一見よさそうですが…

問題点

テストを書くことで問題が明らかになります

Option Explicit On
Option Strict On
Option Infer On

Module Program

    Function AppleSellingPrice() As Integer
        Dim regularPrice = 100
        ' 月曜特価
        If Now.DayOfWeek = DayOfWeek.Monday Then
            Return CInt(regularPrice * 0.8)
        End If
        Return regularPrice
    End Function

    Sub AppleSellingPriceTest()
        If AppleSellingPrice() <> 100 Then
            Throw New Exception("Test failed…")
        End If
    End Sub

    Sub Main()
        AppleSellingPriceTest()
    End Sub
End Module

月曜日になると落ちる「不安定なテスト (Erratic Test)」の出来上がりです!
繰り返し可能でないテストはよくないテスト。

とはいえ、原因はテストではなく実装の方です。

どうすればよかったか

メソッドに引数で曜日を渡すことで、「時」という副作用から解き放たれます。

Option Explicit On
Option Strict On
Option Infer On

Module Program

    Function AppleSellingPrice(currentDayOfWeek As DayOfWeek) As Integer
        Dim regularPrice = 100
        ' 月曜特価
        If currentDayOfWeek = DayOfWeek.Monday Then
            Return CInt(regularPrice * 0.8)
        End If
        Return regularPrice
    End Function

    Sub AppleSellingPriceTest()
        If AppleSellingPrice(DayOfWeek.Sunday) <> 100 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Monday) <> 80 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Tuesday) <> 100 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Wednesday) <> 100 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Thursday) <> 100 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Friday) <> 100 Then Throw New Exception("Test failed…")
        If AppleSellingPrice(DayOfWeek.Saturday) <> 100 Then Throw New Exception("Test failed…")
    End Sub

    Sub Main()
        AppleSellingPriceTest()
    End Sub
End Module

これで今日が何曜日であっても正しく動作するテストができました。

元のメソッドとシグネチャが変わっていることが気になる方もいるかもしれません。ここまでテストできるのであれば、テストできないオーバーロードを書いてもかなり安心でしょう。

    Function AppleSellingPrice() As Integer
        Return AppleSellingPrice(Now.DayOfWeek)
    End Function

    Function AppleSellingPrice(currentDayOfWeek As DayOfWeek) As Integer
        Dim regularPrice = 100
        ' 月曜特価
        If currentDayOfWeek = DayOfWeek.Monday Then
            Return CInt(regularPrice * 0.8)
        End If
        Return regularPrice
    End Function

この例では曜日 DayOfWeek でしたが、直接 Now を使う場合は Date 型の引数で渡すことができます。

今回はかなり問題を単純化しています。実際にはある程度の規模のクラスやメソッド内部で Today や Now を呼び出していることが多々あると思います。それらの実装には潜在的な問題があり、テストできるようにするリファクタリングをする必要があるにもかかわらず、そのリファクタリングのためのテストが書けないというつらさがあります。

最初から、副作用は外から渡すようにコードを書きたいですね…。ああ。

原則

  • 副作用はメソッドの外に出す。

  • 今日や現在時刻は副作用

ヘッダー画像の出典


いいなと思ったら応援しよう!