LoginSignup
3
3

More than 5 years have passed since last update.

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

Posted at

みんな読んでるリーダブルコードの第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 でスライスの扱い方のフォロー記事がある。

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3