[コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その2) ~ 最初の仕様変更[T16MAIN-5]
前回の記事で、 当初の仕様 (T16MAIN-1 ~ 4) を満たすコードは、 さっくり完成しました。 というところで…
TDDBC 名物、 仕様変更がやってまいりました!
■ T16MAIN-5: put の引数で key, value, date(時刻情報) を渡し、 dump を時間順に出力するように仕様変更
・ put の引数で key, value, date(時刻情報) を渡せるようにする。
・ また、 dump 関数は時刻が新しい方から古い方へ順に key、 value を出力するように変更する。
・ 引数に複数指定して追加する関数の場合、 後ろにあるものほど新しいとみなす。
ここまでは、 key と value を持つ Dictionary<string, string> コレクションを使ってデータを保持してきました。 しかし、 この仕様変更に対応するには、 時刻情報も保持しなければなりません。 さて、 どうしましょう?
ひとつには、 Dictionary に渡すキーを、 今までの key だけから、 (key + date) にするという手がありますね。
※ あるいは、 value と date のペアをバリューに渡すとか、 あるいは、 key と date を保持するための Dictionary を増やすとか。
※ TDD 的には、 おそらくそういった方法が正攻法です。 引数に時刻情報を追加した Put() のテストケースを書いて、 それを通すための最小限の変更を製品コードに加えていくと、 たぶんそういう形になるでしょう。
今回は、 key と value と date をそのまま保持できるバリューオブジェクトを導入してみましょう。 そうしておけば、並べ替えや抽出のロジックを作ることになっても、たぶんきれいに行くでしょう。 ただし、 こういった将来のための設計 (先行投資) は、 TDD の流儀からは外れます。
さて。 バリューオブジェクトとしては、 Tuple を使いたい誘惑に駆られます。 しかし、 Tuple は一時的に使うものです。 あるいは、 プライベートなメンバーとして外に見せない使い方がせいぜいです。
※ public に Tuple<string, string, string> なんて型を返されても、 なんだか分かりませんね。 格納している値にアクセスするために result.Item2 と、 時刻情報には result.Item3 などと書かねばならないのも、 利用する立場としては願い下げです。
◆ バリューオブジェクト KeyValueTime を作る
データを格納するバリューオブジェクトは、 Dump() メソッドなどで public に返したいので、 分かりやすくするために新しい型を作ることにします。 .NET Framework の KeyValuePair クラスに似ていますが、 時刻情報も持っているところが違います。
クラス名は KeyValueTime にしましょう。
※ KeyValueTime から KeyValuePair が連想できなさそうであれば、 もっと直截に KeyValueWithTimestamp などとしても良いでしょう。
KeyValueTime クラスが公開すべきものはなんでしょう。
とりあえず必要なもの
プロパティ: Key, Value, Time
コンストラクター: KeyValuTime(key, value, time)
現在のテストケースと実装で使うためには、 コンストラクターの最後の引数 time には null も渡すことが出来た方がよさそうです。
プロパティ: string Key, string Value, Nullable<DateTime> Time
コンストラクター: KeyValuTime(string key, string value, Nullable<DateTime> time)
コンストラクターとプロパティは、 いっぺんにテストケースを書いてしまいましょう。
[Test()]
public void Test01_コンストラクトして3つのプロパティを読み出し() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt = new KeyValueTime("AAA", "Value1", dt);
string key = kvt.Key;
Assert.That(key, Is.EqualTo("AAA"));
string val = kvt.Value;
Assert.That(val, Is.EqualTo("Value1"));
DateTime time = kvt.Time.Value;
Assert.That(time, Is.EqualTo(dt));
}
これで KeyValueTime クラスを書き始めることができます。
public class KeyValueTime {
public KeyValueTime(string key, string value, DateTime time) {
this.Key = key;
this.Value = value;
this.Time = time;
}
public string Key { get; set; }
public string Value { get; set; }
public DateTime Time { get; set; }
}
time は null 許可型にします。
[Test()]
public void Test02_Timeにはnullもセット可能() {
KeyValueTime kvt = new KeyValueTime("AAA", "Value1", null);
Assert.That(kvt.Time, Is.Null);
}
public class KeyValueTime {
public KeyValueTime(string key, string value, DateTime? time) {
this.Key = key;
this.Value = value;
this.Time = time;
}
public string Key { get; set; }
public string Value { get; set; }
public DateTime? Time { get; set; }
}
あとは…
ソートのロジックは .NET Framework に任せてしまいたいので、 IComparable インターフェースの実装も必要になるでしょう。 .NET Framework には、 オブジェクトを追加したときに自動的にソートしてくれるコレクションがいくつかありますが、 いずれも IComparable の実装を要求しますので、 これも実装しておきましょう。
※ ここでの IComparable の実装は、HashCode の比較ぐらいにしておくべきでした。テストケースを勝手に想定して、失敗しました。 与えられた仕様に基づくテストケースは、Dump() メソッドなどでしか作れないのですから、 この時点ではまさに仮実装としておくべきでした。
以下は失敗例ということで、 掲載しておきます。
[Test()]
public void CompareToTest01_Key_Value_Timeともnullではなく等しい() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt);
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value1", dt);
Assert.That(kvt1.CompareTo(kvt2), Is.EqualTo(0));
}
[Test()]
public void CompareToTest02_Key_Value_Timeともnullではない_Timeだけ違う() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt);
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value1", dt.AddSeconds(1.0)); //新しい → 前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.LessThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.GreaterThan(0));
}
[Test()]
public void CompareToTest03_Key_Value_Timeともnullではない_Valueが違う() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt); //こっちが前
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value2", dt.AddSeconds(1.0));
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
[Test()]
public void CompareToTest04_Key_Value_Timeともnullではない_Keyが違う() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt); //こっちが前
KeyValueTime kvt2 = new KeyValueTime("BBB", "Value1", dt.AddSeconds(1.0));
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
// IComparable
public int CompareTo(object obj) {
KeyValueTime kvt = obj as KeyValueTime;
if (kvt.Key == this.Key && kvt.Value == this.Value)
return DateTime.Compare(this.Time.Value, kvt.Time.Value);
if (kvt.Key == this.Key)
return string.Compare(kvt.Value, this.Value);
//return 0;
return string.Compare(kvt.Key, this.Key);
}
◆ バリューオブジェクトを置き換える
※ 注意。 このステップは TDD していません!! テストコードをいじってレッドにしていないので、 テストファーストではありません。 コードがシンプルになるわけでもないので、 リファクタリングでもありません。
オールグリーンになることを確かめてから、 製品コードのバリューオブジェクトを置き換えます。
現在の TddbcDictionary クラスは、 メンバー変数の Dictionary<string, string> コレクションにデータを格納しています。
これを、 KeyValueTime 型を格納し、 なおかつ自動的にソートしてくれる SortedSet<KeyValueTime> に置き換えます。
するとコンパイルエラーが出るので、 その部分を修正します。 修正後、 オールグリーンになったら、 修正した部分を見直し、 新しくレッドになるテストケースが作れないかどうか検討します。
【新しく追加したテストケース】
[Test()]
public void PutGetTest02_keyにnull_例外() {
const string value = "Test";
TddbcDictionary dic = new TddbcDictionary();
Assert.Throws<ArgumentNullException>(new TestDelegate(() => dic.Put(null, value)));
//※ 実装変更無しで GREEN だった。不要。 → KeyValueTime の導入で RED になった。
}
// ↓ KeyValueTime の導入で追加
[Test()]
public void PutGetTest03_ひとつ登録_nullで取得() {
const string key = "AAA";
const string value = "Test";
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key, value);
Assert.Throws<ArgumentNullException>(new TestDelegate(() => dic.Get(null)));
}
[Test()]
public void DeleteTest03_keyにnull_例外() {
TddbcDictionary dic = new TddbcDictionary();
Assert.Throws<ArgumentNullException>(new TestDelegate(() => dic.Delete(null)));
//※ 実装変更無しで GREEN だった。不要。 → KeyValueTime の導入で RED になった。
}
[Test()]
public void PutGetTest03_2回登録ただし同じkey() {
const string key = "AAA";
const string value1 = "Test1";
const string value2 = "Test2";
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key, value1);
dic.Put(key, value2);
string result = dic.Get(key);
Assert.That(result, Is.EqualTo(value2));
// KeyValueTime の導入で、次が RED になる。(今までは、Dictionary<> が上手くやってくれていた)
Assert.That(dic.Dump().Count, Is.EqualTo(1));
}
【新しく追加したテストケースも通るようにした製品コード】
※ KeyValueTime 導入前のコードはコメントアウトして残してある。
public class TddbcDictionary {
//private Dictionary<string, string> _dic = new Dictionary<string, string>();
private SortedSet<KeyValueTime> _dic = new SortedSet<KeyValueTime>();
public void Put(string key, string value) {
//this._dic[key] = value;
KeyValueTime existing = this._dic.FirstOrDefault(kvt => string.Equals(kvt.Key, key));
if (existing != null) {
existing.Value = value;
return;
}
this._dic.Add(new KeyValueTime(key, value, null));
}
public string Get(string key) {
//return this._dic[key];
if (key == null)
throw new ArgumentNullException();
return this._dic.First(kvt => kvt.Key == key).Value;
}
public IList<KeyValueTime> Dump() {
//return this._dic.ToList<KeyValuePair<string, string>>();
return this._dic.ToList();
}
public void Delete(string key) {
//this._dic.Remove(key);
if (key == null)
throw new ArgumentNullException();
this._dic.RemoveWhere(kvt => kvt.Key == key);
}
public void MultiPut(IList<KeyValuePair<string, string>> data) {
foreach (var kv in data) {
if (kv.Key == null)
throw new ArgumentNullException();
}
foreach (var kv in data) {
this.Put(kv.Key, kv.Value);
}
}
}
※ この時点でのソースコード一式 ⇒ TddbcTokyo1.6_CS02a.zip (14KB) (C# 2010 + NUnit 2.6)
◆ 仕様を追加する
既存のコードを、 KeyValueTime を使うようにする修正が完了しました。 ようやく TDD で仕様変更に対応できます。 仕様変更の内容を再掲しておきます。
■ T16MAIN-5: put の引数で key, value, date(時刻情報) を渡し、 dump を時間順に出力するように仕様変更
・ put の引数で key, value, date(時刻情報) を渡せるようにする。
・ また、 dump 関数は時刻が新しい方から古い方へ順に key、 value を出力するように変更する。
・ 引数に複数指定して追加する関数の場合、 後ろにあるものほど新しいとみなす。
※ 最後の 「後ろにあるものほど新しいとみなす」 が意味不明です。 複数指定して追加するときのデータ形式も変えるんでしょうか? それとも、 現在時刻を勝手に割り当てるのでしょうか? これは TODO: に残して、 先送りすることにします。
【追加したテストケース】
// 1) Put() の引数に time を追加 - これは、Put(key, value, time) のオーバーロードを追加実装する。
// ※ オーバーロードが出来上がってから、既存の Put(Key, value) をどうするか考える。
[Test()]
public void Put2Test01_ひとつ登録_ひとつ取得() {
const string key = "AAA";
const string value = "Test";
DateTime time = new DateTime(2011, 8, 3, 17, 40, 0);
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key, value, time);
string result = dic.Get(key);
Assert.That(result, Is.EqualTo(value));
// Put() した time は、ちゃんと保持されているか?
IList<KeyValueTime> data = dic.Dump();
Assert.That(data[0].Time, Is.EqualTo(time));
}
// 2) dump関数は時刻が新しい方から古い方へ順にkey、valueを出力するように変更する。
[Test()]
public void DumpTest02_2つ登録してDump_ただし時刻順で出すのでPut時と順番が変わる() {
const string key1 = "AAA";
const string value1 = "Test1";
DateTime time1 = new DateTime(2011, 8, 3, 17, 40, 0);
const string key2 = "BBB";
const string value2 = "Test2";
DateTime time2 = time1.AddSeconds(1.0); //こっちが新しい → Dump 順は逆になる
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key1, value1, time1);
dic.Put(key2, value2, time2);
IList<KeyValueTime> dump = dic.Dump();
Assert.That(dump[0].Key, Is.EqualTo(key2));
Assert.That(dump[0].Value, Is.EqualTo(value2));
Assert.That(dump[0].Time, Is.EqualTo(time2));
Assert.That(dump[1].Key, Is.EqualTo(key1));
Assert.That(dump[1].Value, Is.EqualTo(value1));
Assert.That(dump[1].Time, Is.EqualTo(time1));
}
//TODO: ん? 複数登録時はtime付けない? 分からないので後回し! > 引数に複数指定して追加する関数の場合、後ろにあるものほど新しいとみなす。
※ 上記 DumpTest02 の実装をしていて、 KeyValueTime.CompareTo() の仕様を間違えていたことに気付く。 テストケースからやり直しです。
【KeyValueTime の CompareTo() のテストケース (作り直し)】
[Test()]
public void CompareToTest01_Key_Value_Timeともnullではなく等しい() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt);
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value1", dt);
Assert.That(kvt1.CompareTo(kvt2), Is.EqualTo(0));
}
[Test()]
public void CompareToTest02_Key_Value_Timeともnullではない_Timeだけ違う() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt);
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value1", dt.AddSeconds(1.0)); //新しい → 前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
[Test()]
public void CompareToTest03_Key_Value_Timeともnullではない_ValueとTimeが違う_Timeが優先() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt); //Valueの比較だけならこっちが前だけど…
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value2", dt.AddSeconds(1.0)); //新しい → 前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
[Test()]
public void CompareToTest04_Key_Value_Timeともnullではない_KeyとTimeが違う_Timeが優先() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "Value1", dt); //Keyの比較だけならこっちが前だけど…
KeyValueTime kvt2 = new KeyValueTime("BBB", "Value1", dt.AddSeconds(1.0)); //新しい → 前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
[Test()]
public void CompareToTest05_Key_Value_Timeともnullではない_Timeが同じ_Valueでの比較になる() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("AAA", "ValueB", dt);
KeyValueTime kvt2 = new KeyValueTime("BBB", "ValueA", dt); //Valueの比較で、こっちが前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
[Test()]
public void CompareToTest06_Key_Value_Timeともnullではない_TimeもValueも同じ_Keyでの比較になる() {
DateTime dt = new DateTime(2011, 8, 3, 13, 55, 0);
KeyValueTime kvt1 = new KeyValueTime("BBB", "Value", dt);
KeyValueTime kvt2 = new KeyValueTime("AAA", "Value", dt); //Keyの比較で、こっちが前に来る
Assert.That(kvt1.CompareTo(kvt2), Is.GreaterThan(0));
Assert.That(kvt2.CompareTo(kvt1), Is.LessThan(0));
}
【製品コード】
public class KeyValueTime : IComparable {
public KeyValueTime(string key, string value, DateTime? time) {
if (key == null)
throw new ArgumentNullException();
this.Key = key;
this.Value = value;
this.Time = time;
}
public string Key { get; set; }
public string Value { get; set; }
public DateTime? Time { get; set; }
// IComparable
public int CompareTo(object obj) {
KeyValueTime kvt = obj as KeyValueTime;
if(kvt.Time != this.Time)
return DateTime.Compare(kvt.Time.Value, this.Time.Value);
if(kvt.Value != this.Value)
return string.Compare(this.Value, kvt.Value);
return string.Compare(this.Key, kvt.Key);
}
}
public class TddbcDictionary {
private SortedSet<KeyValueTime> _dic = new SortedSet<KeyValueTime>();
public void Put(string key, string value) {
KeyValueTime existing = this._dic.FirstOrDefault(kvt => string.Equals(kvt.Key, key));
if (existing != null) {
existing.Value = value;
return;
}
this._dic.Add(new KeyValueTime(key, value, null));
}
public void Put(string key, string value, DateTime time) {
//throw new NotImplementedException();
this._dic.Add(new KeyValueTime(key, value, time));
//TODO: はて? key が同じで time が違うときはどうすればいい?
}
public string Get(string key) {
if (key == null)
throw new ArgumentNullException();
return this._dic.First(kvt => kvt.Key == key).Value;
}
public IList<KeyValueTime> Dump() {
return this._dic.ToList();
}
public void Delete(string key) {
if (key == null)
throw new ArgumentNullException();
this._dic.RemoveWhere(kvt => kvt.Key == key);
}
public void MultiPut(IList<KeyValuePair<string, string>> data) {
foreach (var kv in data) {
if (kv.Key == null)
throw new ArgumentNullException();
}
foreach (var kv in data) {
this.Put(kv.Key, kv.Value);
}
}
}
仕様がよく分からなくて TODO コメントが増えてしまいましたが、 これで T16MAIN-5 の実装はいちおう完了です。
この時点でのソース:
テストケース https://github.com/biac/tddbc-tokyo-1.6/tree/b0bf6a12cf7bec9425087877c85aed9a881c4cdd/TddbcTokyo16Test
製品コード https://github.com/biac/tddbc-tokyo-1.6/tree/b0bf6a12cf7bec9425087877c85aed9a881c4cdd/TddbcTokyo16
しかし、 まだまだ仕様変更の嵐は吹き荒れるのです… (続く)
| 固定リンク
« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その1) | トップページ | [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7] »
「*コラム」カテゴリの記事
- MSTest‐Windows ストア アプリ開発の暗黒大陸 #win8dev_jp #tddadventjp #tddnet(2013.12.13)
- TDD って何だっけ? #tddadventjp(2013.12.06)
- [コラム] テストファーストとは何か?(2012.12.24)
- [コラム] Visual Studio 11 に統合できるテスティング フレームワーク(2012.03.22)
- [コラム] TDD のパターン: Assert First(2012.02.09)
「<NUnit>」カテゴリの記事
- [NEWS] NUnit Test Adapter for VS 2012 and 2013 1.0 RC(2013.09.17)
- [NEWS] NUnit 2.6.2 リリース ~ async/await に対応!(2012.11.16)
- [NEWS] NUnit 2.6.1 リリース ~ NuGet に対応(2012.08.17)
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発 第4回/第5回(2012.07.10)
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発 第2回/第3回(2012.04.13)
この記事へのコメントは終了しました。
コメント