テスト駆動開発から始めるRuby入門 ~アルゴリズムのパフォーマンスチューニングとベンチマークを実践する~

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を返す 振る舞いをするプログラムを作ることにしましょう。

0 …​
0 …​

同様のパターンで他のタスクも切り出してみましょう。

0 1 …​
0 1 …​
0 1 2 …​
0 1 1 …​

とりあえず、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
# frozen_string_literal: true

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 worldhello 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#Fibonacci (0.00s)
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#FibonacciTest (0.00s)
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#FibonacciTest (0.00s)
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 1 2 3 …​
0 1 1 2 …​
  • 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#FibonacciTest (0.00s)
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#Fibonacci (0.01s)
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#Fibonacci (0.01s)
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#Fibonacci (0.00s)
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#Fibonacci (0.00s)
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 メソッドを作成して インスタンス変数 @fibFibonacci クラスの参照を代入します。

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#pop
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#pop
...
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 パターン

diag-3f9e53f62bd2f1b3bfe0f476521170ca.png

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
# frozen_string_literal: true

# Fibonacci Calcultor
class Fibonacci
def initialize(algorithm)
@algorithm = algorithm
end

def exec(number)
@algorithm.exec(number)
end
end

# Fibonacci Recursive algorithm
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

# Fibonacci Loop algorithm
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

# Fibonacci General Term algorithm
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
# frozen_string_literal: true

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
# frozen_string_literal: true

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

# Running:

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

まず、 libfibonacci フォルダを追加します。クラスモジュールは Fibonacci名前空間 で管理するようにします。

lib/fibonacci/command.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# frozen_string_literal: true

module Fibonacci
# Fibonacci Calcultor
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci Recursive algorithm
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci Loop algorithm
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci General Term algorithm
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
# frozen_string_literal: true

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: モジュール分割'

アプリケーションの構成

diag-7e042e6c62bb11af0c26498efef6d39b.png

/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
# frozen_string_literal: true

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
# frozen_string_literal: true

module Fibonacci
# Fibonacci Calcultor
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci Recursive algorithm
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci Loop algorithm
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
# frozen_string_literal: true

module Fibonacci
# Fibonacci General Term algorithm
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
# frozen_string_literal: true

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
# frozen_string_literal: true

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)

Author: k2works
Link: https://k2works.github.io/2020/04/18/1587440576/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.