Като кажем Итератор какво всъщност имаме предвид?
Това е обект, имплементиращ IteratorInterface, който интерфейс декларира какви методи да има даденият Итератор. Те разбира се трябва да са едни и същи откъм signature, но различни откъм имплементация.
И идеята е на този Итератор обект e да можем да му подаваме различни даннови структури, с цел – те да бъдат изциклени по унифициран начин.
И поради това, че отделните даннови структури могат да се итерират по различен начин, не е лоша идея всяка от тях да си върви и със свой Итератор. Примерно, ако имаме клас, който представлява дадена даннова структура, може с интерфейс да го задължим да има и setter, който да му задава Итератор…
Идеята на този design pattern е да имаме свой и също така стандартизиран начин да итерираме дадени data структури като масиви (едномерни или двумерни), обекти и т.н…
Ако например имаме едномерен масив, многомерен масив и масив с обекти, за да ги изциклим трябва да имаме три отделни foreach цикъла.
Ако имаме и трети вид структура – четири отделни foreach цикъла… и т.н…
Отделно, трябва да знаем и структурата на отделните data structures…
Един начин да вкараме нещата в някакъв общ стандарт за да можем да изцикляме по един и същ начин е разбира се да изберем едната като меродавна, и да препишем останалите да се равняват по нея.
Другият вариант е точно идеята на Iterator design pattern.
Да имаме обект (Iterator), на когото подаваме структурата, която ще циклим.
И понеже можем да имаме повече различни итератори – ще имаме интерфейс (IteratorInterface), и всеки Iterator клас ще го имплементира, като ще има методи като например: hasNextElement(), getNextElement() и т.н…
Полиморфизъм.
Демек, итераторът е длъжен да може и да връща един по един елементите на циклената data структура, и да следи индексът (броячът) на текущият връщан елемент.
Ето един прост пример:
Ще създаваме N на брой обекта Book и ще ги пълним например в масив.
$books = new BookList();
$books->addBook(new Book('Voina i Mir', 'Lev Tolstoy'));
$books->addBook(new Book('Pod Igoto', 'Ivan Vazov'));
$books->addBook(new Book('Mamino detentse', 'Luben Karavelov'));
Подаваме масивът с обекти $books на даден итератор.
$booksIterator = new BookListStraightIterator($books);
Циклим.
while ($booksIterator->hasNextBook()) {
$book = $booksIterator->getNextBook();
echo "getting next book with iterator : \n";
echo $book->getAuthorAndTitle();
}
hasNextBook() ще прави проверка дали има още елементи и ще връща true/false.
Aко има, getNextBook() ще взема следващият елемент и ще го връща, с помощта на някакъв вътрешен брояч, който ще се инкрементира всеки път.
Докато има още – цикли и вземай следващият, следващият… като си ги следиш дали не свършват. Отделно, получаваш и брояч, който да следи до къде си стигнал с цикленето.
„Provide a way to access the elements of an aggregate object sequentially
without exposing its underlying representation.“ [GoF]
Защо „without exposing its underlying representation“, демек без да знаем и да работим със самата структура на data структурата?
Защото както се вижда, използваме допълнителни методи, които да „свършат работата“ просто казано. И тези допълнителни методи са специфични за всяка data структура, и са „заповядани“ с интерфейс. И те просто казват: „има ли следващ? Да. Дай ми го… Има ли следващ? Да. Дай ми го…“ без да те интересува самата му структура както и какво точно се има предвид под „следващият“.
Вече като вземеш даденият елемент, тогава може би да, но не и просто да го изциклиш.
Отделно, така „скриваме“ data структурата от използващите я „клиенти“. „Клиентът“ е този който цикли, заради него е цялата дандания. За да може да цикли без да знае повече за това, което цикли от „дай ми следващият, дай ми следващият…“.
Ако искаме да ги изциклим в обратен ред например, пишем още един итератор от интерфейса IteratorInterface, и горе долу всичко ще е същото, само дето вътрешният брояч няма да започва от 0, а от броя елементи, и няма да се инкрементира, а ще се декрементира.
Също и може да си напишем итератор-филтър, който да ни връща само нужните елементи, съдейки по дадена логика…
По колкото различни начини може да искаме да изциклим дадена data структура, толкова различни методи трябва да имаме в нея (data структурата). Ясно е колко много това може да я усложни и уголеми, от там – нарушавайки Single responsibility principle.
Има и друг начин да приложим идеята на Iterator design pattern.
Пак създаваме колекция от книги $book, но вместо да я подаваме на итератор като BookListStraightIterator, в нея (в класа и) си задаваме интератор.
Тоест, класът Books ще има метод:
public function createStraightIterator():
BookListStraightIterator
{ return new BookListStraightIterator($this); }
И циклим така:
$straightIterator = $booksCollection->createStraightIterator();
while ($straightIterator->hasNext()) { echo $straightIterator->next() . "\n"; }
Тоест, разликата основно е, че обектът-колекция си върви в комплект с итератора, като разбира се може да има повече от един.
Но не нарушаваме ли Single Responsibility SOLID принципът…
Три ползи от използването на Iterator design pattern
Този design pattern е фокусиран изцяло и само върху изциклянето на дадените data structures. И по-точно – да капсулира това изцикляне чрез създаване на допълнителна функционалност и разбира се – чрез използването на тази допълнителна функционалност.
Като допълнение към горното, можем отделно да имаме и функционалност за това какво да правим с всеки текущ елемент. В смисъл, че може да имаме функционалност например за изтриване на елемент – remove()…
Също, важно е да се знае, че Iterator design pattern няма отношение с реда на елементите в колекцията, нито трябва да дава функционалност за сортиране.
- Скрива структурата на това, което циклим от външният свят, от клиента, когато искаме да го циклим. Демек, клиентът (циклещият) няма нужда да знае как точно да изцикли структурата, няма нужда да знае масив ли е и т.н…. и отделните и елементи… Просто казва „докато не свършат, давай ми елементите един по един…“
- Lazy evaluation – „не ми ги стоварвай всички наведнъж, давай ми ги един по един. Дай първият, аз ще ти кажа кога искам вторият, третият…“
Така теоретично можем да циклим дори и безкрайни структури. - Имаш по-добър контрол върху самото циклене – можеш да знаеш къде си, да паузираш, да продължиш… не че с обикновено циклене не можеш също, но тук можеш по-гъвкаво да зададеш например някаква логика за това „кой да е следващият например“ и т.н…
При обикновеното циклене просто ти ги подава така както са сортирани, това е единствената логика, и ако искаш допълнителна логика – може разбира се но трябва да е вътре в цикъла и трябва да знаеш структурата на data структурата… връщаме се на точка 1.
Видове Iterator design pattern
- На база това кой контролира цикленето, биват internal и external iterator. При internal самият итератор контролира кой е следващият, при external – използващият клиент. Демек, при external iterator ние като клиент трябва да задаваме броячът и по този начин да казваме кой е следващият елемент, който ни трябва.
Демек, по-горе разгледаните примери са internal iterator, защото вътре в итератора контролираме кой ще е следващият върнат елемент. - Кой задава и определя самата логика за изцикляне – итераторът или клиентът?
Когато итераторът само следи за това кой е текущият елемент съхранявайки само броячът, a клиентът цикли, такъв итератор се нарича cursor.
We call this kind of iterator a cursor, since it merely points to the current position in the aggregate.
Клиентът сам си вика методи като getNext() например когато иска да си вземе текущият за дадената итерация елемент. И getNext() ще зададе новата стойност на брояча. - Колко надежден е един итератор? Какво се има предвид под „надежден“? Има се предвид ако например по време на циклене внасяме различни промени в data структурата (добавяне на елементи, триене…) дали това няма да се отрази негативно на цикленето, например в реда, в това кой е следващият…
Итератор, който е надежден в това отношение, и то без заобиколни мерки като копиране на елементи например, се нарича „robust iterator“.
Например като актуализираме броячът по начин такъв, че например да не върнем два пъти даден елемент, или да не върнем null, ако елементът е бил изтрит и т.н…
On insertion or removal, the aggregate either adjusts the internal state of iterators it has produced, or it maintains information internally to ensure proper traversal. - Minimal iterator – по принцип минималният итератор трябва да съдържа такива методи: first(), next(), isDone() и currentElement().
Но може да имаме и повече методи като например previous() и т.н…
Литература: