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時間はかかってます。そりゃ、生産性は落ちるわな。