"TDD" Boot Camp 参加。と、宿題をやった。

2009-12-19。「これは伝説のイベント」と各所で評判のTDDBCに参加してきました。TDD本を読んだりxUnitの読書会に参加したりしていますが、なかなかリズムというものは体験できるものではありません。そして何より、30組以上がいっせいにペアプロやるってすごいです。


スタッフ・運営の皆様、スポンサーの皆様、かつてないすばらしいイベントをありがとうございました。


当日どんな様子だったかは、Lasseさんの写真を見ていただくとよいかと思います。
動画も...!

ペアプロの感想

実は、というか何というか、当日までペアプロにちょっとびびっていました。
日常のお仕事でそれほどプログラムを書く訳ではないので、自分の実力に不安があったりもしました。
しかし、です。体験してみて、これは楽しいものだということがとても良くわかりました。実力や経験を越えて、あれこれしゃべりながらプログラムする楽しさを味わえます。もちろん、とても勉強になります。すごく勉強になります。テストを書く前に実装に手を出してしまった時、温かく(人によっては)叱ってくれます。自分のコードに対する性癖も丸わかりです。

  • 前半でペアさせていただいたid:a-hisameさん(「あ”、今1つバグに気づいた」のバグが気になるところ。同じキーでputとかその辺かな?)
  • 後半でペアさせていただいたid:hikki-515さん
  • 反省会でペアさせていただいたmitimさん

ありがとうございました。ペア中、変なこと言ってたらごめんなさい。


ちょっと主旨からは外れてしまうかもしれないのですが、自分の選んだ言語以外のコードを見るのも楽しかったです。休み時間に、全言語のテーブルを回らせてもらいました。

TDDの感想

仕様と不安をテストにする大事さ、くるくる回す軽快さ、気持ちよさを実感することができました。

  • テストを先に書くことで脇道にそれない。グリーンへ集中。
  • リファクタをテストが守ってくれる。冒険も可能。
  • 不安の顕在化。テストで解消。健康なコードへ。


どうも自分は心配性らしく、ペアプロ中にテストを実行し過ぎなのかも?
TDD実践者の回しっぷり(言ってしまえば、もがく様)を見ることができると、より良かったなと思います。

持ち帰って、宿題

イベントの数日後、反省を兼ねてmitimさんと復習ペアプロをさせていただきました(仕様変更の2まで)。
Ruby、初Rspec、初gitです。さらしてしまいます(mitimさん、良いですよね???)。


イベント当日は、「ハッシュ」・「キーの配列」・「登録時間の配列」の3つをフィールドに持つ実装が多かったので、別の方向でやってみました。正直、遅いです。(一応、メモリ効率重視ということで)。

lru_cache_spec.rb

# -*- coding: utf-8 -*-
require 'lru_cache'

describe LruCache do
  describe "初期化に関するテスト" do
    it "サイズを渡したらそのサイズのキャッシュができること" do
      targ = LruCache.new(10)
      targ.limit.should == 10
    end

    it "サイズにマイナス値を渡した場合、例外が発生すること" do
      lambda{ LruCache.new(-1) }.should raise_error(ArgumentError)
    end

    it "サイズにnilを渡した場合、例外が発生すること" do
      lambda{ LruCache.new(nil) }.should raise_error(ArgumentError)
    end

    it "サイズに数値以外を渡した場合、例外が発生すること" do
      lambda{ LruCache.new("a") }.should raise_error(ArgumentError)
    end
  end

  describe "値の出し入れに関するテスト" do
    before :each do
      @targ = LruCache.new(3)
    end
    
    it "入れたものが同じキーで取りだせること" do
      @targ.put("a", "A")
      @targ.get("a").should == "A"
      @targ.put("b", "B")
      @targ.get("b").should == "B"
    end

    it "キャッシュの中にないキーを取り出すとnilが返ること" do
      fill(@targ, "a", "b", "c")
      @targ.get("d").should be_nil
    end

    it "キャッシュがサイズを越えない場合、キャッシュの中で最も古いキーが取得できること" do
      fill(@targ, "a", "b", "c")
      @targ.eldest_key.should == "a"
    end

    it "キャッシュが空の場合、最も古いキーとしてnilが返ること" do
      @targ.eldest_key.should be_nil
    end

    it "キャッシュがサイズを越えた場合、越えた分の値が消えていること" do
      fill(@targ, "a", "b", "c", "d")
      @targ.get("a").should be_nil
      @targ.eldest_key.should == "b"
    end

    it "現在キャッシュされている値の個数が取得できること" do
      @targ.size.should == 0
      fill(@targ, "a", "b")
      @targ.size.should == 2
    end
    
    it "同じキーを渡した場合、上書きされること" do
      fill(@targ, "a", "b")
      @targ.size.should == 2
      @targ.put("a", "x")
      @targ.size.should == 2
      @targ.get("a").should == "x"
    end

    it "最も古いキーをgetすると次に古いキーが最も古いキーとして取得できること" do
      fill(@targ, "a", "b", "c")
      @targ.eldest_key.should == "a"
      @targ.get("a")
      @targ.eldest_key.should == "b"
    end
  end

  describe "キャッシュサイズ変更に関するテスト" do
    before :each do
      @targ = LruCache.new(3)
      fill(@targ, "a", "b", "c")
    end

    it "新しいキャッシュサイズにマイナス値を渡した場合、例外が発生すること" do
      lambda{ @targ.resize(-1) }.should raise_error(ArgumentError)
    end

    it "新しいキャッシュサイズにnilを渡した場合、例外が発生すること" do
      lambda{ @targ.resize(nil) }.should raise_error(ArgumentError)
    end

    it "新しいキャッシュサイズに数値以外を渡した場合、例外が発生すること" do
      lambda{ @targ.resize("a") }.should raise_error(ArgumentError)
    end

    it "キャッシュサイズが変更できること" do
      @targ.limit.should == 3
      @targ.resize(100)
      @targ.limit.should == 100
    end

    it "キャッシュサイズを増やした場合、キャッシュの内容が変わらないこと" do
      @targ.resize(4)
      @targ.size.should == 3
      should_have(@targ, "a", "b", "c")
    end

    it "キャッシュサイズを減らした場合、リミットを越えたキャッシュが消えること" do
      @targ.resize(2)
      @targ.size.should == 2
      should_not_have(@targ, "a")
      should_have(@targ, "b", "c")
    end

    it "キャッシュが空の場合に、キャッシュサイズを変更してもエラーが起こらないこと" do
      @targ = LruCache.new(3)
      lambda{ @targ.resize(1); @targ.resize(1000); }.should_not raise_error
    end
  end
  
  describe "キャッシュの保持期間に関するテスト" do
    before :each do
      # 保存期間に10秒を設定する
      @targ = LruCache.new(4, 10)
      @filled_time = now
      fill(@targ, "a", "b", "c")
    end

    it "キャッシュが登録された時間が取得できること" do
      @targ.birthtime_of("a").should == @filled_time
    end

    it "保持期間を過ぎたキャッシュが消えること" do
      should_have(@targ, "a")
      set_forward(9)
      should_have(@targ, "a")
      set_forward(1)
      should_not_have(@targ, "a")
    end

    it "保持期間を過ぎていないキャッシュが消えないこと" do
      set_forward(9)
      @targ.put("d", "D")
      set_forward(1)
      should_not_have(@targ, "a", "b", "c")
      should_have(@targ, "d")
      set_forward(9)
      should_not_have(@targ, "d")
    end
  end
end

def fill(targ, *keys)
  keys.each do |v|
    targ.put(v, v)
  end
end

def should_have(targ, *keys)
  keys.each do |v|
    targ.get(v).should_not be_nil
  end
end

def should_not_have(targ, *keys)
  keys.each do |v|
    targ.get(v).should be_nil
  end
end

def now
  set_forward(0)
end

def set_forward(second)
  time = Time.now + second
  Time.stub!(:now).and_return(time)
  return time
end

lru_cache.rb

class LruCache
  attr_reader :limit

  def initialize(size, lifespan = 10)
    raise ArgumentError.new unless valid_size?(size)
    @limit = size
    @cache = []
    @lifespan = lifespan
  end

  def put(key, value)
    remove_cache(key) if pick_out(key) != nil
    @cache << CacheValue.new(key, value)
    if @cache.size > @limit then
      @cache.shift
    end
  end

  def get(key)
    ret = pick_out(key)
    return ret == nil ? nil : ret.value
  end

  def size
    return @cache.size
  end

  def resize(size)
    raise ArgumentError.new unless valid_size?(size)
    (@limit - size).times do
      @cache.shift
    end
    @limit = size
  end

  def valid_size?(size)
    return size != nil && size > 0
  end
  
  def eldest_key
    return nil if @cache.size <= 0
    return @cache[0].key
  end

  def birthtime_of(key)
    ret = pick_out(key)
    return ret == nil ? nil : ret.birthtime
  end

  private

  def pick_out(key)
    remove_dead_caches
    @cache.each do |v|
      if v.key == key then
        rotate(v)
        return v
      end
    end
    return nil
  end
  
  def rotate(value)
    remove_cache(value.key)
    @cache << value
  end

  def remove_cache(key)
    @cache.each do |v|
      @cache.delete(v) if v.key == key
    end
  end

  def remove_dead_caches
    @cache.each do |v|
      lifetime = Time.now - v.birthtime
      remove_cache(v.key) if lifetime >= @lifespan
    end
  end
end

class CacheValue
  attr_reader :key, :value, :birthtime
  
  def initialize(key, value)
    @key = key
    @value = value
    @birthtime = Time.now
  end
end

githubにも上げてみました。初githubhttp://github.com/htada/tddbc-lrucache/