Open in Gitpod
初めに この記事は テスト駆動開発から始める Ruby 入門 -2 時間で TDD とリファクタリングのエッセンスを体験する- の外伝エピソードです。ちなみに前半の元ネタは テスト駆動開発 の付録 B フィボナッチ を Ruby で実装したものです。後半はオリジナルエピソードでは言及できなかったアルゴリズムの プロファイリング と ベンチマーク の実施に関して解説しています。
前提として、テスト駆動開発から始める Ruby 入門 -ソフトウェア開発の三種の神器を準備する- で開発環境を構築したところから始まります。別途、セットアップ済み環境 を用意していますのでこちらからだとすぐに始めることが出来ます。
前半の用語の詳細については エピソード 1 で解説しています。後半の用語の詳細については エピソード 3 で解説していますので興味があれば御一読ください。
パフォーマンスチューニングから始めるテスト駆動開発 概要 フィボナッチ数 を計算するプログラムを テスト駆動開発 で作ります。
初めに TODO リスト をプログラミング作業をリストアップします。次に、最初に失敗するテストを作成します。 その後 仮実装 でベタ書き値を返すテストを実行します。 それから 三角測量 を使って慎重にアルゴリズムを一般化していきます。そして、 明白な実装 によりアルゴリズムを完成させます。
アルゴリズムが完成したら リファクタリング を実施してコードベースを 動作するきれいなコード に洗練していきます。
動作するきれいなコード になったらパフォーマンスの検証をするためパフォーマンスチューニングを実施します。 パフォーマンスチューニングでは プロファイラ を使ったプログラムのボトルネック調査を実施します。アルゴリズムのパフォーマンスを改善したら別途追加したアルゴリズムと ベンチマーク を実施してどのアルゴリズムを採用するかを決定します。
仕上げは、 モジュール分割 により Ruby アプリケーションとしてリリースします。
仕様 仕様は以下の通りです。
n 番目のフィボナッチ数を Fn で表すと、Fn は再帰的に
F0 = 0,
F1 = 1,
Fn + 2 = Fn + Fn + 1 (n ≧ 0)
で定義される。これは、2 つの初期条件を持つ漸化式である。
この数列 (Fn)はフィボナッチ数列(フィボナッチすうれつ、(英: Fibonacci sequence)と呼ばれ、
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …(オンライン整数列大辞典の数列 A45) と続く。最初の二項は 0, 1 であり、以後どの項もその直前の 2 つの項の和となっている。
— Wikipedia
表形式にすると以下のようになります。
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
18
19
…
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
…
TODO リスト
TODO リスト
何をテストすべきだろうか—-着手する前に、必要になりそうなテストをリストに書き出しておこう。
— テスト駆動開発
TODO リスト を書き出す取っ掛かりとして仕様で定義されている内容からプログラムで実施できる内容に分解してきましょう。仕様では以下のように定義されているので。
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
18
19
…
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
…
最初のタスクは 0を渡したら0を返す
振る舞いをするプログラムを作ることにしましょう。
同様のパターンで他のタスクも切り出してみましょう。
とりあえず、3件ほどタスクとして切り出したので TODO リスト の作成は一旦終了してプログラミング作業に入るとしましょう。
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
仮実装
仮実装を経て本実装へ
失敗するテストを書いてから、最初に行う実装はどのようなものだろうか—-ベタ書きの値を返そう。
— テスト駆動開発
0 を渡したら 0 を返す 早速、 TODO リスト の1つ目から片付けていくとしましょう。
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
まずは最初に失敗するテストを書きますがまずは以下のサンプルコードを使ってテスティングフレームワークの動作確認をしておきましょう。今回利用する Ruby のテスティングフレームワークは minitest です。 test
フォルダ以下に fibonacci_test.rb
ファイルを追加して以下のコードを入力します。
test/fibonacci_test.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' class FibonacciTest < Minitest::Test def greeting 'hello world' end def test_greeting assert_equal 'hello world' , greeting end end
今回テスト結果を見やすくするため minitest/reporters
という gem を使っているのでまずインストールしておきます。
1 $ gem install minitest-reporters
gem インストールが完了したらコマンドラインに ruby test/fibonacci_test.rb
コマンドを入力してテストを実施します。
1 2 3 4 5 6 7 8 $ ruby test /fibonacci_test.rb Started with run options --seed 28548 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01040s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ...
テストは無事実行されたようですね。続いてテストが失敗するか確認しておきましょう。 greeting
メソッドの hello world
を hello world!
に変更してテストを実行します。
1 2 3 4 5 6 7 ... class Fibonacci < Minitest::Test def greeting 'hello world!' end ... end
テストは失敗して以下のようなメッセージが表示されました。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ruby test /fibonacci_test.rb Started with run options --seed 30787 FAIL["test_greeting" , <Minitest::Reporters::Suite:0x000055eaefeef5e0 @name="Fibonacci" >, 0.003157061990350485] test_greeting Expected: "hello world" Actual: "hello world!" test /fibonacci_test.rb:13:in `test_greeting` 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00398s 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
テスティングフレームワークのセットアップと動作確認が終了したので最初の失敗するテストを書きます。まずは アサーションファースト でサンプルコードを削除して以下のコードにします。
1 2 3 4 5 6 ... class FibonacciTest < Minitest::Test def test_fibonacci assert_equal 0 , fib(0 ) end end
テストは無事?失敗します。
1 2 3 4 5 6 7 8 9 10 11 12 $ ruby test /fibonacci_test.rb Started with run options --seed 21656 ERROR["test_fibonacci" , <Minitest::Reporters::Suite:0x0000559acae8d068 @name="FibonacciTest" >, 0.001314591965638101] test_fibonacci Minitest::UnexpectedError: NoMethodError: undefined method `fib' for #<FibonacciTest:0x0000559acae8d860> test/fibonacci_test.rb:9:in `test_fibonacci' ` 1/1: [========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00419s 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
まずは 仮実装 でテストを通すようにしましょう。
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) 0 end def test_fibonacci assert_equal 0 , fib(0 ) end end
テストはレッドからグリーンになりました。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 2885 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00352s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
テストが通ったのでバージョン管理システムにコミットしておきます。
1 2 $ git add . $ git commit -m 'test: 0を渡したら0を返す'
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
三角測量
三角測量
テストから最も慎重に一般化を引き出すやり方はどのようなものだろうか—-2つ以上の例があるときだけ、一般化を行うようにしよう。
— テスト駆動開発
1 を渡したら 1 を返す 1つ目の TODO リスト を片付けたので2つ目の TODO リスト に取り掛かるとしましょう。
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
テストファースト アサーションファースト なのでまずはテストを追加するとこから始めます。
1 2 3 4 5 6 7 8 9 10 11 ... class FibonacciTest < Minitest::Test def fib (n) 0 end def test_fibonacci assert_equal 0 , fib(0 ) assert_equal 1 , fib(1 ) end end
テストは失敗します。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ruby test /fibonacci_test.rb Started with run options --seed 21207 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x000056525007ccb0 @name="FibonacciTest" >, 0.0014098359970375896] test_fibonacci Expected: 1 Actual: 0 test /fibonacci_test.rb:14:in `test_fibonacci` 1/1: [========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00196s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips
仮実装 で 0 しか返さないベタ書きのコードなのだから当然ですよね。0 ならば 0 を返してそれ以外の場合は 1 を返すようにプログラムを変更します。
1 2 3 4 5 6 7 8 9 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? 1 end ... end
プログラムの変更によりテストはレッドからグリーンに戻りました。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 58331 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00169s 1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
ここでコミットしておきます。
1 2 $ git add . $ git commit -m 'test: 1を渡したら1を返す'
リファクタリング
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
次の TODO リスト に着手する前にテストケース内の重複が気になり始めたので、共通部分をアサーションからくくり出して、入力値と期待値の組でテストを回すようにテストコードを リファクタリング します。
1 2 3 4 5 6 7 8 9 10 ... class Fibonacci < Minitest::Test... def test_fibonacci cases = [[0 , 0 ], [1 , 1 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
テストを実行してプログラムが壊れていないことを確認します。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 5991 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00200s 1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
プログラムが壊れていないことが確認できたのでコミットしておきます。
1 2 $ git add . $ git commit -m 'refactor: アルゴリズムの置き換え'
1 を渡したら 2 を返す
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
テストコードの リファクタリング を実施したので続いて TODO リスト の3つ目に着手します。まずは アサーション の追加ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? 1 end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
おや、今回はプロダクトコードを変更しなくてもテストは通るようです。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 26882 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00287s 1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
ここでコミットしておきます。
1 2 $ git add . $ git commit -m 'test: 1を渡したら2を返す'
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
明白な実装
明白な実装
シンプルな操作を実現するにはどうすればいいだろうか—-そのまま実装しよう。
仮実装や三角測量は、細かく細かく刻んだ小さなステップだ。だが、ときには実装をどうすべきか既に見えていることが。 そのまま進もう。例えば先ほどの plus メソッドくらいシンプルなものを仮実装する必要が本当にあるだろうか。 普通は、その必要はない。頭に浮かんだ明白な実装をただ単にコードに落とすだけだ。もしもレッドバーが出て驚いたら、あらためてもう少し歩幅を小さくしよう。
— テスト駆動開発
3 を渡したら 2 を返す 最初に定義した TODO リスト の内容は完了しましたがプログラムの一般化にはまだテストケースが足りないでしょう。3 を渡した場合のテストケースを追加します。
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
3 を渡したら 2 を返す
テストケースを追加してテストを実施します。
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test... def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
テストが失敗しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ruby test /fibonacci_test.rb Started with run options --seed 7598 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x000055c987498120 @name="FibonacciTest" >, 0.00104286998976022] test_fibonacci Expected: 2 Actual: 1 test /fibonacci_test.rb:17:in `block in test_fibonacci'' test /fibonacci_test.rb:16:in `each' test/fibonacci_test.rb:16:in `test_fibonacci' 1/1: [========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00160s 1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
2 までは 1 を返すので条件分岐を追加します。
1 2 3 4 5 6 7 8 9 class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 1 end ... end
まだ、失敗したままです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ruby test /fibonacci_test.rb Started with run options --seed 26066 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x0000562bc96ee330 @name="Fibonacci" >, 0.0055934099946171045] test_fibonacci Expected: 2 Actual: 1 test /fibonacci_test.rb:24:in `block in test_fibonacci' test/fibonacci_test.rb:23:in `each' test /fibonacci_test.rb:23:in `test_fibonacci'' 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00882s 1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
どの条件にも該当としない場合は 2 を返すように変更します。
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 2 end ... end
グリーンになりました。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 25117 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01680s 1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
ここでコミットしておきます。
1 2 $ git add . $ git commit -m 'test: 3を渡したら2を返す'
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
3 を渡したら 2 を返す
フィボナッチ数計算 そろそろゴールが見えてきました。TODO リスト を追加してフィボナッチ数計算アルゴリズムを完成させましょう。
0
1
2
3
4
…
0
1
1
2
3
…
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
3 を渡したら 2 を返す
4 を渡したら 3 を返す
テストファースト アサートファースト です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 2 end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ruby test /fibonacci_test.rb Started with run options --seed 34595 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x0000564fdbd6dfe0 @name="Fibonacci" >, 0.005386559059843421] test_fibonacci Expected: 3 Actual: 2 test /fibonacci_test.rb:24:in `block in test_fibonacci' test/fibonacci_test.rb:23:in `each' test /fibonacci_test.rb:23:in `test_fibonacci'' 1/1: [==========================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01030s 1 tests, 5 assertions, 1 failures, 0 errors, 0 skips
最後に 2 を返すのではなく合計値をかえすのだから
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 1 + 1 end ... end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ruby test /fibonacci_test.rb Started with run options --seed 10848 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x00005621247c9f48 @name="Fibonacci" >, 0.0007573128677904606] test_fibonacci Expected: 3 Actual: 2 test /fibonacci_test.rb:24:in `block in test_fibonacci' test/fibonacci_test.rb:23:in `each' test /fibonacci_test.rb:23:in `test_fibonacci'' 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00130s 1 tests, 5 assertions, 1 failures, 0 errors, 0 skips
一つ前の fib
の結果を足すのだから
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 fib(n - 1 ) + 1 end ... end
グリーンになりました。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 25629 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00467s 1 tests, 5 assertions, 0 failures, 0 errors, 0 skips
ここでコミット…しないで今回は更に進めます。 TODO リスト を追加します。
0
1
2
3
4
5
…
0
1
1
2
3
5
…
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
3 を渡したら 2 を返す
4 を渡したら 3 を返す
5 を渡したら 5 を返す
テストケースを追加して
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test... def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
レッド
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ruby test /fibonacci_test.rb Started with run options --seed 54754 FAIL["test_fibonacci" , <Minitest::Reporters::Suite:0x000055c42397e108 @name="Fibonacci" >, 0.00174815789796412] test_fibonacci Expected: 5 Actual: 4 test /fibonacci_test.rb:24:in `block in test_fibonacci' test/fibonacci_test.rb:23:in `each' test /fibonacci_test.rb:23:in `test_fibonacci'' 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00237s 1 tests, 6 assertions, 1 failures, 0 errors, 0 skips
結局 1 つ前と 2 つ前の fib
の結果を合計して返しているのだから
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n <= 2 fib(n - 1 ) + fib(n - 2 ) end ... end
グリーン
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 8399 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00107s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
一般化ができたので 0 の場合と 1 の場合は与えらた値を返せば良くなったので
1 2 3 4 5 6 7 8 9 10 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + fib(n - 2 ) end ... end
リファクター
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 42476 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00162s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
フィボナッチ数計算アルゴリズムが完成したのでコミットします。
1 2 $ git add . $ git commit -m 'feat: フィボナッチ数計算'
0 を渡したら 0 を返す
1 を渡したら 1 を返す
2 を渡したら 1 を返す
3 を渡したら 2 を返す
4 を渡したら 3 を返す
5 を渡したら 5 を返す
リファクタリング
リファクタリング(名詞) 外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。
— リファクタリング(第 2 版)
リファクタリングする(動詞) 一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。
— リファクタリング(第 2 版
アルゴリズムの実装は出来ましたがアプリケーションとしては不十分なので リファクタリング を適用してコードを 動作するきれいなコード に洗練していきます。
クラスの抽出 まず、テストケース内でメソッドを定義していますがこれでは一つのクラスでアルゴリズムの実行とテストの実行という2つの責務を FibonacciTest
クラスが担当しています。 単一責任の原則 に違反しているので クラスの抽出 を実施して責務を分担させましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... class FibonacciTest < Minitest::Test def fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + 1 end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
Fibonacci
クラスを作成して クラスメソッド の Fibonacci.fib
をコピー&ペーストで作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... class Fibonacci def self .fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + fib(n - 2 ) end end class FibonacciTest < Minitest::Test def self .fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + fib(n - 2 ) end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], fib(i[0 ]) end end end
テストが壊れていないことを確認したら FibonacciTest
クラス内の クラスメソッド FIbonacciTest.fib
を削除して フィクスチャー setup
メソッドを作成して インスタンス変数 @fib
に Fibonacci
クラスの参照を代入します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... class Fibonacci def self .fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + fib(n - 2 ) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.fib(i[0 ]) end end end
テストが壊れていないかを確認します。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 40694 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00393s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
クラスの抽出 の リファクタリング 適用が完了したのでコミットします。
1 2 $ git add . $ git commit -m 'refactor: クラスの抽出'
変数名の変更 続いて、 Fibonacci
クラスに移動した クラスメソッド ですが引数が n
というのは分かりづらいですね。
1 2 3 4 5 6 7 8 9 10 ... class Fibonacci def self .fib (n) return 0 if n.zero? return 1 if n == 1 fib(n - 1 ) + fib(n - 2 ) end end ...
ここは省略せず、引数の型を表す名前に変更して可読性を上げておきましょう。
1 2 3 4 5 6 7 8 9 10 ... class Fibonacci def self .fib (number) return 0 if number.zero? return 1 if number == 1 fib(number - 1 ) + fib(number - 2 ) end end ...
テストが壊れていないか確認します。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 37760 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00744s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
コミットします。
1 2 $ git add . $ git commit -m 'refactor: 変数名の変更'
メソッド名の変更 Fibonacci
クラスの クラスメソッド Fibonacci.fib
はフィボナッチ数を計算するメソッドですが名前が紛らわしいので メソッド名の変更 を適用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... class Fibonacci def self .fib (number) return 0 if number.zero? return 1 if number == 1 fib(number - 1 ) + fib(number - 2 ) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.fib(i[0 ]) end end end
インスタンスメソッド を fib
から calc
に変更します。今回は呼び出し先の FibonacciTest
のテストコードも修正する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... class Fibonacci def self .calc (number) return 0 if number.zero? return 1 if number == 1 calc(number - 1 ) + calc(number - 2 ) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.calc(i[0 ]) end end end
テストが壊れていないか確認します。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 15099 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00285s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
メソッド名の変更 の適用が完了したのでコミットします。
1 2 $ git add . $ git commit -m 'refactor: メソッド名の変更'
パフォーマンスチューニング
心がけるべきことは、他のパフォーマンス分析とおなじように、実際のデータを使い、リアルな利用パターンを試し、プロファイリングを行ってからでないと、パフォーマンスを問題にする資格はない、ということだ。
— テスト駆動開発
これまでのテストケースでは小さな値を使ってきましたが大きな値の場合のプログラムの挙動が問題無いか確認しておく必要があります 100番目までのフィボナッチ数列 を参考に大きな値の場合のテストケースを追加してアプリケーションのパフォーマンスを検証しましょう。
メモ化によるパフォーマンス改善 TODO リスト に新しいタスクを追加します。
0
1
…
38
39
40
…
0
1
…
39088169
63245986
102334155
…
テストケースを追加します。
1 2 3 4 5 6 7 ... class FibonacciTest < Minitest::Test... def test_large_number assert_equal 102_334_155 , @fib.calc(40 ) end end
テストを実行します
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 1160 2/2: [=========================================] 100% Time: 00:00:51, Time: 00:00:51 Finished in 51.15914s 2 tests, 7 assertions, 0 failures, 0 errors, 0 skips
テストが完了するのが随分遅くなってしまいました。これはアルゴリズムを改善する必要がありそうです。 まずは プロファイラ を使って実行状況を確認します。今回は profile ライブラリ を使います。
1 2 3 4 $ ruby -r profile test /fibonacci_test.rb Started with run options --seed 42383 2/1: [====================== ] 50% Time: 00:00:00, ETA: 00:00:00
処理が終わらないようなら ctr-c
で強制終了すれば結果が出力されます。出力内容の Fibonacci.calc
がフィボナッチ数計算メソッド実行部分です。
1 2 3 4 5 6 ... % cumulative self self total time seconds seconds calls ms/call ms/call name 192.39 25.50 25.50 2 12750.69 12750.69 Thread::Queue 75.32 35.49 9.98 246940 0.04 1.65 Fibonacci.calc ....
再帰呼び出しが何度も実行された結果パフォーマンスを低下させているようです。ここは メモ化 を使ってパフォーマンスを改善させましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ... class Fibonacci def self .calc (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = calc(number - 1 , memo) + calc(number - 2 , memo) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.calc(i[0 ]) end end def test_large_number assert_equal 102_334_155 , @fib.calc(40 ) end end
プロファイラ で確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ ruby -r profile test /fibonacci_test.rb Started with run options --seed 20468 2/2: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.04214s 2 tests, 7 assertions, 0 failures, 0 errors, 0 skips % cumulative self self total time seconds seconds calls ms/call ms/call name ... 12.09 0.06 0.06 2 32.09 32.09 Thread::Queue ... 1.33 0.26 0.01 105 0.07 1.41 Fibonacci.calc ...
一気に再帰呼び出し回数が減りパフォーマンスを改善することが出来ましたのでコミットします。
1 2 $ git add . $ git commit -m 'perf: メモ化によるパフォーマンス改善'
ベンチマーク 続いて、異なるフィボナッチ数計算アルゴリズムを実装してどのアルゴリズムを採用するべきかを ベンチマーク を取って判断したいと思います。
ループ処理による実装 まずはループ処理によるフィボナッチ数計算のアルゴリズムを実装します。以下が テストファースト アサートファースト で作成したコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ... class Fibonacci def self .calc (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = calc(number - 1 , memo) + calc(number - 2 , memo) end def self .calc2 (number) a = 0 b = 1 c = 0 (0 ...number).each do |i| a = b b = c c = a + b end c end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.calc(i[0 ]) end end def test_large_number assert_equal 102_334_155 , @fib.calc(40 ) end def test_large_number_calc2 assert_equal 102_334_155 , @fib.calc2(40 ) end end
1 2 3 4 5 6 $ ruby test /fibonacci_test.rb -n test_large_number_calc2 Started with run options -n test_large_number_calc2 --seed 18167 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00123s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
テストが通ることを確認したらコミットします。
1 2 $ git add . $ git commit -m 'feat: ループ処理による実装'
一般項による実装 フィボナッチ数列の一般項 で定義されているのでこれを テストファースト アサートファースト で実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ... class Fibonacci def self .calc (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = calc(number - 1 , memo) + calc(number - 2 , memo) end def self .calc2 (number) a = 0 b = 1 c = 0 (0 ...number).each do |i| a = b b = c c = a + b end c end def self .calc3 (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.calc(i[0 ]) end end def test_large_number assert_equal 102_334_155 , @fib.calc(40 ) end def test_large_number_calc2 assert_equal 102_334_155 , @fib.calc2(40 ) end def test_large_number_calc3 assert_equal 102_334_155 , @fib.calc3(40 ) end end
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb -n test_large_number_calc3 Started with run options -n test_large_number_calc3 --seed 55659 1/1: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00111s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
テストが壊れていないか確認したらコミットします。
1 2 $ git add . $ git commit -m 'feat: 一般項による実装'
メソッド名の変更 各アルゴリズムのメソッド名が calc
では分かりづらいので メソッド名の変更 を適用して リファクタリング します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ... class Fibonacci def self .recursive (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = recursive(number - 1 , memo) + recursive(number - 2 , memo) end def self .calc2 (number) a = 0 b = 1 c = 0 (0 ...number).each do |i| a = b b = c c = a + b end c end def self .calc3 (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.recursive(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @fib.recursive(40 ) end def test_large_number_calc2 assert_equal 102_334_155 , @fib.calc2(40 ) end def test_large_number_calc3 assert_equal 102_334_155 , @fib.calc3(40 ) end end
まず、最初に実装した再帰呼び出しアルゴリズムのメソッド名を Fibonacci.calc
から Fibonacci.recursive
に変更します。
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 15174 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00137s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
続いて、ループアルゴリズムのメソッド名を Fibonacci.calc2
から Fibonacci.loop
に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Fibonacci def self .recursive (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = recursive(number - 1 , memo) + recursive(number - 2 , memo) end def self .loop (number) a = 0 b = 1 c = 0 (0 ...number).each do |i| a = b b = c c = a + b end c end def self .calc3 (number) a = ((1 + Math.sqrt(5 )) / 2 ) ** number b = ((1 - Math.sqrt(5 )) / 2 ) ** number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.recursive(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @fib.recursive(40 ) end def test_large_number_loop assert_equal 102_334_155 , @fib.loop(40 ) end def test_large_number_calc3 assert_equal 102_334_155 , @fib.calc3(40 ) end end
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 28586 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00188s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
最後に、一般項アルゴリズムのメソッド名を Fibonacci.calc3
から Fibonacci.general_term
に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ... class Fibonacci def self .recursive (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = recursive(number - 1 , memo) + recursive(number - 2 , memo) end def self .loop (number) a = 0 b = 1 c = 0 (0 ...number).each do |i| a = b b = c c = a + b end c end def self .general_term (number) a = ((1 + Math.sqrt(5 )) / 2 ) ** number b = ((1 - Math.sqrt(5 )) / 2 ) ** number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.recursive(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @fib.recursive(40 ) end def test_large_number_loop assert_equal 102_334_155 , @fib.loop(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @fib.general_term(40 ) end end
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 42729 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00736s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
変更によりテストが壊れていないことを確認したらコミットします。
1 2 $ git add . $ git commit -m 'refactor: メソッド名の変更'
サブクラスによるタイプコードの置き換え 1 現在の Fibonacci
クラスはアルゴリズムを追加する場合クラスを編集する必要があります。その際に既存のアルゴリズムを壊してしまう可能性があります。これは オープン・クローズド原則 に違反しているので サブクラスによるタイプコードの置き換え を適用してアルゴリズムを カプセル化 して、安全に追加・変更できる設計に リファクタリング します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 ... class Fibonacci def self .recursive (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = recursive(number - 1 , memo) + recursive(number - 2 , memo) end def self .loop (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end def self .general_term (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciRecursive def calc (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = calc(number - 1 , memo) + calc(number - 2 , memo) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci @recursive = FibonacciRecursive.new end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.calc(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.calc(40 ) end def test_large_number_loop assert_equal 102_334_155 , @fib.loop(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @fib.general_term(40 ) end end
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 12762 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00130s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
まず、クラスの抽出 により再帰呼び出しアルゴリズムの メソッドオブジェクト FibonacciRecursive
クラスを作成して テスト フィクスチャー で インスタンス化 して インスタンス変数 にオブジェクトの参照を代入します。ここではメソッドの呼び出しが exec
に変更されているのでテストコードもそれに合わせて変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 ... class Fibonacci def self .loop (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end def self .general_term (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciRecursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci @recursive = FibonacciRecursive.new end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.exec(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.exec(40 ) end def test_large_number_loop assert_equal 102_334_155 , @fib.loop(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @fib.general_term(40 ) end end
まだ、 仕掛ですがコードが壊れていない状態でコミットをしておきます。
1 2 $ git add . $ git commit -m 'refactor(WIP): サブクラスによるタイプコードの置き換え'
サブクラスによるタイプコードの置き換え 2 続いて、 メソッドオブジェクト FibonacciLoop
クラスを抽出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 ... class Fibonacci def self .general_term (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciRecursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end class FibonacciLoop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci @recursive = FibonacciRecursive.new @loop = FibonacciLoop.new end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.exec(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.exec(40 ) end def test_large_number_loop assert_equal 102_334_155 , @loop.exec(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @fib.general_term(40 ) end end
1 2 3 4 5 6 $ ruby test /fibonacci_test.rbStarted with run options --seed 33171 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00337s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
コミットします。
1 2 $ git add . $ git commit -m 'refactor(WIP): サブクラスによるタイプコードの置き換え'
サブクラスによるタイプコードの置き換え 3 続いて、 メソッドオブジェクト FibonacciGeneralTerm
クラスを抽出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 ... class Fibonacci end class FibonacciRecursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end class FibonacciLoop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end class FibonacciGeneralTerm def exec (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @fib = Fibonacci @recursive = FibonacciRecursive.new @loop = FibonacciLoop.new @general_term = FibonacciGeneralTerm.new end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.exec(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.exec(40 ) end def test_large_number_loop assert_equal 102_334_155 , @loop.exec(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @general_term.exec(40 ) end end
1 2 3 4 5 6 $ ruby test /fibonacci_test.rbStarted with run options --seed 65058 4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01576s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
コミットします。
1 2 $ git add . $ git commit -m 'refactor(WIP): サブクラスによるタイプコードの置き換え'
サブクラスによるタイプコードの置き換え 4 最後に、 Fibonacci
クラスに Strategy パターン を適用して各アルゴリズムの実行を 委譲 します。
Strategy パターン
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 ... class Fibonacci def initialize (algorithm) @algorithm = algorithm end def exec (number) @algorithm.exec(number) end end class FibonacciRecursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end class FibonacciLoop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end class FibonacciGeneralTerm def exec (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end class FibonacciTest < Minitest::Test def setup @recursive = Fibonacci.new(FibonacciRecursive.new) @loop = Fibonacci.new(FibonacciLoop.new) @general_term = Fibonacci.new(FibonacciGeneralTerm.new) end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.exec(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.exec(40 ) end def test_large_number_loop assert_equal 102_334_155 , @loop.exec(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @general_term.exec(40 ) end end
サブクラスによるタイプコードの置き換え の適用が完了したのでコメントから (WIP)
を外してコミットします。
1 2 $ git add . $ git commit -m 'refactor: サブクラスによるタイプコードの置き換え'
ファイル分割 続いてテストとアプリケーションを分割します。 lib
ディレクトリを作成して fibonacci.rb
ファイルを追加してアプリケーションコード部分をカット&ペーストします。
lib/fibonacci.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Fibonacci def initialize (algorithm) @algorithm = algorithm end def exec (number) @algorithm.exec(number) end end class FibonacciRecursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end class FibonacciLoop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end class FibonacciGeneralTerm def exec (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end
続いて、分割した fibonacci.rb
ファイル内に定義されたクラスを読み込むようにテストクラスを修正します。 ファイルの読み込みには require
を使います。
test/fibonacci_test.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fibonacci' class FibonacciTest < Minitest::Test def setup @fib = Fibonacci.new(FibonacciRecursive.new) @recursive = Fibonacci.new(FibonacciRecursive.new) @loop = Fibonacci.new(FibonacciLoop.new) @general_term = Fibonacci.new(FibonacciGeneralTerm.new) end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @fib.calc(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.calc(40 ) end def test_large_number_loop assert_equal 102_334_155 , @loop.calc(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @general_term.calc(40 ) end end
1 2 3 4 5 6 7 $ ruby test /fibonacci_test.rb Started with run options --seed 39723 4/4: [==========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00227s 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips
分割したファイルからクラスが読み込まれテストが通ることを確認したらコミットします。
1 2 $ git add . $ git commit -m 'feat: ファイル分割'
ベンチマークの実施 ベンチマーク を実施する準備が出来たので test
ディレクトリに以下の fibonacci_benchmark.rb
ファイルを追加します。
test/fibonacci_benchmark.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 require 'minitest' require 'minitest/autorun' require 'minitest/benchmark' require './lib/fibonacci' class FibonacciTestBenchmark < Minitest::Benchmark def setup @recursive = Fibonacci.new(FibonacciRecursive.new) @loop = Fibonacci.new(FibonacciLoop.new) @general_term = Fibonacci.new(FibonacciGeneralTerm.new) end def bench_recursive assert_performance_constant do |_n| 1000 .times do |i| @recursive.exec(i) end end end def bench_loop assert_performance_constant do |_n| 1000 .times.each do |i| @loop.exec(i) end end end def bench_general_term assert_performance_constant do |_n| 1000 .times.each do |i| @general_term.exec(i) end end end end
ベンチマーク を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ruby test /fibonacci_benchmark.rb Run options: --seed 1009 bench_recursive 0.438420 0.436003 0.437170 0.453267 0.428123 .bench_loop 0.157816 0.160366 0.159504 0.160275 0.162165 .bench_general_term 0.001215 0.001200 0.001255 0.001204 0.001184 . Finished in 3.074021s, 0.9759 runs/s, 0.9759 assertions/s. 3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
結果を見たところ、再帰処理アルゴリズムが一番遅く、一般項アルゴリズムが一番早く実行されるようです。
ベンチマーク を実施してアルゴリズムの性能を比較できたのでコミットします。
1 2 $ git add . $ git commit -m 'perf: ベンチマークの実施'
モジュール分割 アプリケーションのリリース 動作するきれいなコード をリリースするにあたってクラスモジュールごとにファイル分割して エントリーポイント からアプリケーションを実行できるようにしたいと思います。
/
|--lib/
|
-- fibonacci.rb
|--test/
|
-- fibonacci_test.rb
-- fibonacci_benchmark.rb
まず、 lib
に fibonacci
フォルダを追加します。クラスモジュールは Fibonacci
の 名前空間 で管理するようにします。
lib/fibonacci/command.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module Fibonacci class Command def initialize (algorithm) @algorithm = algorithm end def exec (number) @algorithm.exec(number) end end end
lib/fibonacci/recursive.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 module Fibonacci class Recursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end end
lib/fibonacci/loop.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module Fibonacci class Loop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end end
lib/fibonacci/general_term.rb
1 2 3 4 5 6 7 8 9 10 11 12 module Fibonacci class GeneralTerm def exec (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end end
fibonacci.rb
は分割したクラスモジュールを読み込むエントリーポイントに変更します。
lib/fibonacci.rb
1 2 3 4 5 6 require './lib/fibonacci/command' require './lib/fibonacci/recursive' require './lib/fibonacci/loop' require './lib/fibonacci/general_term'
名前空間 を変更して呼び出すクラスが変わったのでテストとベンチマークを修正します。
1 2 3 4 5 6 7 8 9 10 ... require './lib/fibonacci' class FibonacciTest < Minitest::Test def setup @recursive = Fibonacci.new(FibonacciRecursive.new) @loop = Fibonacci.new(FibonacciLoop.new) @general_term = Fibonacci.new(FibonacciGeneralTerm.new) end ...
まず、テストを修正してテストが通ることを確認します。
1 2 3 4 5 6 7 8 9 10 ... require './lib/fibonacci' class FibonacciTest < Minitest::Test def setup @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new) @loop = Fibonacci::Command.new(Fibonacci::Loop.new) @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) end ...
1 2 3 4 5 6 7 8 9 10 ... require './lib/fibonacci' class FibonacciTestBenchmark < Minitest::Benchmark def setup @recursive = Fibonacci.new(FibonacciRecursive.new) @loop = Fibonacci.new(FibonacciLoop.new) @general_term = Fibonacci.new(FibonacciGeneralTerm.new) end ...
続いてベンチマークを修正して実行できることを確認します。
1 2 3 4 5 6 7 8 9 10 ... require './lib/fibonacci' class FibonacciTestBenchmark < Minitest::Benchmark def setup @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new) @loop = Fibonacci::Command.new(Fibonacci::Loop.new) @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) end ...
仕上げはコマンドラインから実行できるようにします。 ルート直下に main.rb
を追加して以下のコードを追加します。 ここでは ベンチマーク で一番良い結果の出た一般項のアルゴリズムを使うことにします。
main.rb
1 2 3 4 5 require './lib/fibonacci' number = ARGV[0 ].to_i command = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) puts command.exec(number)
コマンドラインから引数に番号を指定して実行します。
1 2 3 4 5 6 7 8 9 10 $ ruby main.rb 0 0 $ ruby main.rb 1 1 $ ruby main.rb 2 1 $ ruby main.rb 3 2 $ ruby main.rb 4 3
アプリケーションの完成したのでコミットします。
1 2 $ git add . $ git commit -m 'feat: モジュール分割'
アプリケーションの構成
/main.rb
|--lib/
|
-- fibonacci.rb
fibonacci/
|
-- command.rb
-- general_term.rb
-- loop.rb
-- recursive.rb
|--test/
|
-- fibonacci_test.rb
-- fibonacci_benchmark.rb
/main.rb.
1 2 3 4 5 require './lib/fibonacci' number = ARGV[0 ].to_i command = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) puts command.exec(number)
/lib/fibonacci.rb.
1 2 3 4 5 6 require './lib/fibonacci/command' require './lib/fibonacci/recursive' require './lib/fibonacci/loop' require './lib/fibonacci/general_term'
/lib/fibonacci/command.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module Fibonacci class Command def initialize (algorithm) @algorithm = algorithm end def exec (number) @algorithm.exec(number) end end end
/lib/fibonacci/recursive.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 module Fibonacci class Recursive def exec (number, memo = {}) return 0 if number.zero? return 1 if number == 1 memo[number] || = exec(number - 1 , memo) + exec(number - 2 , memo) end end end
/lib/fibonacci/loop.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module Fibonacci class Loop def exec (number) a = 0 b = 1 c = 0 (0 ...number).each do |_i| a = b b = c c = a + b end c end end end
/lib/fibonacci/general_term.rb.
1 2 3 4 5 6 7 8 9 10 11 12 module Fibonacci class GeneralTerm def exec (number) a = ((1 + Math.sqrt(5 )) / 2 )**number b = ((1 - Math.sqrt(5 )) / 2 )**number ((a - b) / Math.sqrt(5 )).round end end end
/test/fibonacci_test.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fibonacci' class FibonacciTest < Minitest::Test def setup @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new) @loop = Fibonacci::Command.new(Fibonacci::Loop.new) @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) end def test_fibonacci cases = [[0 , 0 ], [1 , 1 ], [2 , 1 ], [3 , 2 ], [4 , 3 ], [5 , 5 ]] cases.each do |i| assert_equal i[1 ], @recursive.exec(i[0 ]) end end def test_large_number_recursive assert_equal 102_334_155 , @recursive.exec(40 ) end def test_large_number_loop assert_equal 102_334_155 , @loop.exec(40 ) end def test_large_number_general_term assert_equal 102_334_155 , @general_term.exec(40 ) end end
/test/fibonacci_benchmark.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 require 'minitest' require 'minitest/autorun' require 'minitest/benchmark' require './lib/fibonacci' class FibonacciTestBenchmark < Minitest::Benchmark def setup @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new) @loop = Fibonacci::Command.new(Fibonacci::Loop.new) @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new) end def bench_recursive assert_performance_constant do |_n| 1000 .times do |i| @recursive.exec(i) end end end def bench_loop assert_performance_constant do |_n| 1000 .times.each do |i| @loop.exec(i) end end end def bench_general_term assert_performance_constant do |_n| 1000 .times.each do |i| @general_term.exec(i) end end end end
参考図書
テスト駆動開発 Kent Beck (著), 和田 卓人 (翻訳): オーム社; 新訳版 (2017/10/14)
新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES) Martin Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 新装版 (2014/7/26)
リファクタリング(第 2 版): 既存のコードを安全に改善する (OBJECT TECHNOLOGY SERIES) Martin Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 第 2 版 (2019/12/1)