Accueil / Tutoriels / Iterator

Iterator

Publié le 17 Mars 2025
Conception

Iterator (itérateur en français) est un design pattern comportemental qui permet de parcourir les éléments d’une collection sans exposer sa structure interne. Une interface commune à tous les itérateurs est définie pour parcourir les éléments séquentiellement, sans avoir une connaissance détaillée de la structure d’une collection.

Principe

Dans la plupart des langages de programmation, les collections peuvent être implémentées de différentes manières (tableaux dynamiques, listes, dictionnaires, etc…). Une collection peut correspondre à une simple liste (successions d’éléments indexés), mais dans certains cas des structures plus complexes peuvent être utilisées, comme des arbres ou des graphes.

Pour une simple liste (par exemple un array en PHP) le code se basera sur les index pour parcourir le tableau. Si maintenant on se retrouve avec un tableau multidimensionnel, le code ne fonctionnera plus. Tout changement de structure implique des modifications du code ce qui entraîne un couplage fort et une dépendance accrue lors de l’écriture du code.

L’objectif va être d’extraire le code qui parcourt la collection et de l'encapsuler dans une classe que l’on appellera un itérateur.

Pour une même collection, on peut avoir différentes façons de parcourir les éléments (du début à la fin ou l’inverse, avec des filtres, etc…) et donc différents itérateurs. Tous les itérateurs vont implémenter une interface commune, ce qui facilite la création de nouveaux itérateurs et permet de découpler l’itération et la structure d’une collection.

Exemples d’utilisations

Parcourir une collection d’articles de blog

Avec une requête sur notre base de données, nous avons extrait une liste d’articles. Nous voulons maintenant parcourir cette liste de différentes façons. Du plus ancien au plus récent (ou dans l’autre sens), obtenir les articles dans une plage de dates spécifiques, ou appartenant à un auteur en particulier.

Pagination des résultats d’une requête API

Une requête API peut renvoyer un très grand nombre de résultats. Par soucis de performances, ces résultats sont souvent paginés. Nous voulons récupérer progressivement ces pages sans devoir appeler manuellement chaque page de résultats. Dans ce cas, l’API nous fournit des informations comme le nombre de résultats sur la page, la page actuelle, la page suivante (si elle existe).

Points d’attentions

L’ajout de code supplémentaire avec les interfaces et les classes spécifiques aux itérateurs peut rendre le code complexe sans réel intérêt. Surtout si l’objectif est de parcourir des structures simples. L’utilisation d’un itérateur peut être moins performante qu'une simple boucle sur une collection. Utilisez ce design pattern judicieusement.

Certains langages proposent des itérateurs natifs comme PHP, Python ou Javascript. Pour des soucis de performances et de simplicité, privilégiez l’utilisation de ces itérateurs au lieu de vouloir réinventer la roue.

Implémenter un itérateur

On définit une interface qui définit les méthodes pour parcourir une collection. Généralement l’interface définit la signature de deux méthodes hasNext() et next(). Cette interface est ensuite implémentée dans des classes qui définissent en détails comment parcourir les collections.

Une seconde interface qui définit la création des itérateurs est mise en place. Cette interface est commune à tous les itérateurs.

Exemple d'utilisation d’Iterator en Go

Voici un exemple d’utilisation d'Iterator avec le langage Go :

Structure d'un utilisateur


package data

type User struct {
   Name string
   Age  int
}

Interface pour l'itérateur bidirectionnel. On peut ainsi parcourir les éléments du début à la fin ou vice versa.


package iterator
 
type Iterator[T any] interface {
   Next() (*T, bool) 
   Prev() (*T, bool) 
}

Implémentation de l’itérateur. On définit UserIterator et on implémente les fonction Next() et Prev().


package iterator

import (
   "iterator/data"
)

type UserIterator struct {
   users    []data.User
   position int
}

func NewUserIterator(users []data.User) *UserIterator {
   return &UserIterator{users: users, position: -1}
}

func (it *UserIterator) Next() (*data.User, bool) {
   if it.position < len(it.users)-1 {
      it.position++
      return &it.users[it.position], true
   }
   return nil, false
}

func (it *UserIterator) Prev() (*data.User, bool) {
   if it.position > 0 {
      it.position--
      return &it.users[it.position], true
   }
   return nil, false
}

Interface pour une collection iterable.


package iteratorCollection

import "iterator"

type IterableCollection[T any] interface {
   CreateIterator() iterator.Iterator[T]
}

On implémente CreateIterator() pour nos données User.


package iteratorCollection

import (
   "iterator/data"
   "iterator"
)

type UserCollection struct {
   users []data.User
}

func NewUserCollection(users []data.User) *UserCollection {
   return &UserCollection{users: users}
}

func (c *UserCollection) CreateIterator() iterator.Iterator[data.User] {
   return iterator.NewUserIterator(c.users)
}


package main

import (
   "fmt"
   "iterator/data"
 
"iterator/iteratorCollection"
)

func main() {
   // Création d'une collection d'utilisateurs
   users := []data.User{
      {"Toto", 25},
      {"Titi", 30},
      {"Tata", 22},
   }

   // Création de la collection et récupération de l'itérateur
   userCollection := iteratorCollection.NewUserCollection(users)
   iterator := userCollection.CreateIterator()

   // Parcours en avant
   fmt.Println("Parcours en avant :")
   for {
      user, hasNext := iterator.Next()
      if !hasNext {
         break
      }

      fmt.Printf("Nom: %s, Âge: %d\n", user.Name, user.Age)
   }

   // Parcours en arrière
   fmt.Println("Parcours en arrière :")
   for {
      user, hasPrev := iterator.Prev()
      if !hasPrev {
         break
      }
      fmt.Printf("Nom: %s, Âge: %d\n", user.Name, user.Age)
   }
}

Le même exemple en PHP

PHP propose déjà une interface Iterator. Elle définit un ensemble de méthodes de base pour itérer sur une collection.

Par rapport à notre exemple, il n’existe pas de méthode prev() dans l’interface Iterator. En revanche PHP à une interface SeekableIterator qui étend Iterator et qui permet d’ajouter la fonctionnalité bidirectionnelle avec la méthode seek().


namespace Practice\DesignPatterns\iterator;

class User
{
   public string $name;
   public int  $age;

   public function __construct($name, $age)
   {
       $this->name = $name;
       $this->age  = $age;
   }
}


namespace Practice\DesignPatterns\iterator;

use SeekableIterator;

class UserCollection {
   private array $users;

   public function __construct($users) {
       $this->users = $users;
   }

   // Retourne un itérateur pour parcourir la collection
   public function getIterator(): SeekableIterator {
       return new UserIterator($this->users);
   }
}

Implémentation de l'itérateur pour User, en utilisant l'interface SeekableIterator


namespace Practice\DesignPatterns\iterator;

use OutOfBoundsException;
use SeekableIterator;

class UserIterator implements SeekableIterator
{
   private array $users;
   private int  $position;

   public function __construct($users)
   {
       $this->users    = array_values($users);
       $this->position = 0;
   }

   // Retourne l'élément actuel
   public function current()
   {
       return $this->users[$this->position];
   }

   // Retourne la clé actuelle
   public function key()
   {
       return $this->position;
   }

   // Avance à l'élément suivant
   public function next()
   {
       $this->position++;
   }

   // Revient au début de la collection
   public function rewind()
   {
       $this->position = 0;
   }

   // Vérifie si la position actuelle est valide
   public function valid()
   {
       return isset($this->users[$this->position]);
   }

   // Déplace l'itérateur à une position spécifique
   public function seek($offset)
   {
       if (!isset($this->users[$offset])) {
           throw new OutOfBoundsException("Position invalide: $offset");
       }
       $this->position = $offset;
   }

   // Retourne à l'élément précédent
   public function prev()
   {
       if ($this->position > 0) {
           $this->position--;
       }
   }
}


declare(strict_types=1);

use Practice\DesignPatterns\iterator\User;
use Practice\DesignPatterns\iterator\UserCollection;

require "./vendor/autoload.php";

$users = [
   new User("Toto", 25),
   new User("Titi", 30),
   new User("Tata", 22)
];

$userCollection = new UserCollection($users);
$iterator = $userCollection->getIterator();

// Parcours en avant
echo "Parcours en avant :\n";
$iterator->rewind();
while ($iterator->valid()) {
   $user = $iterator->current();
   echo "Nom: {$user->name}, Âge: {$user->age}\n";
   $iterator->next();
}

// Utilisation de seek()
echo "Aller directement au 2ème élément :\n";
try {
   $iterator->seek(1);
   $user = $iterator->current();
   echo "Nom: {$user->name}, Âge: {$user->age}\n";
} catch (OutOfBoundsException $e) {
   echo $e->getMessage();
}

// Parcours en arrière sécurisé
echo "Parcours en arrière :\n";
// Commence par la dernière position
$iterator->seek(count($users) - 1);
while ($iterator->valid()) {
   $user = $iterator->current();
   echo "Nom: {$user->name}, Âge: {$user->age}\n";

   // Vérification avant d'appeler prev()
   if ($iterator->key() == 0) {
       break; // Évite la boucle infinie
   }

   $iterator->prev();
}

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Iterator. Vous pouvez retrouver mon article sur les design patterns ou les autres tutoriels sur Observer, Decorator, Adapter et Factory.