Floyd Marinescuです。私は今、JAOOカンファレンスでJim Coplien氏とBob Martin氏と居ます。我々には、TDDの価値が何なのかというところで、とても興味深い見解の相違があります。
そこで、それぞれが2~3分づつ発言しながら、議論をしましょう。
私はあなた達の話を聞いていますから。
Jim Coplien: そうですね。昨日のあなたの基調講演では"テスト駆動開発"を使って何をしようとしているかという話でしたから、それはよいことだと思います。
私は、とりわけXPコミュニティがテスト駆動開発と呼んでいるものについてについて強く反対してきました。
過去6ヶ月の4っのカンファレンスで行われた多くのチュートリアルと聴講してみたのですが、彼らがテスト駆動開発と呼ぶものには、きわめて一貫した恣意的なストーリーが用いられていることがわかります。
しかし昨日の話しは、少し違っていました。そこで、この議論を有意義で意味あるものにするために、まずあなたの昨日の話を簡潔に繰り返してくれますか。そうすれば、前提を合わせて話ができます。
3. BobはJimの要望に応え、三つの原則を紹介している。
最初の一つは、テスト駆動開発者は、失敗するユニットテストを書くまでは、プロダクションコードは1行たりとも書かないというこです。つまり失敗するユニットテストが存在していないのであれば、プロダクションコードは書かれていない筈です。
二つ目の原則は、テストを失敗させるために充分なユニットテストがあれば、それ以上ユニットテスト書いてはいけないということです。コンパイルできないという状態は失敗に含みます。
ですから、プロダクションコードを書いてないうちからユニットテストをたくさん書くことはできません。
三つ目の原則は、テストをパスさせることができる充分なプロダクションコードがあれば、それ以上プロダクションコードを書いてはいけないということです。
ユニットテストは書きかけで、それを脇に置いてプロダクションコードを書くということはやってはいけません。
この三つの原則は、およそ30秒ぐらいのサイクルで行います。テストを30秒から1分ぐらいで行うことで、ユニットテストとプロダクションコードを実質的に同時に書くことが重要なのです。
これが私の定義です。
J: いまのところ、私がTDDについて抱いている主な懸念は、あなたの今の発言に関する限り、あまり問題ではないと考えています。ですから、もし、それ以上でもそれ以下でもないのならば、私たちに考えにズレははないと思います。
では、私の懸念は何なのか、というと、自分が多くの顧客と幅広い仕事をした中で体験したり、他のコンサルタントやスクラムマスター達が彼ら自身のプロジェクトで経験した話を聞いているうちに気づいたものなのです。
私たちは今まで2つの重要な問題を確認してきました。1つ目の問題は、TDDでは実際に稼働している環境と同じアーキテクチャやフレームワークが無い状態でテストされているという点です。これはKent氏が最初の頃に強く主張していた「TDDをアーキテクチャを駆動するために使うのだ」というもので、ユニットテストすることで、手続き型ボトムアップアーキテクチャの原因になります。
私たちは今さっき上の階で「TDDはユニットテストと同じなのか?」という議論をしていましたね。
まあ、同じではないと思います。Fortranではユニットテストというのは素晴らしい考え方でした。APIを階層化したときにソフトウェアの構成要素としてのユニットは、ユニットテストと一致させることができたからです。しかし、今やソフトウェアの構成要素としてのユニットはオブジェクトなのに、テストしているのは手続きです。そこにはちょっとした不整合があるのです。
つまり、アーキテクチャを駆動するためにこういった手段を取るならば、2次元データから3次元データを作り出すようなもので、結局失敗すると思います。
アーキテクチャ上の問題で自分自身の首を絞めてしまうことで、3回目ぐらいのスプリントには、もうこれ以上何もできない状態になってしまい、挙げ句、衝突、爆発炎上して失敗する。こういった光景は多くのプロジェクトでよく見られるものです。
そういう場合はリファクタリングをすることでこの状態から抜けることはできなくなっています。クラスのカテゴリ、階層を超えてリファクタリングしないといけない状況に陥っているので、リファクタリングしても同じ機能を維持しているかの保証ができないのです。
もう一つの問題は、Trygve Reenskaug氏と私がGUIの破壊にみられる現象についていろいろと話していた件です。Javaクラスラッパのような手続き的アーキテクチャを例に出すと、そこにはオブジェクト指向でそうしているような、ドメイン知識やユーザのコンセプトモデルに沿った形での構造の決定がもはやできなくなっているのです。
Kent氏でさえ、しばしば「良いGUIを以てしても拙いアーキテクチャは隠しきれない」と言っています。
アーキテクチャは常にインターフェスを透かして見えてくるものだと思います。さらに言うと、ドメインモデルがインターフェースにどう影響するのか、という構図はインフラストラクチャにあるのですから、インフラストラクチャは重要なのです。
ですから、ボブおじさんの三つの原則を当てはめようと思うなら、そうすること自体は問題ないでしょうが、私は、構造的な側面を把握できるような出発点があるとよいと思います。
4. そうですね。しかし、あなたは、テスト駆動開発の習慣は2007年の現在、プロフェッショナルな行動としての純粋な要件である、という命題については受け入れていないようです。
それはどうしても受け入れられませんね。
J:アーキテクチャが進化するというところには同意します。アーキテクチャというのは、書き上げたコードと、おそらく早い段階で提供されているであろうユースケースとの両方を使って形成していくものだと思います。アーキテクチャはスコープであったり、他のそういった類の情報も与えてくれますが、インクリメンタルにアーキテクチャの設計をやろうとしたならば、文字通り全てがインクリメンタルになってしまいます。ドメイン知識が事前に得られていない状態で顧客と作業を進めてしまうと、完全に間違った方向に進んでしまう怖れがあります。
かつてKent氏と話をしていた時のことを話しましょう。彼がTDDや、動作しうる最も単純な方法で実装しようという意味でYAGNIという言葉を提唱したばかりのころ、彼は「よし。銀行の預金口座を実装しよう。」と言いました。
預金口座っていったい何のことでしょうか?
足したり引いたりできるだけの数字のことです。つまり預金口座というのは計算機のことですね。
では、差引残高に足したり、そこから引いたりするのを表示する計算機を作りましょう。
これが動作しうる最も単純な例で、他の全てはその発展形であると言えます。
もし、本物の金融システムにしようとしても、預金口座はオブジェクトでさえないので、そこからリファクタすることで正しいアーキテクチャへ導けるようなものではありません。
預金口座というのは、監査証跡をしつつ、預金、金利の集計、その他のお金の動きなどのデータベーストランザクションを繰り返し処理する一連のプロセスのことです。
ユーザの立場から見た場合、預金口座は銀行のどこかの棚にいくらかのお金が置いてあるのとは訳が違います。ご存じのように、金融システムの基盤では、税理士や保険計理士、そういった民間の人達をサポートするために、他と比べて複雑な構造になっています。だから、インクリメンタルな手法で取りかかれないのです。
いや、不可能とまでは言えないですね。銀行業界が40年かけてここまできたように40年ぐらいかければ。
そこに40年を費やすことができますか?でも、それはアジャイルとは言えませんね。
ですから、前提知識を活用するべきなのです。確実な決断だけでも最初にいつくか行っておくべきでしょう。そうすることで、決めきれなかった事項を後になって検討する時に、その判断を容易にしてくれるはずです。
確かに状況は変わりますし、アーキテクチャは進化します。「アーキテクチャを塩漬けにしなさい」とまで言う人はいないんじゃないでしょうか。
また、前もって適切なコード、つまり実際のメンバ関数として書かれるコードということですが、それができるとは思えません。
見た目を書く、ロールを書く、ドメイン知識の構造を表現できるようなインターフェースを書く...
そうして書いたものの中身のコードを書くのは、お金を払ってくれるクライアントを見つけてからです。そうしないとリーン原則に違反してしまうからです。
「ジャストインタイム」方式でやるならば、最初から構造を押さえておこうとするべきです。そうしないと、リスク管理しているつもりで、自分自身の首を絞めているようなものです。
問題なのは、いわば、言葉というものが定義とは別に意味を持っているということです。
何かをラバと呼んだり、ラバとは何ですか?と問うことをしなくても、ラバはラバという事実は変わりません。
リンカーンが言ったように「ラバをロバと呼んでみても、ロバになるわけではない。」ということです。
つまり、メンバ関数がセマンティクスとして、意味を教えてくれています。
やりすぎたり、当てずっぽうにやってはいけない...この点ではKent氏に同意できます。「XP入門」で彼が言っているように、「当てずっぽうにやってはいけない」これは真実です。
しかし、ここで私の知っていることを確認しておきたいと思います。いくつかご存じのこともあるかとは思いますが、通信システムや金融システムの仕組みについてです。
"recovery"オブジェクトを作ってはいけないということはご存じかと思います。
私はかつて、大きな通信会社のプロジェクトの再構築に携わっていました。そこではオブジェクト指向と最新のコンピュータサイエンスの技術を駆使して、使用料の切換システムを作り直していました。しかし、そのプロジェクトで私に任されたのは"recovery"オブジェクトを作るような輩と一緒に仕事することでした。
とてもばかげた話です。"recovery"はオブジェクトではないのに。けれども彼の薄っぺらいドメイン知識ではそうするしかなかったのでしょう。
そのオブジェクトのメンバ関数の正体を突き止めてみれば、それがオブジェクトですらないことが理解できると思います。
そこで、こう聞きます。「どうして私がオブジェクトになってないと気付いたと思う?どんなメンバ関数があると思う?」
「う...ええと、recover関数です。」
「素晴らしい。そいつは助かるな。」
現に、私はそこに今はSOAと呼ばれている技術を活用している人達がいることに思い当たりました。
これは危険だな、と。
オブジェクトに意味を与えるには、何か実体が必要なのではないでしょうか。
9. では次に、私は実行可能なコードが将来の判断材料となるようにしていますが、それは大がかりなアーキテクチャや、憶測に基づいた大規模システムにしたくないからです。
それは正しい判断です。異論はありません。
10. では、話を元に戻しますが、実行可能なコードを書き始める前に、どれぐらいの時間を使いますか?例えば、最終的に200万行ぐらいになるシステムだとしたらどうでしょうか?
うーん。200万行という規模は、経験上かなり小規模ですね。
私は何億行という規模のシステムを開発していますが、最初にコードを実行する前だとすると... 個々のシステムによると思いますが、シンプルな通信業界向けのシステムであると仮定しましょう。
きっとこうすると思います。C++のシステムという前提です。少なくともコンストラクタとデストラクタは、然るべきものを作ります。あとはオブジェクト間の重要な関連付けができるように...
12. 素晴らしい!では我々の相違点はどこにあるのでしょうか?おそらくそれはTDDとプロフェッショナリズムの考え方についてではないでしょうか。次はそのことについて話しましょう。
そのことについては個別に反論はありますが、我々の間では解決できるものだと考えています。それはそれでよいのですが。
これを聞いている人達のために再度明確にしておきたいと思います。正しいやり方をしている人に出会えた時に、以前からこの種の問題の回避方法は、話合われてきたのだと考えるようになりました。そうしたことは、TDDの書籍や、TDD入門では扱われていません。
うまくやっている人たちも、例えばDan North氏が今BDDと呼んでいるような手法に乗り換えてきています。私はBDDは本当に素晴らしいと思っています。(RSpecや、低レベルな部分に引きずられているところを除けば、ですが。)
すでに正しいやり方をしている人はたくさん居るのに、それをTDDと呼んでしまっています。その結果、本を買ってTDDを調べ「アーキテクチャはテストによってしか得られないのだ」という古い考え方に辿り着いてしまうことを懸念しています。そういうセリフをこの6カ月のチュートリアルで4回聞きました。あなたが言ったようにそれはただの馬糞です。
ところで、今はプロフェッショナルの議論ということですが、あなたがプロフェッショナルについて考えがあるのであれば、それはどのようなものでしょうか?
14. ええ。それは私も理解しています。ただ、私はそこから一歩進めたいのです、なぜなら我々の業界はどこかプロフェッショナルの基準というものに欠けているからです。
スタート地点として、あなたの定義を確認する必要がありそうですね。そうすれば議論ができると思います。
私は同意できません。
ここは重要で深いポイントだと思うので、例を出して反論させてください。
他の例も出せますが一例として。コードインスペクションやペアプログラミングはとてもよいもので、きっとすごい価値があるという見解に対して、手を振り回してなんやかんやいちゃもんを付けることもできますが、それは別の機会にしましょう。
より重要で、的を射ていると思っていることを言わせてください。
まず、ユニットテストが何なのかということに注目してみましょう。
ユニットテストが何なのかというと、APIや、引数を持つ状態空間がうまく動くか調査するもので、それが半ダースとか百とか、場合によっては数百万(2の32乗)ぐらいの数になることもあります。それを淡々と試行してゆくわけです。
これは本当に発見的なやり方です。そうやってバグを見つけられるのは本当に幸運としか言いいようがないです。
私がより効果的だと考えているのは「契約による設計(Design by Contract)」です。
「契約による設計」では事前条件と事後条件と、そして不変表明を書きます。
ただ、殆どの言語ではこの技術はサポートされていません。
この技術は静的に条件のチェックができる言語でしか使用できないし、Eiffel以外の言語では成熟しているとは言えません。しかし、似たような機能を基盤技術に追加することはできます。
「契約による設計」はあらゆる面でTDDより優れていると考えています。例えば、コードを堅牢にしたいときや、外部からの見え方に焦点を当てたいとき、などが長所です(そう私は思います)。
そして、少なくとも自分にとっては、この「契約」はテストよりも効果的だと気付きました。
さらに言うと、より広範囲なカバレッジを実質的に保証してくれます。ただ無作為に適当な値をいくつかばらまくより、引数の入力範囲をカバーすることができるからです。
最近、Bertrand Meyer氏はこの概念をさらに進め、CDD - 契約駆動開発(Contract Driven Development)と呼んでいるものに取りかかっています。これは何をしているかというと、まず「契約」を設定し、それに基づいた任意の値をいくつか入力してみます。ここで事前条件を満たしていなければ、それはテストは失敗することがわかるので、実行しません。テストをしてみて実行後に事後条件が満たされていれば問題無いですが、そうでなければバグです。
彼らは実際にこの手法を行ってみました。
テストを自動実行できるツールも使いました。
Eiffelライブラリで、1週間使ってこれを実行してみたところ、20年間使われてきたEffielライブラリで、7つのバグを見つけてしまいました。これはちょっと興味深いですね。
ところで、契約による設計では、ビジネス的な意義を突き止めうる方法で、コードを意識的に表現していますが、TDDで問題だと思うのは、多くの開発者がビジネス的な意義をクラスレベルまで落とさないといけないということです。これは事実そうなっていると思います。時としてビジネス的な意義を突き止めるための手段が、クラスレベルでAPIを追跡するしかないことがありますが、それは本当に大変なことです。
それは無理に二元的な考え方をしているのではないでしょうか。考えるべきは一つしかなくて、それはコードであり、コードはつまり設計そのものです。それが成果物です。それ以外はリーンとは言えません。
ユニットテストを使っている典型的なプロジェクトでは、コード量とテストの量はだいたい同じです。そしてコードがあるところにはバグがあります。
自分の作業速度を半分にしているようなものです。
よく知られている例として一番有名なのは、ADAコンパイラがテスト駆動開発を使うことで、逆にバグを増やしてしまったというものです。これは、テストの量を増やすことで、コードの量を増やしてしまったことが原因です。
(「契約による設計」の)表明を使えば、インタフェースが持つセマンティクスとコード自体との間に良い、というかむしろ不可欠の結合を持つことができます。
一方で、テストを使った結合は、はるかに厄介なもので、管理しにくいと言えます。
もう一つ反論しようと思っていたポイントがあります...