事前設計とTDD

実はモデリングが大好きです。元々はオブジェクト指向プログラミングを勉強しているところからUMLに(自然に)興味が向き、そこからオブジェクト指向設計とかオブジェクト指向分析とかそういう脇道にそれ(脇道とか言ったら怒られる!)、仕様も設計もこれからはオブジェクト指向だ!というありがちな若気の至りもありました。デザインパターンにも転んだし、責務!ロール!コラボレーション!ってのもやったし、重厚長大フレームワークとかもなかなか楽しいですよねえ。ねえ?

今でも概念モデルとか大好物で、上の話を聞きながらうっかりとこんなオブジェクトモデリングをしてしまったりします。

そんなわけで、プログラミングする対象の仕様を理解しながら頭の中でモデリングして設計を進めてしまうのはやむを得ません。多かれ少なかれ、なにかしらの設計が浮かんできてしまいます。

でもTDDはテストを書きながら設計をします。先行して設計してしまうのは、TDDの信義にもとる行為です。裏切り者としてTDD原理主義者に糾弾されてしまうかもしれません。大変だ、どうしよう。

TDDの大家であるBob MartinとJim CoplienがTDDをテーマに対談している(そしてCopeがTDD原理主義をこき下ろしている)インタビュー記事があります。Copeの主張の一部を拾って、乱暴に脳内理解すると、こんな感じになります(だいぶ改変しています)。

事前の設計検討なしでTDDを愚直に進めれば自然とアーキテクチャが立ち顕れるだと! ふざけちゃいけない。アーキテクチャなんか考えなくても最初から作れてしまうようなシステムなら、どうやったって動かせるだろう。でも世の中の本気のシステムはそんなもんじゃあないんだ。

たとえば銀行口座を管理するシステムの例で考えよう(「銀行口座を管理する」という発想自体がそもそもお笑いぐさなんだけど)。TDD原理主義者は「よし、まずは銀行口座クラスを作って、預け入れと引き出しを実装しよう。インスタンスに残高を持たせれば、ほら、簡単にできたでしょ?」という子供だましをしようとする。こんなもの、現実のアーキテクチャとはまったく関係がないんだ。

実際の銀行システムで引き出しという行為には、本人確認があり口座が凍結されていないという前提があり残高のチェック、与信枠のチェック、引き出し限度額のチェック、銀行内部の資金の移動や、その他の様々な一連の手続きがある。口座クラスに残高ぶんのお金があって、それを出し入れするというものではない。クライアントが描くイメージはそういうものではないんだ。スタート地点がそもそも間違っているのに、有効なアーキテクチャにTDDでたどり着けると本気で思ってるのか?

TDDは役に立つ。だけど先行するアーキテクチャが、少なくともアーキテクチャ像が必要だ。そうしたアーキテクチャはTDDで積み上げた結果出来上がるものではなく、ユースケースやクライアントとの対話から創出されるものだ。

事前設計の大切さを強調し、TDDを全否定してますね(もっともインタビュー記事を読めば分かりますが、基本的にはTDDを肯定しています。TDDの限界を指摘していると言ったほうが近いでしょう)。Copeの語り口調はいつも過剰にエネルギッシュなので、本当に言いたいことを見抜くには多少割り引いて冷静に聞く必要があります(彼はDCIを推しているので、そのぶんも割り引きましょう)。

原理主義的に言えば、シンプルな(シンプルすぎる)口座クラスを出発点として、銀行の壮大な業務をカバーするアーキテクチャに向かうことはできるはずです。世界で最初の銀行システムを作った人はたぶん分析設計過程でいろいろな発見をしているはずです。そうした発見をTDDで丁寧にトレースしていけば、銀行業務そのものの理解を進めながらアーキテクチャを成長させていくという、すごく勉強になるけどものすごく時間がかかる旅を体験できると思います。死ぬまでに一度やってみたい気がします。

だけど実際の仕事でこんなことをすべきか。時間効率が悪すぎる点だけで十分にダメですが、すでに確立された、先人の知恵としてのアーキテクチャを無視して車輪を再発明しようとしている時点で、プロジェクトにお金を出している人に対する裏切りと言えるでしょう。うまくいくことが分かっているなら、うまくいくやり方を採用すべきである。その部分を試行錯誤で進めようとするのはプログラマの自分勝手です。

ここからはかなり勝手な解釈をしていきます。単純に言うと、うまくいくやり方がわかっているなら使うべき。どうしたらいいかまったくわからないなら、暗中模索するのにTDDはよい手段である。やるべきことがすべてわかっているなら、そもそもアジャイルはあまり有効な方法論ではないとはよく言われます。

じゃあ、うまくいきそうなやり方がだいたいわかってるんだけど、絶対の確信がない場合は? ほとんどの場合は、これが現実でしょう。

モデリングが好きであろうとなかろうと、プログラマであれば問題に対する設計はだいたい見当がつきます。ほとんどの問題は以前に経験したものと似ているのがその理由です。まったく同じ問題なら同じ方法で解決できますが、ソフトウェア開発の世界ではそういうことはめったに起きません。よく似ているけどちょっとだけ違う、表面的には似てるけどよく見ると差異がある、問題は同じだけど制約条件が異なる。そこがベテランプログラマの腕の見せ所で、既知の解法を使いこなしつつ、いかに差分を吸収する仕組みをひねりだすか。過去の経験がものを言います。

そうして考え出した(脳内では完璧に機能する)解法が、「絶対うまくいく」なら苦労はありません。やってみたら思わぬ落とし穴があったり、大事な要素を看過していたり、もうちょっといい方法が存在することもあります。だからTDDを使うのです。これから書くプログラムについてなんらかの見通しを立て、その見通しが正しいか、うまくいくかTDDで確認しながら進めるのです。

問題によって人によって、一息に書いてしまっていい箇所もあれば、しっかり確認しながら進めたい箇所もあります。ある程度先までの方針がないとそうした見通しは立ちません。どれだけ先まで方針があるか、これも場合によって変わります。毎度お馴染みログイン機能のようなものなら、ほとんど不安なく書けるでしょう。ログイン機能でも、LDAP対応の経験がないとか、マルチログインを初めて実装するとか、未検証のプラットフォームに対応するなど、不安が多い要素が出てくれば、TDDのサイクルもその部分だけ細かくなります。できあがった姿も、最初の想定とは違ったものになるでしょう。

いっぽう本当にどうアプローチしたらいいかわからない、新しい問題もあります。システム全体がどうなるかわからないことだってあります。そんなときは、あらゆる手段を使って設計と検証をしなくてはなりません。TDDも有用なツールになります。リファクタリングで設計を直しながら(どうしようもなくなったら諦めて再出発もして)有効な解法にたどり着ければ万々歳です。それに、これからは似たような問題に出会っても、うまく行く方法を知っているんだからもう大丈夫! (それでもちょっとだけ違っているんだけどね。)

最初の話に戻ります。全体像をモデリングで把握できたら、その設計が正しいかどうかをTDDで確かめながら実装を進めます。たいていは細かいところで齟齬が起きたり、さらに細密な設計が必要になったりしますが、全体像が大きく変わってしまうことはまれです。まれなので、たまには想定を根本からひっくり返されて、墓穴を掘ってそっと埋まりたくなることもあります。チームで仕事しているときはさらに辛いですね。とはいえ、よりよい設計を発見できたという「いいニュース」には喜ぶべきです。次回はもうちょっと上手にモデリングできるかもしれないしね。TDDやっててよかった!

まとめ: 事前設計が悪いわけではない。事前設計を検証して、問題発見とよりよい設計のためにTDDを使おう。