リーダブルコードの第15章コード改善プロジェクトを Go で書いてみた

みんな読んでるリーダブルコードの第15章のサンプル改善プロジェクト”「分／時間カウンタ」を設計・実装する”のC++コードをGoで書き換えた。本章の内容は「分／時間カウンタ」プログラムを試案1から3段階にわたり改善していくサンプルプロジェクトで、内容に沿って書き換えていく事でコード改善プロセスを体験できる。

### お題
> ウェブサーバーの直近1分間と直近1時間の転送バイト数を把握したい。

つまり、カウンタープログラムを作るお題。

### 試案1：素朴な解決策
いわゆる最初に書きそうなコード。すごくありそう。

```go
// 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 でスライスの扱い方のフォロー記事がある。

[SliceTricks · golang/go Wiki](https://github.com/golang/go/wiki/SliceTricks)

※コードコメント省略。

```go
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` を見た時に最もシンプル。

```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)
}
```

```go: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()
}
```

```go:
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 だし標準テストは書き加えたいところ。

- [リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice) | Dustin Boswell, Trevor Foucher, 須藤 功平, 角 征典 |本 | 通販 | Amazon](http://amzn.asia/d/6dwynf5)
- [kzkick2nd/MinuteHourCounter](https://github.com/kzkick2nd/MinuteHourCounter)