Skip to content

第 11 章: 不変データとパイプライン処理

11.1 不変データ設計の確認

Go には finalfreeze のようなキーワードがありませんが、非公開フィールド + コンストラクタ + 防御的コピーで不変性を実現できます。

FizzBuzzValue の不変設計

第 8 章で作成した FizzBuzzValue は既に不変です:

type FizzBuzzValue struct {
    number int    // 非公開: 外部から変更不可
    value  string // 非公開: 外部から変更不可
}
  • setter メソッドがない
  • フィールドが非公開(小文字)
  • 値レシーバを使用(ポインタでないため元の構造体は変更されない)

テスト: 不変性を確認する

func TestFizzBuzzValue_不変性を確認する(t *testing.T) {
    v1 := NewFizzBuzzValue(3, "Fizz")
    // v1.number = 5  // コンパイルエラー: 非公開フィールドにアクセスできない
    // v1 の値は変更できない
    v2 := v1 // コピーが作成される(値型)
    if !v1.Equal(v2) {
        t.Fatal("v1 and v2 should be equal")
    }
}

FizzBuzzList の不変設計

type FizzBuzzList struct {
    value []FizzBuzzValue // 非公開
}
  • NewFizzBuzzList でスライスをコピーして保持
  • Value() で防御的コピーを返す
  • FilterMap新しいスライス/リストを返す(元のリストは変更しない)

テスト: Filter が元のリストを変更しないことを確認する

func TestFizzBuzzList_Filterは元のリストを変更しない(t *testing.T) {
    fbt := FizzBuzzType01{}
    cmd := NewFizzBuzzListCommand(fbt, 15)
    original := cmd.Execute().(*FizzBuzzList)
    originalCount := original.Count()

    isFizz := MakeValuePredicate("Fizz")
    _ = original.Filter(isFizz)

    if original.Count() != originalCount {
        t.Fatal("original list should not be modified")
    }
}

11.2 パイプラインメソッド

FizzBuzzList にパイプライン処理のためのメソッドを追加します。

テスト: GroupByValue — 値でグルーピングする

func TestFizzBuzzList_GroupByValue_値でグルーピングする(t *testing.T) {
    fbt := FizzBuzzType01{}
    cmd := NewFizzBuzzListCommand(fbt, 15)
    list := cmd.Execute().(*FizzBuzzList)

    grouped := list.GroupByValue()

    if _, ok := grouped["Fizz"]; !ok {
        t.Fatal("grouped should contain 'Fizz' key")
    }
    if _, ok := grouped["Buzz"]; !ok {
        t.Fatal("grouped should contain 'Buzz' key")
    }
    if _, ok := grouped["FizzBuzz"]; !ok {
        t.Fatal("grouped should contain 'FizzBuzz' key")
    }
}
実装コード: GroupByValue
// GroupByValue は値でグルーピングした map を返します。
func (l *FizzBuzzList) GroupByValue() map[string][]FizzBuzzValue {
    result := make(map[string][]FizzBuzzValue)
    for _, v := range l.value {
        result[v.Value()] = append(result[v.Value()], v)
    }
    return result
}

テスト: CountByValue — 値ごとの出現回数を数える

func TestFizzBuzzList_CountByValue_値ごとの出現回数を数える(t *testing.T) {
    fbt := FizzBuzzType01{}
    cmd := NewFizzBuzzListCommand(fbt, 15)
    list := cmd.Execute().(*FizzBuzzList)

    counts := list.CountByValue()

    if counts["FizzBuzz"] != 1 {
        t.Fatalf("FizzBuzz count = %d, want 1", counts["FizzBuzz"])
    }
}
実装コード: CountByValue
// CountByValue は値ごとの出現回数を返します。
func (l *FizzBuzzList) CountByValue() map[string]int {
    result := make(map[string]int)
    for _, v := range l.value {
        result[v.Value()]++
    }
    return result
}

テスト: Take — 先頭 N 件を取得する

func TestFizzBuzzList_Take_先頭N件を取得する(t *testing.T) {
    fbt := FizzBuzzType01{}
    cmd := NewFizzBuzzListCommand(fbt, 15)
    list := cmd.Execute().(*FizzBuzzList)

    taken := list.Take(5)

    if taken.Count() != 5 {
        t.Fatalf("Take(5).Count() = %d, want 5", taken.Count())
    }
}
実装コード: Take
// Take は先頭から n 件の要素を含む新しいリストを返します。
func (l *FizzBuzzList) Take(n int) *FizzBuzzList {
    if n > len(l.value) {
        n = len(l.value)
    }
    result := make([]FizzBuzzValue, n)
    copy(result, l.value[:n])
    return &FizzBuzzList{value: result}
}

テスト: Join — 要素を文字列で結合する

func TestFizzBuzzList_Join_要素を文字列で結合する(t *testing.T) {
    values := []FizzBuzzValue{
        NewFizzBuzzValue(1, "1"),
        NewFizzBuzzValue(2, "2"),
        NewFizzBuzzValue(3, "Fizz"),
    }
    list := NewFizzBuzzList(values)

    got := list.Join(", ")

    if got != "1, 2, Fizz" {
        t.Fatalf("Join(', ') = %q, want %q", got, "1, 2, Fizz")
    }
}
実装コード: Join
// Join は各要素の文字列表現を区切り文字で結合した文字列を返します。
func (l *FizzBuzzList) Join(sep string) string {
    strs := make([]string, len(l.value))
    for i, v := range l.value {
        strs[i] = v.String()
    }
    return strings.Join(strs, sep)
}

11.3 メソッドチェーンによるパイプライン

Filter と Take は *FizzBuzzList を返すため、メソッドチェーンでパイプラインを構築できます。

テスト: メソッドチェーンで Fizz を 3 件取得して結合する

func TestFizzBuzzList_メソッドチェーンでパイプラインを構築する(t *testing.T) {
    fbt := FizzBuzzType01{}
    cmd := NewFizzBuzzListCommand(fbt, 100)
    list := cmd.Execute().(*FizzBuzzList)

    result := list.
        Filter(MakeValuePredicate("Fizz")).
        Take(3).
        Join(", ")

    if result != "Fizz, Fizz, Fizz" {
        t.Fatalf("pipeline result = %q, want %q", result, "Fizz, Fizz, Fizz")
    }
}

Reduce パターン

Go には組み込みの reduce がありませんが、関数として実装できます。

テスト: Reduce で合計を計算する

func TestFizzBuzzList_Reduce_数値の合計を計算する(t *testing.T) {
    values := []FizzBuzzValue{
        NewFizzBuzzValue(1, "1"),
        NewFizzBuzzValue(2, "2"),
        NewFizzBuzzValue(3, "Fizz"),
    }
    list := NewFizzBuzzList(values)

    sum := list.Reduce(0, func(acc int, v FizzBuzzValue) int {
        return acc + v.Number()
    })

    if sum != 6 {
        t.Fatalf("Reduce sum = %d, want 6", sum)
    }
}
実装コード: Reduce
// Reducer は累積値と要素を受け取り新しい累積値を返す関数型です。
type Reducer func(acc int, v FizzBuzzValue) int

// Reduce は初期値から始めて各要素に関数を適用し、単一の値に畳み込みます。
func (l *FizzBuzzList) Reduce(initial int, fn Reducer) int {
    acc := initial
    for _, v := range l.value {
        acc = fn(acc, v)
    }
    return acc
}

11.4 各言語のパイプライン比較

機能 Go Java Ruby TypeScript
パイプライン構文 メソッドチェーン Stream API then / メソッドチェーン メソッドチェーン
グルーピング 手動 map 構築 Collectors.groupingBy group_by reduce
結合 strings.Join Collectors.joining join Array.join
畳み込み 手動 Reduce Stream.reduce reduce / inject Array.reduce
遅延評価 なし(即時評価) Stream(遅延) Lazy なし

Go はメソッドチェーンでパイプラインを構築しますが、Java の Stream API のような遅延評価はありません。すべての操作は即時評価されます。

11.5 まとめ

本章では以下を学びました:

  • 不変データ設計: 非公開フィールド + 防御的コピーで不変性を実現
  • パイプラインメソッド: GroupByValue、CountByValue、Take、Join
  • メソッドチェーン: Filter → Take → Join のパイプライン構築
  • Reduce パターン: 畳み込みの手動実装

Go のパイプラインは Java の Stream API ほど洗練されていませんが、メソッドチェーンを使って同等の処理を記述できます。不変設計は言語機能に頼らず、設計パターンで実現します。