『The Art of Readable Code』のChapter15のコードをRuby化してみた

www.amazon.co.jp

現在、フィヨルドブートキャンプコミュニティで『The Art of Readable Code』の輪読会に参加しています。

日本語版と英語版どちらも開催され、日本語版の方は進みがよく、すでに輪読会は終了。

その際、最終章であるChapter15で、輪読会メンバーが「わけがわからないよ」と口々に言って半ば沈滞ムード気味になってしまったことがある。

主な原因はReadable Codeという書籍が一昔前の書籍であって、ブートキャンプメンバーが主力にしているRubyでは、殆ど当時の問題が解決されるようなアップデートが済んでいるからだと思われる。(コーダー側に依存していた問題が、言語側で解決されてしまった。)

はじめにJavaScriptJavaを使っていた自分にとっては、あるあるで頷く場面も多く、また、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}"