Tester-DoerパターンとCode Contracts
- 2011-04-26
僕と契約して安全性の高いソフトウェアを作ってよ!というだけじゃ、何か、弱い。動機付けに足りない。という、分かったような分からないようなCode Contracts。困ったところは、で、何が嬉しいの?にたいする積極的具体的な動機付けを提供しにくいということ。契約をしっかり行うことで、強固なソフトウェアが設計出来ます。うーん、理念は分かりますけど実用的に便利ー?if hoge==null throw に毛が生えた程度のものだったら、ちょっとよくわからない。
// こういうコード見るともう目も当てられなくて、画面の半分が引数チェックで埋まってるよ!
public void Hoge(string arg1, string arg2, string arg3)
{
if (arg1 == null)
{
throw new ArgumentNullException("arg1");
}
if (arg1.Length == 0)
{
throw new ArgumentException("arg1");
}
if (arg2 == null)
{
throw new ArgumentNullException("arg2");
}
if (arg2.Length == 0)
{
throw new ArgumentException("arg2");
}
if (arg3 == null)
{
throw new ArgumentNullException("arg3");
}
if (arg3.Length == 0)
{
throw new ArgumentException("arg3");
}
// やっとメソッドの本体...
}
うん、これは、イヤ。ifは必ず{}をつけなければ、とやると行数が嵩んで最悪の視認性に。個人的には、明らかに一行な処理はifの真横に書いてもいいと思う。
// これぐらいなら許す(えらそう)
public void Hoge(string arg1, string arg2, string arg3)
{
if (string.IsNullOrEmpty(arg1)) throw new ArgumentException("arg1");
if (string.IsNullOrEmpty(arg2)) throw new ArgumentException("arg2");
if (string.IsNullOrEmpty(arg3)) throw new ArgumentException("arg3");
// ↑もしくはGuard.NotNull(arg1, "arg1"); とか用意するなど、ね。
}
そんなわけかで、この手のnullチェックが好きでなくて、必要性だって、どうせ次の行のその引数使うところで死ぬんだからどうでもよくね?と思う場合があまりにも多いともにょもにょもにょ。書くけど書かないけど。
その延長線上でContractsも面倒くさいしなー、と思っていた時もありました。しかしCode Contractsは、あらゆる方向から契約を積極的に行うための動機付けを提供してくれています。Premiumにしか提供されていない静的チェックが最も強力なのは確かですが、Standardのユーザーのためにも、ドキュメント生成、IntelliSense表示サポート、Pex自動テスト生成サポート、引数名を文字列で書かなくていい。などなど。
そこまであの手この手で、契約するといいよ、と迫ってこられれば納得です。理屈上素晴らしいから、というだけじゃなくて、何だかんだで面倒くさいものを、現実的にこんなにメリットがあるから契約しようよ!という。そういう姿勢がいいよね、普及させるために全方位から攻めるというの。それだけ並べられれば、そりゃ書くってものですよ?
特にIntelliSense厨の私は、IntelliSenseへの契約表示に感動しまして。Enumerable.Rangeで条件が表示されてるよ、きゃー!って。そして、BCLが契約を表示してくれるなら、自前のクラス群も契約表示させてあげたい。と、いうのが一番のCode Contractsやろう!という動機付けになりましたね。頻繁にクラッシュするんですが、その辺は多めに見てあげます。
Tester-DoerパターンとCode Contracts
Code Contracts自体は第54回CLR/H勉強会発表資料を公開します。 - Bug Catharsisの「オブジェクト指向と契約による設計」と「Code Contracts入門」という資料が素晴らしいので、今すぐそちらを見たほうがいいです!
というわけでCode Contracts自体には全く触れないで、例でも。
// Addのないstring, string辞書(これはひどい)
public class StringDictionary
{
Dictionary<string, string> dict = new Dictionary<string, string>();
public bool ContainsKey(string key)
{
return dict.ContainsKey(key);
}
public string Get(string key)
{
return dict[key];
}
}
static void Main(string[] args)
{
var dict = new StringDictionary();
// 存在しないキーをGetすると例外
dict.Get("hogehoge");
// チェックしてから取得する(Tester-Doerパターン)
if (dict.ContainsKey("hogehoge"))
{
dict.Get("hogehoge");
}
}
取得時にダメな可能性があるものは、先にチェックしてから取りに行く。といったことは、.NETのクラスライブラリ設計に書いてあるので読もう~。とてもお薦め本。
問題は、キーの存在チェックをすることがほぼ必須なのに、それを強制出来ないんですね。あくまで任意でしかなく、別にチェックしなくてもコンパイラ通るし、そうして書かないでいるとうっかりな例外発生の可能性を常に抱えてしまう。こういう問題は例外自体にも言えますが。ほぅ、では検査例外が必要か…… いえ、あれはいりません。
まあともかくで、ここでCode Contractsを使うとどうなるか、というと……
public class StringDictionary
{
Dictionary<string, string> dict = new Dictionary<string, string>();
[Pure] // Pureじゃないと怒られるのでPureってことにしておこう(善意の申告制です)
public bool ContainsKey(string key)
{
return dict.ContainsKey(key);
}
public string Get(string key)
{
Contract.Requires(ContainsKey(key)); // 事前条件 ContainsKeyがtrueでなければダメ
return dict[key];
}
}
static void Main(string[] args)
{
var dict = new StringDictionary();
// ContainsKeyの前にGetしようとすると...
dict.Get("hogehoge");
}
静的チェッカーが警告を出してくれます。いいねいいね。そんなわけで、Code Contractsを使うと、比較的安全にTester-Doerパターンが適用できるのでした。こういう、コードだけでは表現できない約束事を表現でき、実行時ではなくコンパイル時に検出出来るようになる、っていうのは、魅力的な話なのではと思います。
まとめ
Code Contractsは一部で無理矢理感が否めません。そもそもバイナリリライター必須なうえに、文法的にもContract.Requiresぐらいならまあいいとしても、Ensures(Contracts.Result)やInvariantやContractClassForはC#的に不自然さを残す介入の仕方で、些か残念と言わざるをえない。
不自然さ漂う記述方法がC#に統合される(それSpec#)日は来るのだろうか(絶対来ないよね)。Spec#は軽く仕様とTutorialを眺めた感じだと、やはり言語統合されてると、洗練されてるし、書きやすさも段違いになるよねえ、などと思いました。全ては必要ないけれど、一部はC#に入って欲しい、のですが文法と衝突しなくても、既存のコードとルールが衝突してしまったりするので難しいかなあ。
とはいえ、Code Contracts全体のエコシステムがもたらすメリットも多大だし、mscorlibすらContractsが書き足されている(.NET4から)ぐらいなので、今のうちにそれに従って流行りものに乗っかるのもいいと思います。流行ってるか謎ですが。単純なnullチェックぐらいならサクッと書けますが、少し凝った契約をしようとすると途端にワケワカランし正道がサッパリ。という敷居の高さはありますが……。
実際ヨクワカラナイデス。少しよーし、張り切って書いちゃうぞー、とやると、それダメそれダメ、とリライターに言われてしまったりでnullチェックに毛の生えた程度しか使いこなせない昨今です。それだけでも有益といえば有益なんですが、あちこちでrequires not null, ensures not nullを書いていると、もうデフォルトを非nullにしてくれよ!と叫ばずにはいられない。何ともいえない不毛感がちょっと、かなり、嫌。
あと、静的チェッカが上手く機能するように書くには静的チェッカが必要なのも。当たり前?うーん、静的チェッカなしで、ただ普通にContractsを書いているだけじゃあダメなのかな?契約自体は成立していてリライターは通るけど静的チェッカ使うと警告だらけ。みたいな形になりがちで。Standardで書いてPremiumでチェックしたら涙目の落差が激しすぎて、じゃあ結局は静的チェッカ必須なの?でもそれPremium以上じゃん、というのが残念で。そうなるとStandardにも欲しいねえ、静的チェッカー。
でも、ちょっと気の利いたGuard句として、事前条件だけで画面半分が埋まるような事態が緩和されるなら、それはとっても嬉しいなって。まずは、その程度から始めよう。順を追ってステップアップすればいいのだから。