« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その1) | トップページ | [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7] »

2011年8月18日 (木)

[コラム] 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> コレクションにデータを格納しています。

  private Dictionary<string, string> _dic = new Dictionary<string, string>();

これを、 KeyValueTime 型を格納し、 なおかつ自動的にソートしてくれる SortedSet<KeyValueTime> に置き換えます。

  private SortedSet<KeyValueTime> _dic = new 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] »

*コラム」カテゴリの記事

<NUnit>」カテゴリの記事

コメント

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その2) ~ 最初の仕様変更[T16MAIN-5]:

» [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7] [TDD.NET]
前回は、 [T16MAIN-5] までやりました。 今回のこの2つの仕様変更は、 機能の追加です。 ですから、 新しいメソッドをどんどん TDD していけばよいです。 ◆ ひとつめの機能追加■ T16MAIN-6: dump の引数に時刻を指定できるようにする。 dump 関数は時刻が指定された場合、 指定時刻以降のデ... [続きを読む]

受信: 2011年8月30日 (火) 19時26分

« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その1) | トップページ | [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7] »