『The Art of Readable Code』のChapter15のコードをRuby化してみた
現在、フィヨルドブートキャンプコミュニティで『The Art of Readable Code』の輪読会に参加しています。
日本語版と英語版どちらも開催され、日本語版の方は進みがよく、すでに輪読会は終了。
その際、最終章であるChapter15で、輪読会メンバーが「わけがわからないよ」と口々に言って半ば沈滞ムード気味になってしまったことがある。
主な原因はReadable Codeという書籍が一昔前の書籍であって、ブートキャンプメンバーが主力にしているRubyでは、殆ど当時の問題が解決されるようなアップデートが済んでいるからだと思われる。(コーダー側に依存していた問題が、言語側で解決されてしまった。)
はじめにJavaScriptやJavaを使っていた自分にとっては、あるあるで頷く場面も多く、また、Rubyがいかに書きやすいかが改めて実感できたのだけれど...
また、この章は、他の章とは違って、通して動くような長いコードを書くことになる。コードはクラスごとに解説が入っていて、前後のコードの関連性や一気見した時の全体像がわかりにくくなっている。加えて、C++(たぶん)で書かれているので、手元でサクッと動かす環境がない人が多い。
以上のことを踏まえ、コードをRuby化して全体のコードを一つに繋げて実際に動かしてみたら、自分だけでなく勉強会の他のメンバーにも大きく貢献できると思ったので、書いてみた。
結局のところ、これを共有して他のメンバーの助けになったかどうかわからないが、とりあえず「なんとなくわかったかも」くらいの反応をいただけた。
最初はGistで密かに共有したのだが、どうやらこの本はコードの引用におおらかであるっぽいので、記事として公開することにしました。
下記コードをRubyで実行するとそのまま動きます。(確認環境: ruby 2.5.0)
(実行例)minute_hour_counter.rb
というファイルに全部コピペして実行する
$ ruby minute_hour_counter.rb #=> hour: 80 minute: 40 hour: 40 minute: 0 hour: 0 minute: 0
コード
# p222 # 後で書き加えられる # # A class that keeps counts for the past N buckets of time. # class TrailingBucketCounter # def initialize(num_buckets: 60, secs_per_bucket: 0) # # Example: TrailingBucketCounter(30, 60) tracks the last 30 minute-buckets of time. # @num_buckets = num_buckets # @secs_per_bucket = secs_per_bucket # end # # def Add(count, now) # end # # ## Return the total count over the last num_buckets worth of time # def TrailingCount(now) # end # end # p223 class MinuteHourCounter # 動作確認のため時間を短縮 # NUM_BUCKET = 60 # MINUTE_BUCKET = 1 # HOUR_BUCKET = 60 NUM_BUCKET = 4 MINUTE_BUCKET = 1 # 4秒 HOUR_BUCKET = 2 # 8秒 def initialize @minute_counts = TrailingBucketCounter.new(num_buckets: NUM_BUCKET, secs_per_bucket: MINUTE_BUCKET) @hour_counts = TrailingBucketCounter.new(num_buckets: NUM_BUCKET, secs_per_bucket: HOUR_BUCKET) end def Add(count) now = Time.now.to_i @minute_counts.Add(count, now) @hour_counts.Add(count, now) end def MinuteCount now = Time.now.to_i @minute_counts.TrailingCount(now) end def HourCount now = Time.now.to_i @hour_counts.TrailingCount(now) end end # p224 # 後で書き加えられる # # A queue with a maximum number of slots, where old data "falls off" the end. # # class ConveyorQueue # ConveyorQueue(max_items) # # # Increment the value at the back of the queue. # def AddToBack(count) # end # # # Each value in the queue is shifted forward by 'num_shifted'. # # New items are initialized to 0. # # Oldest items will be removed so there are <= max_items. # def Shift(num_shifted) # end # # # Return the total value of all items currently in the queue. # def TotalSum # end # end # p224 class TrailingBucketCounter # Calculate how many buckets of time have passed and Shift() accordingly. def Update(now) current_bucket = now / @secs_per_bucket last_update_bucket = @last_update_time / @secs_per_bucket @buckets.Shift(current_bucket - last_update_bucket) @last_update_time = now end def initialize(num_buckets: 60, secs_per_bucket: 0) @buckets = ConveyorQueue.new(num_buckets) @secs_per_bucket = secs_per_bucket @last_update_time = Time.now.to_i # the last time Update() was called end def Add(count, now) Update(now) @buckets.AddToBack(count) end def TrailingCount(now) Update(now) @buckets.TotalSum end end # p226 # A queue with a maximum number of slots, where old data gets shifted off the end. class ConveyorQueue def initialize(max_items) # sum of all items in q @q = [] @max_items = max_items @total_sum = 0 end def TotalSum @total_sum end def Shift(num_shifted) # In case too many items shifted, just clear the queue. if num_shifted >= @max_items @q = [] # clear the queue @total_sum = 0 end # Push all the needed zeros. while num_shifted > 0 @q.push(0) num_shifted -= 1 end # Let all the excess items fall off. while @q.size > @max_items @total_sum -= @q.first @q.delete_at(0) end end def AddToBack(count) Shift(1) if @q.empty? # Make sure q has at least 1 item. @q[-1] += count @total_sum += count end end # 以下、雑な動作確認 minute_hour_counter = MinuteHourCounter.new # 1秒ごとに10を足す # 8回繰り返す minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) sleep 1 minute_hour_counter.Add(10) puts "hour: #{minute_hour_counter.HourCount}" puts "minute: #{minute_hour_counter.MinuteCount}" sleep 4 puts "hour: #{minute_hour_counter.HourCount}" puts "minute: #{minute_hour_counter.MinuteCount}" sleep 4 puts "hour: #{minute_hour_counter.HourCount}" puts "minute: #{minute_hour_counter.MinuteCount}"