Korrektur des Kompositum-Iterators aus dem Buch "Entwurfsmuster von Kopf bis Fuß"

In diesem Buch wird in Kapitel 9 "Die Iterator- und Composite-Muster" auf Seite 369 ein Kompositum-Iterator beschrieben, der über alle Knoten und Blätter einer Baumstruktur iterieren soll. Das abgedruckte Code-Beispiel hat einige Fehler, die dazu führen, dass die Blätter in der untersten Ebene mehrfach durchlaufen werden (siehe Seite 374). Das Problem liegt in diesen drei Zeilen:

    MenuComponent component = (MenuComponent) iterator.next();
    if (component instanceof Menu)
        stack.push(component.createIterator());

Durch die Rekursion wird für einen Knoten (component des Typs Menu), für den gerade ein Kompositum-Iterator erzeugt wurde, in der Ebene darüber fälschlicherweise ein zweiter Kompositum-Iterator erzeugt. Im Verlauf der Iteration werden alle erzeugten Iteratoren abgearbeitet und somit der betroffene Knoten und sein Teilbaum ein zweites Mal durchlaufen. Das Problem würde sich auf jeder weiteren Ebene darüber wiederholen, das heißt wenn es im Beispiel oberhalb von allMenus noch eine Über-Sammel-Speisekarte als Toplevel gäbe, würden die Einträge von dessertMenu dreimal durchlaufen werden.

Die Lösung besteht nun darin, vor den Aufruf der Funktion createIterator() einfach eine zusätzliche Bedingung einzufügen, die prüft, ob der aktuelle Iterator ein einfacher und nicht bereits ein Kompositum-Iterator ist. Das könnte zum Beispiel so aussehen:

    if (component instanceof Menu)
        if (currentIterator == baseIterator)
            currentIterator = component.createIterator();

baseIterator ist hier der einfache Iterator, welcher dem Konstruktor übergegen wurde (im Code-Beispiel des Buches ein ArrayList-Iterator). Außerdem kann das Stack-Objekt aus der Klasse entfernt werden, weil die Rekursion bereits wie ein Stack wirkt. Dadurch vereinfacht sich der Code um einiges. Unten sehen Sie die komplette Definition einer neuen Version des Kompositum-Iterators sowie die Ausgaben einiger Testläufe.

Weitere Informationen zu dem problematischen Code-Beispiel im Buch finden Sie auch auf diesen Seiten:

Errata von O'REILLY
Diskussion zum Thema
JavaRanch Big Moose Saloon


Neue Definition der Klasse CompositeIterator

import java.util.*;
  
public class CompositeIterator implements Iterator {
    Iterator baseIterator, currentIterator;
    
    public CompositeIterator(Iterator iterator) {
        currentIterator = baseIterator = iterator;
    }
    
    public Object next() {
        if (hasNext()) {
            MenuComponent component = (MenuComponent) currentIterator.next();
            if (component instanceof Menu) {
                if (currentIterator == baseIterator) {
                    currentIterator = component.createIterator();
                }
            } 
            return component;
        } else {
            return null;
        }
    }
    
    public boolean hasNext() {
        if (currentIterator.hasNext()) {
            return true;
        }
        if (currentIterator == baseIterator) {
            return false;
        }
        currentIterator = baseIterator;
        
        return currentIterator.hasNext();
    }
    
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

Der neue Kompositum-Iterator enthält nicht mehr Codezeilen als die Originalversion. Ohne den Stack lässt sich auch seine Funktionsweise leichter nachvollziehen. Es sei jedoch angemerkt, dass die Funktion next() - ebenso wie in der Originalversion - nicht den Root-Node (allMenus) zurückgibt.

Ausgabe des Testlaufs:

Screenshot


In der deutschen Ausgabe fällt der Fehler noch deutlicher ins Auge, da hier im Code-Beispiel auf Seite 365 noch eine Speisekarte zusätzlich hinzugefügt wird (kaffeeSpeisekarte), so dass auf der untersten Ebene zwei weitere Einträge mehrfach ausgegeben werden. Auf Seite 374 erscheinen somit fünf Einträge doppelt (Apfelkuchen, Käsekuchen, Sorbet, Zupfkuchen, Kekse). Die folgende Abbildung zeigt die korrekte Arbeitsweise des neuen Kompositum-Iterators.

Ausgabe des deutschsprachigen Testlaufs mit neuem KompositumIterator:

Screenshot

Weitere Testläufe

Testlauf mit Speisekarte, Unterspeisekarte und einer Speise auf unterster Ebene

public class MenuTestDrive1 {
    public static void main(String args[]) {
    
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        
        allMenus.add(dinerMenu);
        
        dinerMenu.add(dessertMenu);
        
        dessertMenu.add(new MenuItem(
            "Apple Pie",
            "Apple pie with a flakey crust, topped with vanilla icecream",
            true, 1.59));
        
        Waitress waitress = new Waitress(allMenus);
        
        waitress.printVegetarianMenu();
    
    }
}

Ausgabe:

Screenshot

Testlauf mit Speisekarte, Unterspeisekarte und zwei Speisen auf mittlerer Ebene

public class MenuTestDrive2 {
    public static void main(String args[]) {
    
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        
        allMenus.add(dinerMenu);
        
        dinerMenu.add(dessertMenu);
        
        dinerMenu.add(new MenuItem(
            "Vegetarian BLT",
            "(Fakin') Bacon with lettuce & tomato on whole wheat", 
            true, 2.99));
        
        dinerMenu.add(new MenuItem(
            "Steamed Veggies and Brown Rice",
            "A medly of steamed vegetables over brown rice", 
            true, 3.99));
        
        Waitress waitress = new Waitress(allMenus);
        
        waitress.printVegetarianMenu();
    
    }
}

Ausgabe:

Screenshot

Testlauf mit Speisekarte und leerer Unterspeisekarte

public class MenuTestDrive3 {
    public static void main(String args[]) {
    
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        
        allMenus.add(dinerMenu);
        
        dinerMenu.add(dessertMenu);
        
        Waitress3 waitress = new Waitress3(allMenus);
        
        waitress.printVegetarianMenu();
    
    }
}

public class Waitress3 {
    ...
        } catch (UnsupportedOperationException e) {
            System.out.println("--- Menu node ---");
        }
    ...
}

Ausgabe:

Screenshot

Testlauf ohne Sammelkarte

public class MenuTestDrive4 {
    public static void main(String args[]) {
    
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
        
        dinerMenu.add(new MenuItem(
            "Vegetarian BLT",
            "(Fakin') Bacon with lettuce & tomato on whole wheat", 
            true, 2.99));
        
        dinerMenu.add(dessertMenu);
        
        dinerMenu.add(new MenuItem(
            "Steamed Veggies and Brown Rice",
            "A medly of steamed vegetables over brown rice", 
            true, 3.99));
        
        dessertMenu.add(new MenuItem(
            "Apple Pie",
            "Apple pie with a flakey crust, topped with vanilla icecream",
            true, 1.59));
        
        dessertMenu.add(new MenuItem(
            "Cheesecake",
            "Creamy New York cheesecake, with a chocolate graham crust",
            true, 1.99));
        
        Waitress waitress = new Waitress(dinerMenu);
        
        waitress.printVegetarianMenu();
    
    }
}

Ausgabe:

Screenshot


Home - Kontakt © 2008 software-entwicklung.peter-nebe.de - Impressum