📚

Goでiteratorパターンはどうする

2023/06/24に公開
2

iteratorパターンとは

簡単にだけ…

登場人物

  • 要素
  • コレクション(要素の集まり)
  • iterator(コレクションをどう走査するかを知っているもの)

使用者側のイメージ

  1. なんらかのコレクションを走査したい
  2. コレクションからiteratorを生み出す
  3. iteratorに従って走査する

こんな感じでiteratorを利用することで実際の走査の詳細はiterator内に隠蔽されます。
メリットとしては、走査の順番が複雑な場合毎回そのアルゴリズムを書かなくてすみます。
またそのアルゴリズムに変更があった場合には各所に点在しているそのアルゴリズムを全て修正する必要がなくなり、iteratorを修正すればokです。

実装

実装最中に抽象度で何パターンか考えられたのでそれぞれ書いていきます。

1. interface{}でなんでも屋のiterator

次のようにiteratorを定義します。

type Iterator interface {
    // 次の要素が存在するか
	HasNext() bool
    // 現在の要素を返し、次の要素をセットする
	Next() interface{}
}

Next()の返り値がinterface{}でありどんな要素でも受け付けるようになっているため、抽象度が高いです。

bookを走査するbooksIteratorを実装してみます。

type BooksIterator struct {
	books []*Book
	index int
}

// 次の要素があるかの判定
func (b *BooksIterator) HasNext() bool {
	return b.index < len(b.books)
}

// 現在の要素を返し、次の要素のindexを持つ
func (b *BooksIterator) Next() interface{} {
	if b.HasNext() {
		defer func() {
			b.index++
		}()
		return b.books[b.index]
	}
	return nil
}

type Book struct {
	name string
}
func NewBook(name string) *Book {
	return &Book{name: name}
}
func (b Book) GetName() string {
	return b.name
}

// Bookのコレクション
type Books struct {
	bookList []*Book
}

func NewBooks() *Books {
	bookList := make([]*Book, 0)
	return &Books{
		bookList: bookList,
	}
}

func (b *Books) Append(book *Book) {
	b.bookList = append(b.bookList, book)
}

func (b *Books) CreateIterator() Iterator {
	return &BooksIterator{
		books: b.bookList,
		index: 0,
	}
}

これをmain関数で走査してみるとこうなります。

func main() {
	book1 := NewBook("ボボボーボ・ボーボボ")
	book2 := NewBook("バババーバ・バーババ")
	book3 := NewBook("ビビビービ・ビービビ")

	books := NewBooks()
	books.Append(book1)
	books.Append(book2)
	books.Append(book3)
	
	it := books.CreateIterator()
	for it.HasNext() {
		fmt.Println(it.Next().(Book).GetName())
	}
}

it.Next()interface{}を返す都合上、要素がBook型に戻さないといけません。イケてない印象です。

追記

ジェネリクスを使うと解決するよとのアドバイスをいただきました。ありがとうございます!
https://zenn.dev/link/comments/1a90553406370e

2. 要素book専用のiteratorとする

主な変更点は以下です。Book型の専用iteratorとしました。他にも返り値を変えたことによる修正がありますが、そこは割愛します。

type IBooksIterator interface {
    // 次の要素が存在するか
	HasNext() bool
	// 現在の要素を返し、次の要素をセットする
	Next() *Book
}

すると、main関数ではBook型が返ってくることが分かっているのでキャストの必要はなくなります。

func main() {
	book1 := NewBook("ボボボーボ・ボーボボ")
	book2 := NewBook("バババーバ・バーババ")
	book3 := NewBook("ビビビービ・ビービビ")

	books := NewBooks()
	books.Append(book1)
	books.Append(book2)
	books.Append(book3)
	
	it := books.CreateIterator()
	for it.HasNext() {
		fmt.Println(it.Next().GetName()) // ここが簡潔
	}
}

こっちの方がイケてます。
しかし、この程度の抽象化だったらinterface定義は過剰のような気がします。

3. iteratorのinterfaceをなくす

今まで抽象のiteratorを返していたCreateIterator()で具象iteratorを返します。

func (b *Books) CreateIterator() *BooksIterator {
	return &BooksIterator{
		books: b.bookList,
		index: 0,
	}
}

自分の考え

1はどんな要素でも受け入れられる分iteratorとして抽象度は高いですが、interface{}型を本来の型に戻す必要があります。ここがネックな感じがしていて、実際に実装するなら2か3かなと思います。
2,3の良し悪しは正直今の自分には分かっていないです。(詳細に依存せず抽象に依存しろ、みたいなことは聞いたことはありますが…)

GitHubで編集を提案

Discussion

NoboNoboNoboNobo

go1.18以降のジェネリクスを使うとこういう書き方もできるようです。

https://go.dev/play/p/zbu7DSN9nft

package main

import "fmt"

type Iterator[T any] interface {
	HasNext() bool
	Next() T
}

type iter[T any] struct {
	slice []T
	index int
}

func Iter[T any](s []T) Iterator[T] {
	return &iter[T]{slice: s}
}

func (s *iter[T]) HasNext() bool {
	return len(s.slice) > s.index
}

func (s *iter[T]) Next() T {
	v := s.slice[s.index]
	s.index++
	return v
}

func Print[T any](s Iterator[T]) {
	for s.HasNext() {
		fmt.Println(s.Next())
	}
}

func main() {
	si := []int{1, 2, 3}
	Print(Iter(si))
	ss := []string{"foo", "bar"}
	Print(Iter(ss))
}
isseiiisseii

ジェネリクスについて無知でした。ありがとうございます!
なるほど、示していただいたiterもジェネリクス使っていますが、ここをbookなどに置き換えてもIteratorインターフェースを満たすようにできるのですね。大変勉強になりましたmm

https://go.dev/play/p/49_eDLrupz4