"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にも上げてみました。初github。http://github.com/htada/tddbc-lrucache/