1. Co to jest wzorzec Iterator

Wzorzec iterator to wzorzec projektowy behawioralny, który zapewnia jednolitą metodę przechodzenia elementów obiektu agregatu bez ujawniania wewnętrznej reprezentacji obiektu agregatu.

2. Charakterystyka i Zalety wzorca Iterator

Następujące są cechy i zalety wzorca iterator:

  • Może ukryć wewnętrzną strukturę obiektu kolekcji, odłączając algorytm przechodzenia od obiektu kolekcji.
  • Zapewnia ujednolicony sposób przechodzenia różnymi rodzajami obiektów kolekcji.
  • Upraszcza kod klienta, sprawiając, że jest bardziej czytelny i zwięzły.
  • Może dostarczać różne implementacje iteratorów, aby dostosować się do różnych potrzeb przechodzenia.

3. Przykłady praktycznych zastosowań wzorca Iterator

Wzorzec iteratora ma wiele praktycznych zastosowań, takich jak:

  • Przechodzenie wyników zapytania bazy danych.
  • Przechodzenie plików i folderów w systemie plików.
  • Przechodzenie elementów w kolekcji.

4. Implementacja wzorca Iterator w języku Golang

4.1 Diagram klas UML

Wzorzec Iterator w języku Golang

4.2 Wprowadzenie przykładu

Na powyższym diagramie klas UML mamy dwie główne role: Iterator i Collection.

  • Iterator definiuje interfejs do przechodzenia obiektu kolekcji, w tym metodę HasNext() do określania, czy jest kolejny element, oraz metodę Next() do pobrania następnego elementu.
  • ConcreteIterator to konkretne klasa implementacji Iteratora, implementująca metody HasNext() oraz Next().
  • Collection definiuje interfejs do tworzenia obiektów iteratora za pomocą metody CreateIterator().
  • ConcreteCollection to konkretne klasa implementacji Collection, implementująca metodę CreateIterator().

4.3 Kroki implementacji

Następnie wykonamy krok po kroku implementację wzorca Iterator w języku Golang.

4.3.1 Zdefiniuj interfejs Iteratora oraz konkretną klasę Iteratora

type Iterator interface {
    HasNext() bool
    Next() interface{}
}

type ConcreteIterator struct {
    collection *ConcreteCollection
    index      int
}

func (it *ConcreteIterator) HasNext() bool {
    if it.index < len(it.collection.items) {
        return true
    }
    return false
}

func (it *ConcreteIterator) Next() interface{} {
    if it.HasNext() {
        item := it.collection.items[it.index]
        it.index++
        return item
    }
    return nil
}

4.3.2 Zdefiniuj interfejs obiektu iterowalnego oraz konkretną klasę obiektu iterowalnego

type Collection interface {
    CreateIterator() Iterator
}

type ConcreteCollection struct {
    items []interface{}
}

func (c *ConcreteCollection) CreateIterator() Iterator {
    return &ConcreteIterator{
        collection: c,
        index:      0,
    }
}

4.3.3 Implementacja logiki generowania iteratora w klasie obiektu iterowalnego

func main() {
    collection := &ConcreteCollection{
        items: []interface{}{"Golang", "Python", "Java"},
    }

    iterator := collection.CreateIterator()
    for iterator.HasNext() {
        item := iterator.Next()
        fmt.Println(item)
    }
}

Uruchomienie powyższego kodu spowoduje wygenerowanie następującego wyniku:

Golang
Python
Java

W powyższym kodzie definiujemy klasę ConcreteCollection, która implementuje interfejs Collection, a jej metoda CreateIterator() zwraca obiekt iteratora. Używamy tego obiektu iteratora w funkcji main() do przechodzenia.

4.4 Wydłużenie kroku implementacji: Uproszczenie implementacji iteratora przy użyciu funkcji generatorowej

W Golang, możemy uprościć implementację iteratorów przy użyciu funkcji generatorowej (yield). Oto przykład użycia funkcji generatorowej:

func GenerateItems() <-chan interface{} {
    items := []interface{}{"Golang", "Python", "Java"}

    out := make(chan interface{})
    go func() {
        defer close(out)
        for _, item := range items {
            out <- item
        }
    }()
    return out
}

func main() {
    for item := range GenerateItems() {
        fmt.Println(item)
    }
}

W powyższym kodzie definiujemy funkcję GenerateItems(), która zwraca kanał tylko do odczytu (<-chan), i używamy yield do sekwencyjnego przesyłania elementów do kanału w ramach tej funkcji. W funkcji main() używamy range do przeglądania tego kanału tylko do odczytu i wyświetlamy wartości elementów.

W ten sposób udało nam się uprościć implementację iteratora przy użyciu funkcji generatorowej.