僕はテスト駆動開発(以下TDD)の信者だ。
TDDを学びだした最初は「TDDのメリット」とは「テストコード」が同時に出来上がり回帰テストが可能になる事だと思っていた。
しかしすぐに「TDDで開発を行うとシステムの複雑性が下がりやすい」事こそが最大のメリットであると気がついた。
その事について書く。
TDDで開発しない場合
構造に対して興味が薄い人が作業した場合、以下のような感じでコードが書かれている事が良くある。
# userのtypeを更新する(コード構造イメージを示すだけで、処理自体に特に意味はない) X処理() { # DBからデータ取得する処理 user = load() if user.status == 1 { user.type = 1 } else { user.type = 2 } # DB保存処理 save(user) }
データの取得や永続化と処理がごっちゃに実装されている。 この関数を使っている側からのコードの見た目は以下のようになる。
serviceMethod() { # 事前処理コード等 ..... ..... ..... X処理() # 事後処理コード等 ..... ..... ..... }
初めてこれを見た人は、[X処理]を見て何が起こるのかさっぱりわからない。(関数名が悪いというのもあるが。) [X処理]のような構造(内部でデータアクセスして処理する構造)の関数が中心になってシステムが構成されると非常に見通しの悪いシステムになる。
以下のserviceMethodが実行されたときに、影響があるデータは何か分かるだろうか?
serviceMethod() { X処理() Y処理() } X処理() { # DBからデータ取得する処理 user = load() if user.status == 1 { Z処理() } else { Z'処理() } # DB保存処理 save(user) }
これらの関数は内部でデータアクセスするので、使われている全ての関数の内部を見ないと影響のあるデータが何なのかわからない。 上記の[Z処理]の中でどんなデータにアクセスされているかわからない。 こんな関数が至るところで使われているともう把握しきれない。
こういう構造になってると、コードに修正を入れたときの影響範囲も測りきれない。
TDDで開発した場合
TDDで開発すると、実装よりもまずテストコードを書くので、テストコードでテストしやすい構造にしたくなる。
「内部でDBからデータ取得し更新する構造の関数」をテストコードでテストするのは以下のような理由で非常に面倒くさい。
- 事前にDBに前提データを入れてやらねばならない。
- idがautoincrementであればテスト毎に変化するので考慮しなければならない
- 入れるデータはテーブル的に整合性が取れていなければならない(テストに関係ないデータでもリレーションを考慮してなにかいれてやらないといけない)
- 他テストからのデータ変更の影響を考慮せねばならない
- データベースの設定しなければならない。
- 変更されたデータをdbから拾ってきて検証せねばならない
テストコードでテストしやすい構造にするとこういう形になる。
# データを渡して、結果を受け取る out_data = someFunc(in_data) assert(out_data, expected_data)
そもそもプログラミングとは[データストア => 処理 => データストア]の流れの繰り返しを作っていく事なので、こちらの方が自然に思える。
さきほどの悪い例で実装された「X処理」をTDDで実装すると以下のような感じになる。
# statusを受け取り、対応するtypeを返す X処理(status) { if status == 1 { return 1 } else { return 2 } }
データは外から貰い、結果を返すだけに注力している。 この関数を使っている側からのコードの見た目は以下のようになる。
serviceMethod() { # 事前処理コード等 ..... ..... ..... # DBからデータ取得する処理 user = load() user.type = X処理(user.status) # DB保存処理 save(user) # 事後処理コード等 ..... ..... ..... }
関数のネーミングが最悪なのは一緒なのに、何をやっているのか雰囲気が分かるようになった。
補足:
「単にX処理の中身をぶちまけただけ」に見えるかもしれないが違う。
メソッドによる隠蔽とは「全てを隠す」のではなく「実装の詳細」だけを隠して「何をやっているか?」のエッセンスは残さなければならない。 そうしないと「なにをやってるかわからんので中身を見ないといけない」状態になってしまう。
こういう構造の関数が中心にシステムが構成されると、処理の見通しが良くなる。
以下のように書かれていれば(これもあいかわらず関数名がひどいが)なんとなくやってることがわかるのではないだろうか?
serviceMethod() { user = load() user.type = X処理(user.status) user.age = Y処理(user.birthday) save(user) bill = Z処理(user.type) save(bill) }
どういうデータが関わっているのかもわかりやすい。
まとめ
TDDを行うと、テストしやすい構造にしたくなる。
「テストしやすい構造 = シンプルな構造、依存が少ない構造」であるため、システムの複雑性が下がる。