ひとりTDDBC（LRUCache）仕様変更その2

# 前回までのあらすじ

- ひとりTDDBCをやります
- お題は[t_wadaさんのTDDBCお題スライド P.5](http://www.slideshare.net/t_wada/tddbc-exercise)
- Githubの[このリポジトリ](https://github.com/haazime/tddbc_lru_cache)で進めます
- [Vol.1](http://qiita.com/haazime/items/2c61cb10eea3c6d97e4d)
- [Vol.2](http://qiita.com/haazime/items/769e86bf81a9709f9c6a)
- [Vol.3](http://qiita.com/haazime/items/85d52a3755f7ebca0a65)

# 仕様変更その2

一定時間経ったデータは消えて欲しいとのことです。

```rb
cache = LRUCache.new(2, timeout: 60) # タイムアウトを60秒に設定
cache[:a] = 'alpha'
# 59秒以内にデータを取り出す
cache[:a] # => 'alpha'
# 60秒以上経った後でデータを取り出す
cache[:a] # => nil
```

まず`LRUCache`インスタンスを作る時に、`timeout`オプションで秒数を渡せるようにします。

- データを入れてから、まだ`timeout`秒経っていない場合は取り出せる
- データを入れてから、`timeout`秒以上経った場合は取り出せない（`nil`を返す）

というふうにしてみます。

# レビュー

[仕様変更その2に対応したコード](https://github.com/haazime/tddbc_lru_cache/tree/feature/timeout)

## テスト

今回は時間が絡む動作をテストしないといけないので
テストコードでは時間をコントロールする必要があります。

例えば、60秒以上経ったデータが消えていることをテストしたい場合
簡単に書くと、

```rb
cache[:a] = 'alpha'
sleep(60)
expect(cache[:a]).to be_nil

cache[:z] = 'zulu'
expect(cache[:z]).to eq('zulu')
```

こういうふうに書けます。

が、このテストを実行すると必ず60秒かかってしまいます。
同じようなテストが複数あると、どんどんテストの実行が遅くなります。

TDDでは、`Red => Green => Refactor`
のサイクルをテンポよく回すことがとても重要で
テストはできる限り速く実行したいので、この方法はちょっと使えません。

反対に、タイムアウトを0.1秒に設定し、

```rb
cache[:a] = 'alpha'
sleep(0.1)
expect(cache[:a]).to be_nil

cache[:z] = 'zulu'
expect(cache[:z]).to eq('zulu')
```

こういうテストを書いたとします。

テストを速く実行する点では問題ありませんが
`:z`のデータを扱うところが微妙です。

```rb
cache[:z] = 'zulu' # 0.1秒以内に処理が完了すれば...
expect(cache[:z]).to eq('zulu') # => 'zulu' OK!

cache[:z] = 'zulu' # 何らかの理由で0.1秒以内に処理が完了しないと...
expect(cache[:z]).to eq('zulu') # => nil NG!
```

このように、テストを実行する度に結果が変わってしまう可能性があります。
プロダクションコードとテストが同じであれば、テスト結果も常に同じでないといけないので
この方法も使えません。

### という訳で

```rb
RSpec.describe LRUCache, 'タイムアウトを60秒に設定' do
  let(:cache) { LRUCache.new(2, timeout: 60) }

  it '60秒以上経ったデータは消える' do
    stub_now(Time.now - 60) do # 60秒前にデータを入れたことにする
      cache[:a] = 'alpha'
    end
    cache[:b] = 'bravo'
    expect(cache[:a]).to be_nil
    expect(cache[:b]).to eq('bravo')
  end

  it '60秒経っていないデータは残る' do
    stub_now(Time.now - 59) do # 60秒経っていないことを保証する
      cache[:x] = 'x-ray'
      cache[:y] = 'yankee'
      cache[:x]
      cache[:z] = 'zulu'
      expect(cache[:x]).to eq('x-ray')
      expect(cache[:y]).to be_nil
      expect(cache[:z]).to eq('zulu')
    end
  end
end
```

テストはこのようにしました。

時間が絡むテストをしたい時は[timecop](https://github.com/travisjeffery/timecop)などを使って、時間を制御すると便利なのですが
今回はそんなに凝ったことをしないので、ヘルパーメソッドを自作しました。

```rb
def stub_now(fake_time)
  allow(Time).to receive(:now) { fake_time }
  yield
  allow(Time).to receive(:now).and_call_original
end
```

`stub_now`メソッドに渡したブロック内で、`Time.now`を呼び出すと
必ず`fake_time`を返すようにし
ブロックを抜けると`Time.now`は本来の時間を返すようになっています。

（このへんのスタブ周りもRSpec3.0で大きく変わりましたね）

# プロダクションコード

```rb
class History
  class Line < Struct.new(:key)

    def initialize(key)
      super(key)
      @created_at = Time.now
    end

    def expired?(expire_at)
      @created_at <= expire_at
    end
  end
end
```

前のバージョンでは、`History`が保持するキーはキーそのものでしたが
今回はタイムアウトを考慮する必要があるので、キーを入れておく`Line`クラスを作り
キーとなるオブジェクトと一緒に、`Line`オブジェクトが生成された日時も記録できるようにしました。

```rb
class Line < Struct.new(:key, :created_at)
```

としていないのは、
`Line#==`メソッドで`key`だけを同値性の基準としたいからです。

```rb
class History

  def initialize(timeout)
    @lines = []
    @timeout = timeout
  end

  def record(key)
    line = Line.new(key)
    @lines.tap {|x| x.delete(line) } << line
    line.key
  end

  def keys(capacity)
    @lines.slice!(0, @lines.size - capacity)
    timeout
    @lines.map {|l| l.key }
  end

private

  def timeout
    return unless @timeout
    @lines.reject! {|l| l.expired?(Time.now - @timeout) }
  end
end
```

`History`クラスの方は、キャッシュすべきキーを取得する`keys`メソッドの中で
`@timeout`秒以上前に作成された`Line`（キー）も削除するようにしています。