みんな読んでるリーダブルコードの第15章のサンプル改善プロジェクト”「分/時間カウンタ」を設計・実装する”のC++コードをGoで書き換えた。本章の内容は「分/時間カウンタ」プログラムを試案1から3段階にわたり改善していくサンプルプロジェクトで、内容に沿って書き換えていく事でコード改善プロセスを体験できる。
お題
ウェブサーバーの直近1分間と直近1時間の転送バイト数を把握したい。
つまり、カウンタープログラムを作るお題。
試案1:素朴な解決策
いわゆる最初に書きそうなコード。すごくありそう。
// Package mhcounter track the cumulative counts over the past minute and over the past hour.
// Useful, for example, to track recent bandwidth usage.
package mhcounter
import "time"
type event struct {
count int
time time.Time
}
type Events struct {
event []event
}
func (e *Events) countSince(cutoff time.Time) int {
var count int
for _, v := range e.event {
if v.time.After(cutoff) {
count += v.count
}
}
return count
}
// Add a new data point (count >= 0).
// For the next minute, MinuteCount() will be larger by +count.
// For the next hour, HourCount() will be larger by +count.
func (e *Events) Add(count int) {
e.event = append(e.event, event{count, time.Now()})
}
// MinuteCount returns the accumulated count over the past 60 seconds.
func (e *Events) MinuteCount() int {
return e.countSince(time.Now().Add(-1 * time.Minute))
}
// HourCount returns the accumulated count over the past 3600 seconds.
func (e *Events) HourCount() int {
return e.countSince(time.Now().Add(-1 * time.Hour))
}
試案2:ベルトコンベヤー設計
制限付きベルトコンベヤー構造を試案1に適用して計算量を減らした。試案3パターンの中で一番嫌なコードだと思う。
Go のスライスには push_back や pop_front 等々は無く悩んだが公式 wiki でスライスの扱い方のフォロー記事がある。
※コードコメント省略。
package mhcounter
import "time"
type event struct {
count int
time time.Time
}
type Events struct {
minuteEvents []event
hourEvents []event
minuteCount int
hourCount int
}
func (e *Events) shiftOldEvents(now time.Time) {
var minuteEvents []event
var hourEvents []event
minuteAgo := now.Add(-1 * time.Minute)
hourAgo := now.Add(-1 * time.Hour)
for _, v := range e.minuteEvents {
if v.time.Before(minuteAgo) {
e.hourEvents = append(e.hourEvents, v)
e.minuteCount -= v.count
} else {
minuteEvents = append(minuteEvents, v)
}
}
e.minuteEvents = minuteEvents
for _, v := range e.hourEvents {
if v.time.Before(hourAgo) {
e.hourCount -= v.count
} else {
hourEvents = append(hourEvents, v)
}
}
e.hourEvents = hourEvents
}
func (e *Events) Add(count int) {
var now = time.Now()
e.shiftOldEvents(now)
e.minuteEvents = append(e.minuteEvents, event{count, now})
e.minuteCount += count
e.hourCount += count
}
func (e *Events) MinuteCount() int {
e.shiftOldEvents(time.Now())
return e.minuteCount
}
func (e *Events) HourCount() int {
e.shiftOldEvents(time.Now())
return e.hourCount
}
試案3:時間バケツの設計
計算量を節約したが柔軟性を欠いてしまった試案2のアイデアを元に再設計したのが試案3。
クラスとコード量は増えたが、プログラムを使う側として mhcounter.go
を見た時に最もシンプル。
mhcounter.go
package mhcounter
import (
"time"
"../conveyor"
"../trailingBucket"
)
type Counter struct {
minuteCounts trailingBucket.Counter
hourCounts trailingBucket.Counter
}
func NewCounter() *Counter {
return &Counter{
minuteCounts: trailingBucket.Counter{
Buckets: conveyor.NewBuckets(60),
SecsPerBucket: 1,
},
hourCounts: trailingBucket.Counter{
Buckets: conveyor.NewBuckets(60),
SecsPerBucket: 60,
},
}
}
func (c *Counter) Add(count int) {
var now = time.Now()
c.minuteCounts.Add(count, now)
c.hourCounts.Add(count, now)
}
func (c *Counter) MinuteCount() int {
var now = time.Now()
return c.minuteCounts.TrailingCount(now)
}
func (c *Counter) HourCount() int {
var now = time.Now()
return c.hourCounts.TrailingCount(now)
}
trailingBucket.go
package trailingBucket
import (
"time"
"../conveyor"
)
type Counter struct {
Buckets *conveyor.QueneBuckets
SecsPerBucket int
updated time.Time
}
func (c *Counter) update(now time.Time) {
currentBucket := int(now.Unix() / int64(c.SecsPerBucket))
lastUpdateBucket := int(c.updated.Unix() / int64(c.SecsPerBucket))
c.Buckets.Shift(currentBucket - lastUpdateBucket)
c.updated = now
}
func (c *Counter) Add(count int, now time.Time) {
c.update(now)
c.Buckets.AddToBack(count)
}
func (c *Counter) TrailingCount(now time.Time) int {
c.update(now)
return c.Buckets.TotalSum()
}
package conveyor
type QueneBuckets struct {
maxItems int
quene []int
totalSum int
}
func NewBuckets(maxItems int) *QueneBuckets {
return &QueneBuckets{maxItems: maxItems}
}
func (q *QueneBuckets) Shift(shifted int) {
if shifted > q.maxItems {
q.quene = []int{}
q.totalSum = 0
return
}
for shifted > 0 {
q.quene = append(q.quene, 0)
shifted--
}
for len(q.quene) > q.maxItems {
q.totalSum -= q.quene[0]
q.quene = q.quene[1:]
}
}
func (q *QueneBuckets) AddToBack(count int) {
if len(q.quene) == 0 {
q.Shift(1)
}
q.quene[len(q.quene)-1] += count
q.totalSum += count
}
func (q *QueneBuckets) TotalSum() int {
return q.totalSum
}
本章はスキップ気味だが改善プロセスを体験できる機会はまれで、この書き換えはとても面白かった。
15章にテストコードの掲載はないが Go だし標準テストは書き加えたいところ。