Goでiteratorパターンはどうする
iteratorパターンとは
簡単にだけ…
登場人物
- 要素
- コレクション(要素の集まり)
- iterator(コレクションをどう走査するかを知っているもの)
使用者側のイメージ
- なんらかのコレクションを走査したい
- コレクションからiteratorを生み出す
- 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
型に戻さないといけません。イケてない印象です。
追記
ジェネリクスを使うと解決するよとのアドバイスをいただきました。ありがとうございます!
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の良し悪しは正直今の自分には分かっていないです。(詳細に依存せず抽象に依存しろ、みたいなことは聞いたことはありますが…)
Discussion
go1.18以降のジェネリクスを使うとこういう書き方もできるようです。
ジェネリクスについて無知でした。ありがとうございます!
なるほど、示していただいた
iter
もジェネリクス使っていますが、ここをbook
などに置き換えてもIteratorインターフェースを満たすようにできるのですね。大変勉強になりましたmm