Opis
Wzorzec projektowy Iterator należy do grupy wzorców behawioralnych. Jego głównym zadaniem jest wydzielenie kodu odpowiedzialnego za iterowanie po elementach kolekcji. Taki obiekt enkapsuluje logikę algorytmu gwarantującego dostęp do konkretnych elementów.
Rzecz jasna, takich iteratorów może być kilka, a każdy z nich związany jest tym samym interfejsem. Przy takim podejściu dołożenie nowej logiki przechowywania elementów, czy iterowania po nich jest niezwykle proste.
Problem i rozwiązanie
Kolekcje w programowaniu to standard. Wykorzystuje się je na co dzień w różnych formach. Na przykład w postaci tablicy, czy złożonego obiektu. Kolekcje muszą zapewniać możliwość dostępu do wszystkich jej elementów. O ile w prostych tablicach nie jest to skomplikowane, bo wystarczy użyć jakiekolwiek pętli, to w przypadku bardziej rozbudowanych struktur obiektowych nie jest to już tak trywialne.
Dodatkowo, zamiast uzależniać klasę od konkretnego rodzaju kolekcji, w niektórych miejscach wypadałoby wprowadzić odpowiednią abstrakcję. Tak, by nie wymuszać na kliencie konkretnego rodzaju agregacji, a w dodatku pozbawić go informacji w jaki sposób elementy są przechowywane i dzięki temu zagwarantować większą elastyczność.
Rozwiązaniem jest wzorzec Iterator, który przyda się głównie do obsługi bardziej skomplikowanych kolekcji, chociażby w połączeniu z wzorcem Kompozyt. Dla struktur o takiej samej budowie często będzie też reużywalny na poziomie całej aplikacji, czy danego modułu.
Plusy i minusy
Logika obsługi kolekcji jest odseparowana do osobnej klasy Iteratora (single responsibility), a kod jest łatwo rozszerzalny (open-closed). Zapewnia odpowiednią enkapsulację, przez co gwarantuje elastyczność i wygodę używania, ale też pozwala uniknąć błędów. Dostarcza tylko konkretne metody dostępu, tym samym blokując klientowi nieuprawniony dostęp i mając odpowiednią kontrolę. Iteratory mogą być używane w wielu kontekstach, dzięki czemu redukują zduplikowany kod.
Ten wzorzec projektowy ma dwie wady. Po pierwsze nie należy go używać do każdej iteracji. Jasne jest, że kod będzie zawsze nieco bardziej skomplikowany, niż dla zwykłej tablicy i pętli po niej. Po drugie, działając na abstrakcji zyskuje się odpowiednią uniwersalność, ale niestety traci się ukierunkowanie na konkretny rodzaj kolekcji. Może to mieć zły wpływ na kwestie wydajnościowe przy bardziej zaawansowanych obiektach.
Przykładowa implementacja w PHP
W PHP istnieje wiele wbudowanych mechanizmów ułatwiających iterację, a sam wzorzec Iterator dzięki nim jest jeszcze bardziej popularny. Jednym z nich jest interfejs Traversable
, który jest tak zwanym markerem, czyli nie wymusza implementacji żadnych metod, a jedynie nadaje typ klasie. Co więcej, interfejs ten jest dość nietypowy, bo jest to interfejs abstrakcyjny – nie może zostać rozszerzony bezpośrednio, ale przez Iterator
lub IteratorAggregate
, które po nim dziedziczą i markerami już nie są.
Obiekty typu Traversable
nadają się do iteracji. Dodatkowo sam język od wersji 7.1 posiada też typ iterable
, a kwalifikują się do niego właśnie obiekty implementujące wcześniejszy interfejs i zwykłe tablice. To wszystko przyda się do implementacji tego wzorca projektowego. Dodam tylko jeszcze, że SPL (Standard PHP Library) posiada masę gotowych iteratorów, które z powodzeniem można wykorzystać do wielu zadań. Sam wzorzec można implementować na kilka sposobów.
Poniższy przykład prezentuje proste zastosowanie wzorca Iterator w połączeniu z wzorcem Kompozyt. Sam Iterator to podstawowa kolekcja obiektów, więc nie powinno być problemów ze zrozumieniem. Głównie trzeba skupić się na klasie FileSystemElementCollection
, która jak widać implementuje wcześniej wspomniany interfejs. Przykład jest trochę mniej standardowy, bo kolekcja pod spodem opiera się o tablicę asocjacyjną. Częściej można trafić na klasyczną tablicę indeksowaną numerycznie – wtedy metody implementuje się nieco łatwiej.
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Iterator;
use Iterator;
final class FileSystemElementCollection implements Iterator
{
private array $elements = [];
public function add(FileSystemElementInterface $element): void
{
$this->elements[$element->getId()] = $element;
}
public function current(): ?FileSystemElementInterface
{
return current($this->elements) ?: null;
}
public function next(): void
{
next($this->elements);
}
public function key(): ?string
{
return key($this->elements);
}
public function valid(): bool
{
return null !== $this->key();
}
public function rewind(): void
{
reset($this->elements);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Iterator;
interface FileSystemElementInterface
{
public function getId(): string;
public function getSize(): int;
}
final class Directory implements FileSystemElementInterface
{
private string $id;
private FileSystemElementCollection $elements;
public function __construct()
{
$this->id = uniqid();
$this->elements = new FileSystemElementCollection();
}
public function getId(): string
{
return $this->id;
}
public function getSize(): int
{
$size = 0;
foreach ($this->elements as $element) {
$size += $element->getSize();
}
return $size;
}
public function addElement(FileSystemElementInterface $element): void
{
$this->elements->add($element);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Iterator;
final class File implements FileSystemElementInterface
{
private string $id;
public function __construct(
private int $size
) {
$this->id = uniqid();
}
public function getId(): string
{
return $this->id;
}
public function getSize(): int
{
return $this->size;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Iterator\Test;
use DesignPatterns\Structural\Iterator\Directory;
use DesignPatterns\Structural\Iterator\FileSystemElementCollection;
use PHPUnit\Framework\TestCase;
class FileSystemElementCollectionTest extends TestCase
{
public function testIsCurrentElementNullOfEmptyCollection(): void
{
self::assertNull((new FileSystemElementCollection())->current());
}
public function testCanGoForNextElementOfCollection(): void
{
$directoryOne = new Directory();
$directoryTwo = new Directory();
$collection = new FileSystemElementCollection();
$collection->add($directoryOne);
$collection->add($directoryTwo);
$collection->next();
self::assertSame($directoryTwo, $collection->current());
}
public function testIsCurrentKeyElementNullOfEmptyCollection(): void
{
self::assertNull((new FileSystemElementCollection())->key());
}
public function testIsCurrentPositionOfEmptyCollectionInvalid(): void
{
self::assertFalse((new FileSystemElementCollection())->valid());
}
public function testIsCurrentPositionOfFullCollectionValid(): void
{
$collection = new FileSystemElementCollection();
$collection->add(new Directory());
self::assertTrue($collection->valid());
}
public function testCanRewindEmptyCollection(): void
{
$collection = new FileSystemElementCollection();
$collection->rewind();
self::assertFalse($collection->valid());
}
public function testCanRewindFullCollection(): void
{
$directoryOne = new Directory();
$directoryTwo = new Directory();
$collection = new FileSystemElementCollection();
$collection->add($directoryOne);
$collection->add($directoryTwo);
$collection->next();
$collection->rewind();
self::assertSame($directoryOne, $collection->current());
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Iterator\Test;
use DesignPatterns\Structural\Iterator\Directory;
use DesignPatterns\Structural\Iterator\File;
use PHPUnit\Framework\TestCase;
final class DirectoryTest extends TestCase
{
public function testCanGetSizeOfEmptyDirectory(): void
{
self::assertSame(0, (new Directory())->getSize());
}
public function testCanGetSizeOfDirectoryThatContainsOnlyFiles(): void
{
$directory = new Directory();
$directory->addElement(new File(12));
$directory->addElement(new File(8));
self::assertSame(20, $directory->getSize());
}
public function testCanGetSizeOfDirectoryThatContainsDirectoriesAndFiles(): void
{
$firstChildDirectory = new Directory();
$secondChildDirectory = new Directory();
$parentDirectory = new Directory();
$firstChildDirectory->addElement(new File(12));
$secondChildDirectory->addElement(new File(12));
$secondChildDirectory->addElement(new File(24));
$secondChildDirectory->addElement(new File(12));
$parentDirectory->addElement(new File(16));
$parentDirectory->addElement(new File(24));
$parentDirectory->addElement($firstChildDirectory);
$parentDirectory->addElement($secondChildDirectory);
self::assertSame(100, $parentDirectory->getSize());
}
}
Iterator – podsumowanie
Wzorzec projektowy Iterator to opcja, której na pewno warto się przyjrzeć. Chociażby dlatego, że prędzej, czy później pewnie traficie na niego w kodzie. A już na pewno na przypadek, w którym jego użycie mogłoby znacznie uprościć kod. Kolekcje są lepszą wersją tablicy, gdyż są obiektową reprezentacją, a to daje szersze możliwości. Tablica ma sporo wad, jak chociażby brak wymuszania typu w PHP, co skutkuje że przyjmie wszystko, co się wrzuci. Iterowanie po kolekcjach jest więc codziennością. Te najprostsze jak obiektowa reprezentacja tablicy to żadne wyzwanie (też przydatne, bo takie kolekcje również się tworzy – chociażby ArrayCollection
w Doctrine). Czasem jednak trafi się bardziej skomplikowana struktura i właśnie tam ten wzorzec projektowy będzie pasował najbardziej.
Odpowiedz