LRUCache、一定時間経つと消えるところまで。
仕様変更その2まで対応。
- Clockオブジェクトを導入
- 記事にしやすいよう、モッククラスをテストないに記述してるけど、分けた方がいい。
- でも、Clock.SYSTEM_CLOCKは、interface Clock内に定義すべき。通常使うだろう、デフォルトオブジェクトがわかりやすいはず。
- 容量変更のメソッド(fixCache())と時間でクリアのメソッド(deleteDeadCache())を分けた。
- 理由は、put/getメソッド内でのcache.putとcache.getとの前後関係を同じようにするため。
- この一歩手前までは、fixCache()内に2行記述していた。そうすると、get時はget前に、put時はput後にfixCache()を呼ぶ必要があった。
- これで、重複が出来た。が、どうやって取り除こうか。
- 生存時間と容量も、KeyHolderに移動。
- これで、外側のオブジェクトはほぼMapの機能しか残っていない。
んー、でも、この方向でただしいのか?
できれば、KeyHolderとLRUCacheの相互参照状態を解消したい…。
テストコード
package tddbc; import static org.junit.Assert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNull.*; import org.junit.*; // TODO スレッドセーフ public class LRUCacheTest { LRUCache<String, String> target; MockClock clock; class MockClock implements Clock { long currentTime = 0; @Override public long currentTime() { return this.currentTime; } void set(long currentTime) { this.currentTime = currentTime; } } @Before public void setup() { target = new LRUCache<String, String>(); clock = new MockClock(); target.setClock(clock); } void fillTarget(Integer...is) { for (Integer i : is) target.put(i.toString(), i.toString()); } @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(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()); } @Test public void putした時刻が記録される() throws Exception { clock.set(1); fillTarget(1); clock.set(2); assertThat(target.keyHolder.d.get(0), is(1L)); } @Test public void getした時刻が記録される() throws Exception { clock.set(1); fillTarget(1); clock.set(2); target.get("1"); clock.set(3); assertThat(target.keyHolder.d.get(0), is(2L)); } @Test public void 一定時間経過すると削除対象になる() throws Exception { fillTarget(1); clock.set(3600000); assertThat(target.keyHolder.dead(), is(false)); clock.set(3600001); assertThat(target.keyHolder.dead(), is(true)); } @Test public void 一定時間経過すると削除される() throws Exception { fillTarget(1); clock.set(3600001); assertThat(target.get("1"), nullValue()); } }
Clockオブジェクト
package tddbc; public interface Clock { public long currentTime(); public static final Clock SYSTEM_CLOCK = new Clock() { @Override public long currentTime() { return System.currentTimeMillis(); } }; }
プロダクトコード
package tddbc; import java.util.*; public class LRUCache<K, V> { final HashMap<K, V> cache = new HashMap<K, V>(); final KeyHolder keyHolder = new KeyHolder(); class KeyHolder { final ArrayList<K> l = new ArrayList<K>(); final ArrayList<Long> d = new ArrayList<Long>(); int capacity = 1000; long survivalTime = 3600000; Clock clock = Clock.SYSTEM_CLOCK; void touch(K key) { if(this.l.contains(key)) { this.d.remove(this.l.indexOf(key)); this.l.remove(key); } this.l.add(key); this.d.add(clock.currentTime()); } void fixCache() { while (capacity < size()) kill(); } void deleteDeadCache() { while (0 < size() && dead()) kill(); } void kill() { this.d.remove(0); cache.remove(this.l.remove(0)); } boolean dead() { return this.d.get(0) + survivalTime < clock.currentTime(); } void capa(int capacity) { if (capacity < 1) throw new IllegalArgumentException("Capacity must be positive integer..."); this.capacity = capacity; this.fixCache(); } } public void setSurvivalTime(long survivalTime) { keyHolder.survivalTime = survivalTime; } public void changeCapacityTo(int capacity) { this.keyHolder.capa(capacity); } public void setClock(Clock clock) { keyHolder.clock = clock; } public void put(K key, V value) { try { this.keyHolder.deleteDeadCache(); this.keyHolder.touch(key); this.cache.put(key, value); } finally { this.keyHolder.fixCache(); } } public V get(K key) { try { this.keyHolder.deleteDeadCache(); this.keyHolder.touch(key); return this.cache.get(key); } finally { this.keyHolder.fixCache(); } } public int size() { return this.cache.size(); } }