TDD Boot Campのコード
というわけで、晒します。
前半は@katzchang・@kozy4324のペア。後半は@katzchang・@yugoriのペア。
Eclipseは独自に履歴を持っているので、2パターンを引っ張り出してきました。クラス宣言部にカーソルを当てて右クリックからLocal History。たぶん20セーブくらいしか保持してないけど、たまに助かることもあります。
仕様変更直後くらい
ということで、仕様変更直後くらいのセーブから。大体の時間で取っているので、REDな組み合わせかも知れません。
基本的には、テストコードは下に順に追加していっています。上にある項目から順に、プロダクトコードを作り込んでいったってことです。
第一の仕様変更(キャッシュサイズを変更できるようにする)まで対応済、第二の仕様変更(一定時間が経過したデータはキャッシュから消える)に対応しようとしたら、既存機能にバグの疑いがあり、バグを出現させるテストコードを書いてREDであることを確認、改修してGREENになった直後です(たぶん)。
このときは第二の仕様変更のテストコードは途中まで書いていたけど、とりあえずコメントアウト。@Igunoreでもいいかもしれないけど、コンパイルエラーとか拾われたら面倒なのでコメント化してます。
その他、仕様変更の未対応のものは"// TODO"タグでメモ。あと、ミルズさんに「このーputputってすごい長いよね」と指摘されたので、これもTODOタグを付けています。
テストコード
package tddbc; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; import org.hamcrest.core.IsNull; import org.junit.Before; import org.junit.Test; public class LRUCacheTest { LRUCache<String, String> target; @Before public void setup() throws Exception { target = new LRUCache<String, String>(3); } @Test public void putしたものがgetできる() throws Exception { target.put("a", "A"); assertThat(target.get("a"), is("A")); } @Test public void sizeがとれる() throws Exception { assertThat(target.size(), is(0)); target.put("a", "A"); assertThat(target.size(), is(1)); target.put("b", "B"); assertThat(target.size(), is(2)); } @Test public void リストが更新される() throws Exception { target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); assertThat(target.get("a"), is("A")); assertThat(target.get("b"), is("B")); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), nullValue()); target.put("d", "D"); assertThat(target.get("a"), nullValue()); assertThat(target.get("b"), is("B")); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), is("D")); } @Test public void 上限値を設定できる() throws Exception { target = new LRUCache<String, String>(1); target.put("a", "A"); target.put("b", "B"); assertThat(target.size(), is(1)); } @Test(expected=IllegalArgumentException.class) public void 上限値は0はだめ() throws Exception { target = new LRUCache<String, String>(0); } @Test public void 上限値は1はおk() throws Exception { target = new LRUCache<String, String>(1); } @Test(timeout=3000) public void _1万回の操作が3秒以内に終わること() throws Exception { LRUCache<Integer, String> target = new LRUCache<Integer, String>(1); for (int i = 0; i < 10000; i++) { target.put(i, "A"); } } @Test public void 古いものが消すリストの先頭に来ること() throws Exception { target.put("a", "A"); // a target.put("b", "B"); // a,b target.put("c", "C"); // a,b,c assertThat(target.oldest(), is("a")); target.get("a"); // b,c,a assertThat(target.oldest(), is("b")); target.put("b", "B"); // c,a,b assertThat(target.oldest(), is("c")); } @Test public void とちゅうでgetしてputしたら次のヤツが消えるか() throws Exception { target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); target.get("a"); target.put("d", "D"); assertThat(target.get("b"), nullValue()); } @Test public void 一つも使われていない場合は最初に追加したものから消える() throws Exception { target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); target.put("d", "D"); assertThat(target.get("a"), IsNull.nullValue()); } @Test public void getされたら使われたと見なす() throws Exception { target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); target.get("a"); target.put("d", "D"); assertThat(target.get("b"), nullValue()); } //TODO 一定時間でキャッシュは消える。 //TODO スレッドセーフ @Test public void キャッシュサイズをふやせる() throws Exception { target.setCacheSize(1); target.put("a", "A"); target.put("b", "B"); assertThat(target.size(), is(1)); target.setCacheSize(2); target.put("c", "C"); assertThat(target.size(), is(2)); } @Test public void キャッシュサイズを減らせる() throws Exception { target.setCacheSize(4); target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); target.put("d", "D"); target.setCacheSize(2); assertThat(target.get("a"), nullValue()); assertThat(target.get("b"), nullValue()); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), is("D")); } @Test public void historyが消えてないかも() throws Exception { target.setCacheSize(3); target.put("a", "A"); target.put("b", "B"); target.put("c", "C"); target.put("d", "D"); //should remove key a target.put("e", "E"); //should remove key b assertThat(target.get("b"), nullValue()); } //TODO putputputって書くのがめんどくさい。 // @Test // public void 更新時刻を持っていること() throws Exception { // long start = System.currentTimeMillis(); // target.put("a", "A"); // long end = System.currentTimeMillis(); // assertTrue(start <= target.updateTimeOf("a")); // assertTrue(target.updateTimeOf("a") <= end); // } // @Test // public void キャッシュ保持期間が指定できる() throws Exception { // target.setAlive(1000); // target.put("a", "A"); // Thread.sleep(1010); // assertThat(target.get("a"), nullValue()); // } }
プロダクトコード
package tddbc; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class LRUCache<K, V> { final Map<K, V> cache; int limit; final List<K> history; final List<Long> historyTime; long alive; public LRUCache(int limit) { if(limit < 1) throw new IllegalArgumentException(); this.history = new ArrayList<K>(limit); this.historyTime = new ArrayList<Long>(limit); this.cache = new HashMap<K, V>(limit); this.limit = limit; } public LRUCache() { this(10); } public void put(K key, V value) { if (limit <= cache.size()) { cache.remove(history.remove(0)); } cache.put(key, value); updateOldest(key); } void removeHistoryAt() { } private void updateOldest(K key) { history.remove(key); history.add(key); } public V get(K key) { if(history.contains(key)) { updateOldest(key); } return cache.get(key); } public Integer size() { return cache.size(); } K oldest() { return this.history.get(0); } public void setCacheSize(int i) { while(history.size()>i){ cache.remove(history.remove(0)); } limit=i; } public void setAlive(long alive) { this.alive = alive; } long updateTimeOf(String string) { // TODO Auto-generated method stub return 0; } }
最終的に
第二の仕様変更に対応。あと、テストのためのtarget.putが連続して長いよねという指摘に対応しようとして、途中までしか直してません。
プロダクトコード側もだいぶぐちゃぐちゃになってきたので、クラスを分割しようとしたところであと10分なので、とりあえずここまでです。
テストコード
package tddbc; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; import org.hamcrest.core.IsNull; import org.junit.Before; import org.junit.Test; public class LRUCacheTest { LRUCache<String, String> target; @Before public void setup() throws Exception { target = new LRUCache<String, String>(3); } private void fillTarget(String...keys) { for (String key : keys) target.put(key, key.toUpperCase()); } @Test public void putしたものがgetできる() throws Exception { fillTarget("a"); assertThat(target.get("a"), is("A")); } @Test public void sizeがとれる() throws Exception { assertThat(target.size(), is(0)); fillTarget("a"); assertThat(target.size(), is(1)); fillTarget("b"); assertThat(target.size(), is(2)); } @Test public void リストが更新される() throws Exception { fillTarget("a", "b", "c"); assertThat(target.get("a"), is("A")); assertThat(target.get("b"), is("B")); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), nullValue()); fillTarget("d"); assertThat(target.get("a"), nullValue()); assertThat(target.get("b"), is("B")); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), is("D")); } @Test public void 上限値を設定できる() throws Exception { target = new LRUCache<String, String>(1); fillTarget("a", "b"); assertThat(target.size(), is(1)); } @Test(expected=IllegalArgumentException.class) public void 上限値は0はだめ() throws Exception { target = new LRUCache<String, String>(0); } @Test public void 上限値は1はおk() throws Exception { target = new LRUCache<String, String>(1); } @Test(timeout=3000) public void _1万回の操作が3秒以内に終わること() throws Exception { LRUCache<Integer, String> target = new LRUCache<Integer, String>(1); for (int i = 0; i < 10000; i++) { target.put(i, "A"); } } @Test public void 古いものが消すリストの先頭に来ること() throws Exception { fillTarget("a", "b", "c"); assertThat(target.oldest(), is("a")); target.get("a"); // b,c,a assertThat(target.oldest(), is("b")); fillTarget("b"); assertThat(target.oldest(), is("c")); } @Test public void とちゅうでgetしてputしたら次のヤツが消えるか() throws Exception { fillTarget("a", "b", "c"); target.get("a"); fillTarget("d"); assertThat(target.get("b"), nullValue()); } @Test public void 一つも使われていない場合は最初に追加したものから消える() throws Exception { fillTarget("a", "b", "c", "d"); assertThat(target.get("a"), IsNull.nullValue()); } @Test public void getされたら使われたと見なす() throws Exception { fillTarget("a", "b", "c"); target.get("a"); fillTarget("d"); assertThat(target.get("b"), nullValue()); } //TODO スレッドセーフ @Test public void キャッシュサイズをふやせる() throws Exception { target.setCacheSize(1); fillTarget("a", "b"); assertThat(target.size(), is(1)); target.setCacheSize(2); fillTarget("a"); assertThat(target.size(), is(2)); } @Test public void キャッシュサイズを減らせる() throws Exception { target.setCacheSize(4); fillTarget("a", "b", "c", "d"); target.setCacheSize(2); assertThat(target.get("a"), nullValue()); assertThat(target.get("b"), nullValue()); assertThat(target.get("c"), is("C")); assertThat(target.get("d"), is("D")); } @Test public void historyが消えてないかも() throws Exception { target.setCacheSize(3); fillTarget("a", "b", "c", "d"); fillTarget("d"); //should remove key a fillTarget("e"); //should remove key b assertThat(target.get("b"), nullValue()); } @Test public void 更新時刻を持っていること() throws Exception { { long start = System.currentTimeMillis(); target.put("a", "A"); long end = System.currentTimeMillis(); assertTrue(start <= target.updateTimeOf("a")); assertTrue(target.updateTimeOf("a") <= end); } { long start = System.currentTimeMillis(); target.put("a", "A"); long end = System.currentTimeMillis(); assertTrue(start <= target.updateTimeOf("a")); assertTrue(target.updateTimeOf("a") <= end); } { assertTrue(target.updateTimeOf("b") == 0); long start = System.currentTimeMillis(); target.put("b", "B"); long end = System.currentTimeMillis(); assertTrue(start <= target.updateTimeOf("b")); assertTrue(target.updateTimeOf("b") <= end); } } @Test public void キャッシュ保持期間が指定できる() throws Exception { target.setAlive(1000); target.put("a", "A"); target.put("b", "B"); Thread.sleep(1010); target.put("c", "C"); assertThat(target.get("a"), nullValue()); assertThat(target.get("b"), nullValue()); assertThat(target.get("c"), notNullValue()); } }
プロダクトコード
package tddbc; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class LRUCache<K, V> { final Map<K, V> cache; int limit; final List<K> history; final List<Long> historyTime; long alive; public LRUCache(int limit) { if(limit < 1) throw new IllegalArgumentException(); this.history = new ArrayList<K>(limit); this.historyTime = new ArrayList<Long>(limit); this.cache = new HashMap<K, V>(limit); this.limit = limit; this.alive = 0; } public LRUCache() { this(10); } public LRUCache<K, V> put(K key, V value) { if (limit <= cache.size()) removeCache(0); cache.put(key, value); updateOldest(key); return this; } void removeCache(int i) { cache.remove(removeHistoryAt(i)); } K removeHistoryAt(int i) { historyTime.remove(i); return history.remove(i); } private void updateOldest(K key) { if (history.contains(key)) removeHistoryAt(history.indexOf(key)); history.add(key); historyTime.add(System.currentTimeMillis()); } public V get(K key) { if (alive > 0) clearDeadCache(); if (history.contains(key)) updateOldest(key); return cache.get(key); } void clearDeadCache() { long now = System.currentTimeMillis(); for (int i = 0; i< historyTime.size(); i++) { long dead = historyTime.get(i) + alive; if(dead < now) removeCache(i); } } public Integer size() { return cache.size(); } K oldest() { return this.history.get(0); } public void setCacheSize(int i) { while(history.size()>i) removeCache(0); limit = i; } public void setAlive(long alive) { this.alive = alive; } long updateTimeOf(K key) { if (history.contains(key)) return historyTime.get(history.indexOf(key)); return 0; } }