LRUCache、仕様変更その1(事後の容量変更)まで
TDD Boot Campの課題の再挑戦です。
仕様変更その1(事後の容量変更)まで対応。ここまでは大して複雑にはならないのは、前回の通り。スナップショットとして。
ポイントは、キー管理を内部クラス"KeyHolder"に委譲していることか。put時の容量超過と、capacityを減らしたときのキャッシュ整理が一元化できている。ただ、「xxxer」的なクラスは、あまりいい匂いはしない。
テストコードは、アサーションを何個も重ねないよう気をつけてみた。1メソッド1アサートを厳守するのは難しいかもしれないけど、基本的には採用すべきな気はする。あとは、ヘルパメソッド"fillTarget"の記述を簡略化した。「テスト対象は主にキーであり、値はそれほど重要ではない」。
途中までは、前回と同様に、キー順序をテストするメソッドを入れていた。が、最終的に、KeyHolder#oldest()はテストコード以外のどこからも参照されないことに気づき、要らないと判断して削除した。削除したのは↓のコード。
@Test public void 最初にputしたものが一番古い() throws Exception { fillTarget(1,2,3); assertThat(target.keyHolder.oldest(), is("1")); } @Test public void 途中でputしたら次のものが一番古くなる() throws Exception { fillTarget(1,2,3,1); assertThat(target.keyHolder.oldest(), is("2")); } @Test public void 途中でgetしたら次のものが一番古くなる() throws Exception { fillTarget(1,2,3); target.get("1"); assertThat(target.keyHolder.oldest(), is("2")); }
//class KeyHolder K oldest() { return this.l.get(0); }
作ったコード
テストコード
package tddbc; import static org.junit.Assert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNull.*; import org.junit.*; // TODO 一定時間が経つと削除される // TODO スレッドセーフ public class LRUCacheTest { LRUCache<String, String> target; @Before public void setup() { target = new LRUCache<String, String>(); } void fillTarget(Integer...is) { for (Integer i : is) target.put(i.toString(), i.toString()); } @Test public void はじめは空である() throws Exception { assertThat(target.isEmpty(), is(true)); } @Test public void putしたものをgetできる() throws Exception { target.put("key", "value"); assertThat(target.get("key"), is("value")); } @Test public void 容量が取得できる() throws Exception { fillTarget(1,2,3,4); assertThat(target.size(), is(4)); } @Test public void 限界容量まで格納できる() throws Exception { target.changeCapacityTo(3); fillTarget(1,2,3); assertThat(target.size(), is(3)); fillTarget(4); assertThat(target.size(), is(3)); } @Test public void 存在しないキーにはnullを返す() throws Exception { assertThat(target.get("存在しないキー"), nullValue()); } @Test public void 限界容量を超えると一番古いものから削除される() throws Exception { target.changeCapacityTo(3); fillTarget(1,2,3,4); assertThat(target.get("1"), nullValue()); fillTarget(5); assertThat(target.get("2"), nullValue()); } @Test public void あとで容量を増やせる() throws Exception { target.changeCapacityTo(3); fillTarget(1,2,3,4); target.changeCapacityTo(5); fillTarget(5,6); assertThat(target.size(), is(5)); } @Test public void あとで容量を減らせる() throws Exception { target.changeCapacityTo(5); fillTarget(1,2,3,4,5); target.changeCapacityTo(1); assertThat(target.size(), is(1)); } @Test public void putしたら空ではない() throws Exception { fillTarget(1); assertThat(target.isEmpty(), is(false)); } @Test(expected=IllegalArgumentException.class) public void 容量0は指定できない() throws Exception { target.changeCapacityTo(0); } @Test public void あとで容量を減らしたら古いキーから削除される() throws Exception { target.changeCapacityTo(5); fillTarget(1,2,3,4,5); target.changeCapacityTo(3); assertThat(target.get("1"), nullValue()); assertThat(target.get("2"), nullValue()); } }
プロダクトコード
package tddbc; import java.util.*; public class LRUCache<K, V> { final HashMap<K, V> cache = new HashMap<K, V>(); final KeyHolder keyHolder = new KeyHolder(); int capacity = 1000; class KeyHolder { final ArrayList<K> l = new ArrayList<K>(); void touch(K key) { this.l.remove(key); this.l.add(key); } void fixCache() { while (capacity < size()) cache.remove(this.l.remove(0)); } } public Boolean isEmpty() { return this.cache.isEmpty(); } public void put(K key, V value) { this.keyHolder.touch(key); this.cache.put(key, value); this.keyHolder.fixCache(); } public V get(K key) { this.keyHolder.touch(key); return this.cache.get(key); } public int size() { return this.cache.size(); } public void changeCapacityTo(int capacity) { if (capacity < 1) throw new IllegalArgumentException("Capacity must be positive integer..."); this.capacity = capacity; this.keyHolder.fixCache(); } }
とまぁ、
ここまでで1.5時間はかかってます。そりゃ、生産性は落ちるわな。