[コラム] F# で TDD (@TDD道場#06)
わんくま名古屋勉強会#16 (2011/1/15) の TDD 道場では、 F# で FizzBuzz をやりました。
登壇していただいた 4名の方、 ありがとうございました。 会場には F# を見るのも初めてという方が多かったかと思いますが、 楽しんでいただけましたでしょうか。
テストコードの全体と、 製品コードの途中までは、 「[コラム] F# で NUnit するには」 に掲載してあります。 また、 当日のスライドとソースコードは、 「勉強会などで使った資料」 からダウンロードできます。 ここでは、 コードが成長していく様子と、 最後のリファクタリングについて書いてみます。
テストファーストでは、 失敗する (NUnit ランナーがレッドを表示する) テストケースをひとつ書いて、 それを通すように製品コードを追加・修正してそのテストケースを成功させます (グリーン)。 そのステップを示すと、 FizzBuzz では次のようになります。 (太字にして色を付けた部分が、 そのステップで変更・追加したところ。)
【テストケース】 [<TestCase(1, "1")>]
member x.SayTest (n:int) (expected:string) :unit =
Assert.AreEqual(expected, FizzBuzz.Say(n))
↓
【製品コード】let Say n =
"1"
↓
【テストケース】 [<TestCase(1, "1")>]
[<TestCase(2, "2")>]
member x.SayTest (n:int) (expected:string) :unit =
Assert.AreEqual(expected, FizzBuzz.Say(n))
↓
【製品コード】let Say n =
n.ToString()
↓
【テストケース】 [<TestCase(1, "1")>]
[<TestCase(2, "2")>]
[<TestCase(3, "Fizz")>]
member x.SayTest (n:int) (expected:string) :unit =
Assert.AreEqual(expected, FizzBuzz.Say(n))
↓
【製品コード】let Say n =
if n % 3 = 0 then "Fizz"
else n.ToString()
↓
【テストケース】 [<TestCase(1, "1")>]
[<TestCase(2, "2")>]
[<TestCase(3, "Fizz")>]
[<TestCase(5, "Buzz")>]
member x.SayTest (n:int) (expected:string) :unit =
Assert.AreEqual(expected, FizzBuzz.Say(n))
↓
【製品コード】let Say n =
if n % 3 = 0 then "Fizz"
elif n % 5 = 0 then "Buzz"
else n.ToString()
↓
【テストケース】 [<TestCase(1, "1")>]
[<TestCase(2, "2")>]
[<TestCase(3, "Fizz")>]
[<TestCase(5, "Buzz")>]
[<TestCase(15, "Fizz Buzz")>]
member x.SayTest (n:int) (expected:string) :unit =
Assert.AreEqual(expected, FizzBuzz.Say(n))
↓
【製品コード】let Say n =
if n % 15 = 0 then "Fizz Buzz"
elif n % 3 = 0 then "Fizz"
elif n % 5 = 0 then "Buzz"
else n.ToString()
完成! テストファースト終了。
製品コードが望みの応答を返してくれるようになったので、 一応は完成です。
テストケースをひとつづつこなしていくと、往々にしてこのように if 文の連続になってしまうことがありますが、 これはあまり美しくありませんね。 リファクタリングしたくなります。
最初の if 文の条件は、 n % 15 = 0 となっていますが、 これは n % 3 = 0 && n % 5 = 0 であるとも言えます (問題の意味的には、 後者が本来の姿)。 二つ目は n % 3 = 0 ですが、 これは n % 3 = 0 && n % 5 <> 0 と書いても構わないはずです (n % 5 <> 0 は常に True と評価されますが)。 同様にして、 製品コードを次のように書き直しても構わないでしょう。
let Say n =
if n % 3 = 0 && n % 5 = 0 then "Fizz Buzz"
elif n % 3 = 0 && n % 5 <> 0 then "Fizz"
elif n % 3 <> 0 && n % 5 = 0 then "Buzz"
elif n % 3 <> 0 && n % 5 <> 0 then n.ToString()
上のコードを実際に書くことはないでしょうが、 このような形に書けると分かれば、 パターンマッチ に置き換えることが出来ると気づきます。 すなわち、 次のようにリファクタリングできます。
※ あえてワイルドカードは使っていません。
let Say n =
match (n % 3 = 0, n % 5 = 0) with
| (true, true) -> "Fizz Buzz"
| (true, false) -> "Fizz"
| (false, true) -> "Buzz"
| (false, false) -> n.ToString()
このように F# では、 if 文の連鎖を見たらパターンマッチを考えてみるのがひとつのパターンになっています。
※ そのため、 F# では FizzBuzz に文字列連結や "If (output.Length = 0) Then" ( → リファクタリングはどっちへ進むべきだろう? ) などというコードが登場する余地はありません。
ところで上のパターンマッチによるコード、 何かに似ていませんか?
古株にはピンと来る人も多いでしょう。 そう、 デシジョンテーブルそのものです。
※ このデシジョンテーブルは自己流です。 JIS X 0125 の書き方が正式なんでしょうが、 冗長なので好きじゃありません。
3の倍数 | 5の倍数 | 出力 |
---|---|---|
Yes | Yes | "Fizz Buzz" |
Yes | No | "Fizz" |
No | Yes | "Buzz" |
No | No | その数字 |
F# = 関数型言語 → 数学向き → 業務アプリケーションには不向き
…という連想をされる人もいるようですが、 このようにデシジョンテーブルをそのままコードとして表現できるなど F# は論理を表現するのに優れた言語でもあって、 業務アプリケーションのロジック部分にはむしろ向いているのではないかと思います。
| 固定リンク
「*コラム」カテゴリの記事
- MSTest‐Windows ストア アプリ開発の暗黒大陸 #win8dev_jp #tddadventjp #tddnet(2013.12.13)
- TDD って何だっけ? #tddadventjp(2013.12.06)
- [コラム] テストファーストとは何か?(2012.12.24)
- [コラム] Visual Studio 11 に統合できるテスティング フレームワーク(2012.03.22)
- [コラム] TDD のパターン: Assert First(2012.02.09)
「<NUnit>」カテゴリの記事
- [NEWS] NUnit Test Adapter for VS 2012 and 2013 1.0 RC(2013.09.17)
- [NEWS] NUnit 2.6.2 リリース ~ async/await に対応!(2012.11.16)
- [NEWS] NUnit 2.6.1 リリース ~ NuGet に対応(2012.08.17)
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発 第4回/第5回(2012.07.10)
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発 第2回/第3回(2012.04.13)
この記事へのコメントは終了しました。
コメント