今日や現在時刻は副作用
例題
りんごの価格は通常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 を呼び出していることが多々あると思います。それらの実装には潜在的な問題があり、テストできるようにするリファクタリングをする必要があるにもかかわらず、そのリファクタリングのためのテストが書けないというつらさがあります。
最初から、副作用は外から渡すようにコードを書きたいですね…。ああ。
原則
副作用はメソッドの外に出す。
今日や現在時刻は副作用