21. Januar 2013

Entwurfsmuster: Iterator

Das Verhaltensmuster "Iterator", was in diesem Post vorgestellt werden soll, ist eines der meist verwendeten Entwurfsmuster überhaupt. Es dient dazu sequentiell auf eine Datenstruktur (Aggregat) zuzugreifen, ohne dass dazu deren interne Repräsentation offen gelegt werden muss. Das dazugehörige Klassendiagramm ist im Folgenden dargestellt:

Die Klassen und Interfaces eines Iterators

Das Interface "Aggregate" muss dabei von Datenstrukturen (in diesem Beispiel eine Liste) implementiert werden, die Iteratoren anbieten sollen, und definiert eine einzelne Methode, die einen solchen Iterator zurückgibt. Das Interface "Iterator" defniert die Schnittstelle, die von einer Iterator-Implementierung, von der es für jeden Datenstrukturtyp jeweils eine geben wird, implementiert werden muss. Die in diesem Beispiel definierte Schnittstelle stellt nur die mindestens nötigen Funktionen bereit, es wären noch viele weitere denkbar. Jede Datenstruktur, die das "Aggregate"-Interface implementiert, liefert beim Aufruf der iterator()-Methode eine Instanz auf einen, zur Datenstruktur passenden, Iterator, wie es in dem Code-Fragment angedeutet ist.

Vorteile:
  • Die Schnittstelle des Aggregats wird vereinfacht, da es selbst keine Schnittstelle für den Zugriff auf die Daten anbieten muss.
  • Es sind mehrere Traversierungen zur selben Zeit möglich, da jeder Iterator seinen eigenen Traversierungszustand verwaltet.
Nachteile: 
  • Der Iterator muss damit umgehen können, wenn Daten während des Iterierungsvorgangs hinzugefügt oder gelöscht werden. Dazu muss der interne Zustand des Iterators vom Aggregat angepasst werden, sobald dessen interne Repräsentation geändert wird.
  • Ein Iterator benötigt oft priviligierten Zugriff auf die zu iterierende Datenstruktur. Dieses Problem kann entweder gelöst werden, indem man den Iterator als Inner-Class des Aggregats implementiert (in diesem Fall kann der Iterator dann explizit über einen Aufruf wie  new List.Iterator() oder polymorph über eine Fabrikmethode des Aggregats erzeugt werden), oder indem man den Iterator in einer separaten Klasse implementiert, die eine Referenz auf das Aggregat beim Konstruktoraufruf übergeben bekommt.

Der folgende Code-Ausschnitt zeigt eine Iterator-Implementierung, wie sie üblicherweise vorkommen könnte:
 class ListIterator implements Iterator {   
   
   private List list;   
   
   int count = 0;   
   
   public ListIterator(List list) {   
     this.list = list;   
   }   
   
   pubic boolean hasNext() {   
     return count < list.size();   
   }   
   
   public Object next() {   
     synchronized (list) {   
       if (count < list.size()) {   
         return list.get(count++);   
       }   
       return null;   
     }   
   }   
   
 }   

Dabei wird dem Iterator eine Referenz auf die Liste, über deren Daten iteriert werden soll, beim Konstruktoraufruf übergeben. Bei dem Attribut count handelt es sich um einen Zeiger auf den Index des Elements, auf das der Iterator gerade verweist. Die Methode hasNext() gibt zurück, ob es nach diesem Index noch weitere Elemente in der Datenstruktur gibt. Die Methode next() inkrementiert den internen Zeiger und gibt das jeweils nächste Element der Datenstruktur zurück. Dabei erfolgt der Zugriff über ein synchronized, damit es nicht zu Komplikationen zwischen, zeitgleich auf der Datenstruktur arbeitenden, Iteratoren gibt.

Im folgenden soll noch kurz gezeigt werden, wie die Verwendung eines Iterators in Java üblicherweise aussieht:
 Vector<Object> data = new Vector<>();  
 // TODO: Code um Datenstruktur zu füllen  
   
 Iterator<Object> iterator = data.iterator();  
   
 while (iterator.hasNext()) {  
   Object object = iterator.next();  
 }  

Keine Kommentare:

Kommentar veröffentlichen