Contents

開発者的な書き方 第3話: リファクタリング1(意図の明確化)

リファクタリングとは、プログラムの動作は維持したまま内部構造を書き換え、保守性を高めることだ。

/images/refactoring.jpg

前回学んだテストファーストでの実装では、プログラムの保守性はいったん度外視し、 まず最短距離で課題を解決した。 課題解決ばかりに集中してしまうと、プログラムの保守性はどんどん悪化していくが、 リファクタリングをこまめに挟むことで、設計面での負債を解消しながら開発をすすめていくことができる。 前回からつづく一連の流れは、ノンプログラマにとって、よりよい仕事をするためのヒントとなるはずだ。

本記事で扱うトピック

  • 意図の明確化
  • 監視下での作業
  • 再命名

前回の流れ

  1. テストファースト実装で新しいルールを追加
  2. テストは通ったが、コードが汚くなってしまった
  3. 保守性を高める必要がある

原時点のディレクトリ構造

1
2
3
4
fizzbuzz
    ├── fizzbuzz.py
    ├── fizzbuzz_worldcup.py
    └── test_fizzbuzz.py

本記事の流れ

  • テストをどう書きたいか明確にする
  • テスト駆動でユーテリティを追加する
  • ユーティリティを使ってテストを簡潔にする

本記事で完成するコード

Before😅

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

@pytest.mark.parametrize('multiple_of_3', [i for i in range(1, 50) if i % 3 == 0 if i % 5 != 0])
def test_return_fizz(multiple_of_3):
    assert generate_fizzbuzz_msg(multiple_of_3) == 'Fizz'


@pytest.mark.parametrize('multiple_of_5', [i for i in range(1, 50) if i % 5 == 0 and i % 3 != 0])
def test_return_buzz(multiple_of_5):
    assert generate_fizzbuzz_msg(multiple_of_5) == 'Buzz'


@pytest.mark.parametrize('multiple_of_15', [i for i in range(1, 100) if i % 3 == 0 and i % 5 == 0])
def test_return_fizzbuzz(multiple_of_15):
    assert generate_fizzbuzz_msg(multiple_of_15) == 'FizzBuzz'

After😍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
n_tests = 100
baisu3 = baisu(multi=3, n=n_tests)
baisu5 = baisu(multi=5, n=n_tests)
baisu15 = baisu(multi=15, n=n_tests)


@pytest.mark.parametrize('x', baisu3)
def test_return_fizz(x):
    assert generate_fizzbuzz_msg(x) == 'Fizz'

@pytest.mark.parametrize('x', baisu5)
def test_return_buzz(x):
    assert generate_fizzbuzz_msg(x) == 'Buzz'

@pytest.mark.parametrize('x', baisu15)
def test_return_fizzbuzz(x):
    assert generate_fizzbuzz_msg(x) == 'FizzBuzz'

前提条件

  • Python 3 がインストールされている
  • pytest-watch がインストールされている

どう書きたいかを整理する

前回学んだテストファーストでの開発は、プログラムの要件を明確にしたうえで実装に着手できるメリットがある。 いわば、機能を無駄なく実装するための手法だ。 一方でこの手法は、書き上がったプログラムの読みやすさまでは保証してくれない。

現に、現時点のテストでは、デコレータ部分に長ったらしいリスト内包があり、意図が不明確だ。 意図がわかったとしても、リスト内包が正しく書かれているのかどうかは、よく読んでみないとわからない。

1
2
3
@pytest.mark.parametrize('multiple_of_3', [i for i in range(1, 50) if i % 3 == 0 if i % 5 != 0])
def test_return_fizz(multiple_of_3):
    assert generate_fizzbuzz_msg(multiple_of_3) == 'Fizz'

できることならば、この部分をもっと、「3 の倍数」というふうに宣言的に書きたい。

疑似コードで表すとこんな感じだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def test_return_fizz():
    assert generate_fizzbuzz_msg(3の倍数) == 'Fizz'


def test_return_fizz():
    assert generate_fizzbuz_msg(5の倍数) == 'Buzz'


def test_return_fizz():
    assert generate_fizzbuzz_msg(15の倍数) == 'FizzBuzz'

これを実現するためには、3 の倍数や 5 の倍数を生成するための関数が必要になる。 言い換えれば、この競技における「n の倍数」の定義そのものだ。 作っていこう。

インナーツールを定義する

前回に引き続き、開発はテスト駆動ですすめていく。 ここから先は、「3 の倍数」などの各数字の定義を numdefs.py (number definitions) というファイルに書いていこうと思う。 まずは接頭辞 ‘test’ をつけたテスト用ファイル test_numdefs.py を用意しよう。

3 の倍数

💥 失敗するテスト

test_fizzbuzz.py の中でスマートに 3 の倍数を呼び出すために、3 の倍数を生成する関数が欲しい。 これから作ろうとする関数がどう動いてほしいかを、 test_numdefs.py の中に表現してみよう。

1
2
3
4
5
6
7
from numdefs import multiple_of_3


def test_multiple_of_3():
    assert multiple_of_3(length=1) == [3]
    assert multiple_of_3(length=2) == [3, 6]
    assert multiple_of_3(length=3) == [3, 6, 9]

assert 文の左辺には、これから実装しようとする関数に期待する使い勝手を書いてみた: 3の倍数(いくつ欲しいか) という形だ。 右辺には、期待する返り値が書いてある。 length を 2 にしたなら、3 の倍数が二つ返ってくるイメージだ。

では、我々の課題を明確にするために、テストを実行しよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
~/Documents/GitHub/website/pnp_codes/fizzbuzz $ pytest
================================================ test session starts ================================================
platform darwin -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yourname/fizzbuzz
collected 21 items

test_fizzbuzz.py ....................                                                                         [ 95%]
test_numdefs.py F                                                                                             [100%]

===================================================== FAILURES ======================================================
______________________________________________ test_multiple_of_3 _______________________________________________

    def test_multiple_of_3():
>       assert multiple_of_3(length=1) == [3]
E       NameError: name 'multiple_of_3' is not defined

test_numdefs.py:4: NameError
============================================== short test summary info ==============================================
FAILED test_numdefs.py::test_multiple_of_3 - NameError: name 'multiple_of_3' is not defined
=========================================== 1 failed, 20 passed in 0.05s ============================================
~/Documents/GitHub/website/pnp_codes/fizzbuzz $

予想通り、 multiple_of_3() が未定義というエラーが返った。 実装によって、この課題を解決していこう。

  • 👀 監視下での作業

    ところで、今回のテーマはリファクタリングなので、これから何度もテストを繰り返すことになる。 毎回テストを手動で実行するのは賢明とはいえないだろう。 pytest の継続化ツール pytest-watch を使えば、ディレクトリのコードとテストが更新されるたびに、 pytest をトリガーすることができる。 テスト実行は pytest-watch に任せ、ファイルの編集に集中しよう。

    pytest-watch でディレクトリの監視をスタートさせたら、 3 の倍数を生成する関数を numdefs.py に実装していこう。

✨ 実装

length で返り値の長さをコントロールしながら 3 の倍数を生成する関数は、次のように書ける:

1
2
3
4
5
6
7
8
9
def multiple_of_3(length):
    multi = 3
    out = [multi]
    i = 2
    while len(out) < length:
        candidate = i * 3
        out.append(candidate)
        i += 1
    return out

これを numdefs.py に保存した瞬間 pytest がトリガーされ、 先ほど失敗していたテストが通った。 継続的テストも関数の実装も、うまくいっているということだ。

さて、今は何をしたいんだったっけ?──そうだ、 test_fizbuzz.py をもっとスマートに書きたいんだった。

1
2
3
@pytest.mark.parametrize('multiple_of_3', [i for i in range(1, 50) if i % 3 == 0 and i % 5 != 0 and i != 12])
def test_return_fizz(multiple_of_3):
    assert generate_fizzbuzz_msg(multiple_of_3) == 'Fizz'

この長ったらしいリスト内包を見ると、 原時点で、「3 の倍数」として扱ってはいけない数があることを思い出す:

  • 5 の倍数でもあるとき
  • 連番になるとき

これらの要件をテストとして表現しよう。

💥 テスト追加

3 の倍数に含めたくない数──連番である ‘12’および 5 の倍数である ‘15’ と ‘45'──についてのテストは、 例外に着目したテスト test_multiple_of_3_exceptions() として、下のように書いてみた。

1
2
3
4
5

def test_multiple_of_3_exceptions():
    assert multiple_of_3(length=4)[-1] != 12
    assert multiple_of_3(length=4)[-1] != 15
    assert multiple_of_3(length=8)[-1] != 30

ここでは各リストの末尾の数値に着目したいので、インデックス -1 を使って値を取り出している😅

保存しよう──現行の multiple_of_3() は、愚直に 3 の倍数を返すので、テストが失敗するようになった。 テストが我々の期待を反映するようになったわけだ。 次はこれを解決していこう。

🆙 修正

multiple_of_3() が期待通りに動作するように修正しよう。 コードはこのようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def multiple_of_3(length):
    multi = 3
    out = [multi]
    i = 2
    while len(out) < length:
        candidate = i * multi
        if candidate % 5 != 0 and candidate != 12:
            out.append(candidate)
        i += 1
    return out

これを保存すると、失敗していたテストが通るようになった。

:white_check_mark: テスト追加

この調子で、 length=10 までテストを書いてみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def test_multiple_of_3():
    assert multiple_of_3(length=1) == [3]
    assert multiple_of_3(length=2) == [3, 6]
    assert multiple_of_3(length=3) == [3, 6, 9]
    assert multiple_of_3(length=4) == [3, 6, 9, 18]
    assert multiple_of_3(length=5) == [3, 6, 9, 18, 21]
    assert multiple_of_3(length=6) == [3, 6, 9, 18, 21, 24]
    assert multiple_of_3(length=7) == [3, 6, 9, 18, 21, 24, 27]
    assert multiple_of_3(length=8) == [3, 6, 9, 18, 21, 24, 27, 33]
    assert multiple_of_3(length=9) == [3, 6, 9, 18, 21, 24, 27, 33, 36]
    assert multiple_of_3(length=10) == [3, 6, 9, 18, 21, 24, 27, 33, 36, 39]

OK、全ての assert 文が問題なく評価された。 ついでに、左辺や右辺を変えてみて、テストが失敗することも確認しておこう。 こうすることで、 multiple_of_3() を想定通りに実装できているという確信をさらに高めることができる。

さて、書き上がったテストを見返してみると、関数名 multiple_of_3 はちょっと長い気もしてきた😅 名前を baisu_3 にしてしまおう。 ついでに引数 lengthn にする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def baisu_3(n):
    multi = 3
    out = [multi]
    i = 2
    while len(out) < n:
        candidate = i * multi
        if candidate % 5 != 0 and candidate != 12:
            out.append(candidate)
        i += 1
    return out

pytest-watch の監視下なら、関数の再命名もこわくない。

♻️ パラメタ形式で書き直す

さてここまで書いてきたテストを見返してみると、重複している部分が目に付く。 重複は、プログラムの見た目を損ねるだけでなく、保守すべき箇所をいたずらに増やしてしまう。 また、注目すべき箇所が不明確になるのも問題だ。 プログラムの中からパラメタ化できる部分を探して、なるべく簡潔にしてみよう。

それぞれの assert 文を見比べると、変化しているのは引数 length と、リスト末尾の値だけだ。 これらを testdata_for_3 というリストにまとめよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from numdefs import baisu_3, baisu_5
import pytest


testdata_for_3 = [
#   (length of baisu sequence, the last baisu)
    (1, 3),
    (2, 6),
    (3, 9),
    (4, 18),
    (5, 21),
    (6, 24),
    (7, 27),
    (8, 33),
    (9, 36),
    (10, 39)
]


@pytest.mark.parametrize("n, last", testdata_for_3)
def test_baisu_3_last(n, last):
    assert baisu_3(n=n)[-1] == last

同じように、例外のテストも書き直せる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
testdata_for_3_exceptions = [
#   (n, last)
    (4, 12),
    (4, 15),
    (8, 30)
]


@pytest.mark.parametrize("n, last", testdata_for_3)
def test_baisu_3_last_exceptions(n, last):
    assert not baisu_3(n=n)[-1] == last

テストケース数を保ったまま、コードの見通しをよくすることができた。 3 の倍数を生成する baisu_3() のテストは、ここで一段落としよう。

5 の倍数

💩 とりあえずコピペ

3 の倍数と同じ要領で、5 の倍数のテストも追加していこう。 ファイル先頭でのインポートも忘れずに。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
testdata_for_5 = [
#   (n, last)
    (1, 5),
    (2, 10),
    (3, 20),
    (4, 25),
    (5, 35),
    (6, 40)
]
@pytest.mark.parametrize("n, last", testdata_for_5)
def test_baisu_5_last(n, last):
    assert baisu_5(n=n)[-1] == last

存在しない関数をインポートしているこのテストを保存すると、即座に pytestImportError で失敗するようになった。 これは、テストが機能していることを知らせてくれるいいニュースだ。 関数を実装していこう。 3の倍数との違いは、 multi の値と、例外部分の条件式だ。 なんとなくここも関数にできそうだが、まずは愚直に実装して、動作を固めてしまおう😅

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def baisu_5(n):
    multi = 5
    out = [multi]
    i = 2
    while len(out) < n:
        candidate = i * multi
        if candidate % 5 != 0 and candidate != 12:
            out.append(candidate)
        i += 1
    return out

これでテストが通るようになった。

ここでも例外のテストも追加しよう。 例えば 3 の倍数でもある 15 と 30 は、この競技では 5 の倍数としてカウントしない。

1
2
3
4
5
6
7
8
testdata_for_5_exceptions = [
#   (n, last)
    (3, 15),
    (5, 30)
]
@pytest.mark.parametrize("n, last", testdata_for_5)
def test_baisu_5_last_exceptions(n, last):
    assert not baisu_5(n=n)[-1] == last

♻️ 関数を抽象化する

さて、 baisu_3()baisu_5() の共通点と相違点に着目することで、これらをまとめることはできないだろうか。 テストで表現すると、下のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from numdefs import baisu, baisu_3, baisu_5
import pytest


@pytest.mark.parametrize("multi, n, last",
                         [(3, 1, 3),
                          (3, 2, 6),
                          (5, 1, 5),
                          (5, 2, 10)])


def test_baisu(multi, n, last):
    assert baisu(multi=multi, n=n)[-1] == last

こうなるように実装していこう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def baisu(multi, n):
    out = [multi]
    i = 2
    while len(out) < n:
        candidate = i * multi
        if multi == 3:
            if candidate % 5 != 0 and candidate != 12:
                out.append(candidate)
        elif multi == 5:
            if candidate % 3 != 0 and candidate != 12:
                out.append(candidate)
        i += 1
    return out

テストが通った。 ここまでこれば、テストを成功と失敗だけに着目してまとめることができる。

testdata を、正しい結果と誤った結果に分けて整理しよう。

 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
testdata = {
#       (multi, length of baisu sequence, the last baisu)
    'correct':[
        (3, 1, 3),
        (3, 2, 6),
        (3, 2, 6),
        (3, 3, 9),
        (3, 4, 18),
        (3, 5, 21),
        (3, 6, 24),
        (3, 7, 27),
        (3, 8, 33),
        (3, 9, 36),
        (3, 10, 39),
        (5, 1, 5),
        (5, 2, 10),
        (5, 3, 20),
        (5, 4, 25),
        (5, 5, 35),
        (5, 6, 40)
    ],
    'wrong':[
        (3, 4, 12),
        (3, 4, 15),
        (3, 8, 30),
        (5, 3, 15),
        (5, 5, 30)
    ]
}
1
2
3
4
5
6
7
8
@pytest.mark.parametrize("multi, n, last", testdata['correct'])
def test_baisu_last(multi, n, last):
    assert baisu(multi=multi, n=n)[-1] == last


@pytest.mark.parametrize("multi, n, last", testdata['wrong'])
def test_baisu_last_exceptions(multi, n, last):
    assert not baisu(multi=multi, n=n)[-1] == last

15 の倍数

🐛 テスト追加──バグ修正

15 の倍数には例外がないのでかんたんだ。 例のごとく、まずは失敗するテストを追加しよう。 「15 の倍数のうち、最初の数は 15」を表す (15, 1, 15) をテストデータに追加する。 …あれ?失敗しない。

そうか、 while 文のところで無限ループに陥っているんだ。 未定義の倍数値が与えられたときには処理が停止するようにしよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def baisu(multi, n):
    if multi not in [3, 5]:
        raise Exception("Behavior for 'multi={multi}' is not defined."
                        .format(multi=multi))
    out = [multi]
    i = 2
    while len(out) < n:
        candidate = i * multi
        if multi == 3:
            if candidate % 5 != 0 and candidate != 12:
                out.append(candidate)
        elif multi == 5:
            if candidate % 3 != 0 and candidate != 12:
                out.append(candidate)
        i += 1
    return out

一見かんたんに思える仕事でも、油断によるミスが起こることがある。 テストファーストでの実装は、自らを守るセーフティネットとなってくれる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
============================================================================================================ test session starts ============================================================================================================
platform darwin -- Python 3.8.6, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yourname/fizzbuzz
collected 47 items

test_fizzbuzz.py ....................                                                                                                                                                                                                 [ 42%]
test_numdefs.py .....................F.....                                                                                                                                                                                           [100%]

================================================================================================================= FAILURES ==================================================================================================================
_________________________________________________________________________________________________________ test_baisu_last[15-1-15] __________________________________________________________________________________________________________
multi = 15, n = 1

    def baisu(multi, n):
        if multi not in [3, 5]:
>           raise Exception("Behavior for 'multi={multi}' is not defined."
                            .format(multi=multi))
E           Exception: Behavior for 'multi=15' is not defined.

numdefs.py:16: Exception

よし、未定義の倍数値に対して、自作のメッセージが返ることを確認できた。 例によって、次は関数の実装だ。

✨ 実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def baisu(multi, n):
    if multi not in [3, 5, 15]:
        raise Exception("Behavior for 'multi={multi}' is not defined."
                        .format(multi=multi))
    out = [multi]
    i = 2
    while len(out) < n:
        candidate = i * multi
        if multi == 3:
            if candidate % 5 != 0 and candidate != 12:
                out.append(candidate)
        elif multi == 5:
            if candidate % 3 != 0 and candidate != 12:
                out.append(candidate)
        elif multi == 15:
            out.apend(candidate)
        i += 1
    return out

…よし。テストを増やしたら、15 の倍数も完成だ。

ここまでで、3・5・15 の各倍数を、 baisu() から生成できるようになった。 当初の目的を達成しよう──テストの可読性向上だ。

テストのリファクタリング

test_fizzbuzz.py を短くする ここまで実装してきたインナーツール baisu() を使って、ゲームのテストを簡潔にしていく。 100 種類の 3 の倍数に対して、‘Fizz’ コールが返るかをテストするコードはこうだ:

1
2
3
4
5
6
7
n_tests = 100
baisu3 = baisu(multi=3, n=n_tests)


@pytest.mark.parametrize('x', baisu3)
def test_return_fizz(x):
    assert generate_fizzbuzz_msg(x) == 'Fizz'

まずテストの前に、長さ 100 の 3 の倍数のリストを生成し、これを baisu3 と命名した。 テストのデコレータは、 baisu3 の各要素を x と再命名して取り出し、 一つずつ generate_fizzbuzz_msg() に渡している。

以前の実装では、リスト内包がわかりにくかったために、 そこから取り出した各要素に multiple_of_3 という説明的な名前をつけていたが、

1
2
3
@pytest.mark.parametrize('multiple_of_3', [i for i in range(1, 50) if i % 3 == 0 if i % 5 != 0])
def test_return_fizz(multiple_of_3):
    assert generate_fizzbuzz_msg(multiple_of_3) == 'Fizz'

リファクタリングによって、その必要もなくなった──より宣言的な書き方が可能になったからだ、 multiple_of_3x と書けるようになったのは、見た目には小さい変化だが、保守性も高まっている。 もし今後ルールが変わり、3 の倍数ではなく 4 の倍数に対して ‘Fizz’ をコールすることになった場合にも、変更箇所が少なくて済むのだ。

5 の倍数と 15 の倍数のテストもリファクタリングしたものはこうだ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
n_tests = 100
baisu3 = baisu(multi=3, n=n_tests)
baisu5 = baisu(multi=5, n=n_tests)
baisu15 = baisu(multi=15, n=n_tests)


@pytest.mark.parametrize('x', baisu3)
def test_return_fizz(x):
    assert generate_fizzbuzz_msg(x) == 'Fizz'

@pytest.mark.parametrize('x', baisu5)
def test_return_buzz(x):
    assert generate_fizzbuzz_msg(x) == 'Buzz'

@pytest.mark.parametrize('x', baisu15)
def test_return_fizzbuzz(x):
    assert generate_fizzbuzz_msg(x) == 'FizzBuzz'

改めて全体を見返し、わかりにくいところがないか、今一度確認してみよう。

3 つのテストを比較すると、 generate_fizzbuzz_msg() が良い名前ではないことがわかる。 「FizzBuzz ゲームのメッセージを生成する」のか、「‘FizzBuzz’ というメッセージを生成する」のか、 判断できないからだ。 読み手の迷いがなくなるように再命名しよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
n_tests = 100
baisu3 = baisu(multi=3, n=n_tests)
baisu5 = baisu(multi=5, n=n_tests)
baisu15 = baisu(multi=15, n=n_tests)


@pytest.mark.parametrize('x', baisu3)
def test_return_fizz(x):
    assert generate_call(x) == 'Fizz'

@pytest.mark.parametrize('x', baisu5)
def test_return_buzz(x):
    assert generate_call(x) == 'Buzz'

@pytest.mark.parametrize('x', baisu15)
def test_return_fizzbuzz(x):
    assert generate_call(x) == 'FizzBuzz'

これで完成だ。

まとめ

今回のリファクタリングによって、テスト駆動開発のサイクルをやっと一巡することができた。 時間はかかったが、プログラムの内部設計を改善することができた。

しかし注意すべきは、今回の作業によって、プログラムのもっとも外側の機能、 つまり数字に対して適切なコールを返す機能には、なにも変化がないという点だ。 これは言い換えれば、ビジネス側の人間──ここでは我々のプログラムによるサポートを期待する競技の審判団──からすると、 時間がかかった割に、なにも仕事が進んでいないように見えてしまう可能性があるということだ。 あくまでも、プログラムの開発はビジネス課題の解決のためであることを忘れてはならない。

難しいのは、課題解決と設計改善のバランスだ。 課題解決ばかりを優先してリファクタリングをおろそかにすると、 設計面での負債が蓄積し、肝心な局面で新機能の実装が難しくなってしまうこともある。 課題解決とリファクタリングのバランスは、 短期的にも長期的にも、最も生産性が高くなるような配分ですすめるのがよいだろう。

テスト駆動開発で開発をすすめることで、課題解決の合間にも、リファクタリングをこまめに挟むことができる。 課題解決のための動作はテストによって保証されているので、大胆な設計も安全にすすめられるはずだ。

日々のこまめな改善を可能にする開発者的な仕事のしかたは、ノンプログラマにとってもよい仕事をするためのヒントとなるのではないだろうか。

次回予告: インナーツールのリファクタリング

今回の記事では、ひとまず 3・5・15 の倍数について、テスト駆動開発のサイクルを一巡させることができたが、 現行のテストには、まだ「その他の数」についてのわかりにくいリスト内包が残っている。

1
2
3
@pytest.mark.parametrize('otherwise', [i for i in range(1, 100) if i % 3 != 0 and i % 5 != 0])
def test_return_asis(otherwise):
    assert generate_fizzbuzz_msg(otherwise) == str(otherwise)

「その他の数」を生成する関数が必要だが、 それには今回定義したインナーツールのリファクタリングをもっと洗練させる必要がある。 次回は、蓄積した負債もあわせて解消しつつ、リファクタリングをすすめよう。 ここまで書いてきたテストが、引き続き大きな味方になってくれるはずだ。