Javascript. Iterator pattern

Паттерн Итератор (Iterator pattern) предоставляет механизм последовательного перебора элементов коллекции без раскрытия ее внутреннего представления. То есть паттерн позволяет перебирать элементы коллекции, не зная, как реализована коллекция. Применение паттерна позволяет передать ответственность перебора элементов от объекта коллекции объекту итератора. Это обстоятельство не только упрощает интерфейс, но и избавляет коллекцию от посторонних обязанностей (ее главной задачей является управление объектами, а не перебор). Данный паттерн относится к поведенческим паттернам.



Давайте рассмотрим пример реализации данного паттерна на примере двух меню: один реализуем на основе массивов, другой на основе абстрактной структуры данных - списка. Для этих коллекции реализуем свои итераторы.

/* Abstract data type - List */

var List = (function () {

    var Node = function () {

        var element,    
            next;
        
        this.getElement = function () {
            return element;
        };
        
        this.setElement = function (_element) {
            element = _element; 
        };
        
        this.getNext = function () {
            return next;
        };
        
        this.setNext = function (_next) {
            next = _next;
        };

    };
    
    return function () {

        var headNode = null,
            tailNode = headNode,
            length = 0;

        this.push = function (element) {
            if (headNode === null) {
                headNode = new Node();
                headNode.setElement(element);
                tailNode = headNode;
                headNode.setNext(null);                
            }
            else {
                var newNode = new Node();
                tailNode.setNext(newNode);
                tailNode = newNode;
                tailNode.setElement(element);
                tailNode.setNext(null);
            }
            length++;
        };
        
        this.get = function (position) {
            if (position >= length) {
                throw new Error("");
            }
            for (var i = 0, tempNode = headNode; i < position; i++) {
                tempNode = tempNode.getNext();
            }
            return tempNode.getElement();
        };
        
        this.length = function () {
            return length;
        };

    };

})();

/* Template of menu collection */

var MenuItem = function (name, description, vegetarian, price) {

    var name = name,
        description = description,
        vegetarian = vegetarian,
        price = price;
    
    this.getName = function () {
        return name;
    };
    
    this.getDescription = function () {
        return description;
    };
    
    this.getPrice = function () {
        return price;
    };
    
    this.isVegetarian = function () {
        return vegetarian;
    };
    
};

/* interface Iterator */

var Iterator = function () {
    this.hasNext = function () {};
    this.next = function () {};
    this.remove = function () {};
};

/* interface Aggregate - interface Menu */

var Menu = function () {
    this.createIterator = function () {};
};

/* ConcreteIterator - DinerMenuIterator - for concreteAggreate - DinerMenu */

var DinerMenuIterator = function (items) {
    
    var items = items || [],
        position = 0;
        
    this.next = function () {
        var menuItem = items[position];
        position = position + 1;
        return menuItem;
    };
    
    this.hasNext = function () {
        if (position >= items.length || typeof items[position] === "undefined") {
            return false;
        } 
        else {
            return true;
        }
    };
    
    this.remove = function () {};
    
};

DinerMenuIterator.prototype = new Iterator();
DinerMenuIterator.prototype.constructor = DinerMenuIterator;

/* ConcreteAggreate - DinerMenu collection */

var DinerMenu = function () {
    
    var MAX_ITEMS = 6;
    var numberOfItems = 0;
    var menuItems = new Array(MAX_ITEMS);

    this.addItem = function (name, description, vegetarian, price) {
        var menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS) {
            throw new Error("Sorry, menu is full! Can`t add item to menu");
        }
        else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    };

    this.createIterator = function () {
        return new DinerMenuIterator(menuItems);
    };

    // other methods
    
    (function (menu) {    
        menu.addItem("Vegetarian BLT", "(Fakin) Bacon with lettuce & tomato on whle wheat", true, 2.99);
        menu.addItem("BLT", "Bacon with lettuce & tomato on whle wheat", false, 2.99);
        menu.addItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29);
        menu.addItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese", false, 3.05);
    })(this);

};

DinerMenu.prototype = new Menu();
DinerMenu.prototype.constructor = DinerMenu;

/* ConcreteIterator - PancakeHouseMenuIterator - for concreteAggreate - PancakeHouseMenu */

var PancakeHouseMenuIterator = function (items) {
    
    var items = items,
        position = 0;
        
    this.next = function () {
        var menuItem = items.get(position);
        position = position + 1;
        return menuItem;
    };
    
    this.hasNext = function () {
        if (position >= items.length()) {
            return false;
        } 
        else {
            return true;
        }
    };
    
    this.remove = function () {};
    
};

PancakeHouseMenuIterator.prototype = new Iterator();
PancakeHouseMenuIterator.prototype.constructor = PancakeHouseMenuIterator;

/* ConcreteAggreate - PancakeHouseMenu collection */

var PancakeHouseMenu = function () {
    
    var menuItems = new List;

    this.addItem = function (name, description, vegetarian, price) {
        var menuItem = new MenuItem(name, description, vegetarian, price);
        menuItems.push(menuItem);
    };

    this.createIterator = function () {
        return new PancakeHouseMenuIterator(menuItems);
    };

    // other methods
    
    (function (menu) {    
        menu.addItem("K&B`s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99);
        menu.addItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99);
        menu.addItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49);
        menu.addItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59);
    })(this);

};

PancakeHouseMenu.prototype = new Menu();
PancakeHouseMenu.prototype.constructor = PancakeHouseMenu;

/* Client of PancakeHouseMenu & DinerMenu concreteAggreates */

var Waitress = function (pancakeHouseMenu, dinerMenu) {

    var pancakeHouseMenu = pancakeHouseMenu,
        dinerMenu = dinerMenu;
    
    this.printMenu = function () {
    
        if (arguments.length === 0) {
            var pancakeIterator = pancakeHouseMenu.createIterator();
            var dinerIterator = dinerMenu.createIterator();
            console.log("MENU");
            console.log("---");
            console.log("BREAKFAST");
            this.printMenu(pancakeIterator);
            console.log("LUNCH");
            this.printMenu(dinerIterator);
        }
        
        if (arguments.length === 1 && arguments[0] instanceof Iterator) {
            var iterator = arguments[0];
            while (iterator.hasNext()) {
                var menuItem = iterator.next();
                menuItem =  menuItem.getName() + ", " +
                            menuItem.getPrice() + " -- " +
                            menuItem.getDescription();
                console.log(menuItem);
            }
        }
    
    };

};

/* test application */

var Application = function () {
    this.run = function () {
    
        var pancakeHouseMenu = new PancakeHouseMenu();
        var dinerMenu = new DinerMenu();
        
        var waitress = new Waitress(pancakeHouseMenu, dinerMenu);
        
        waitress.printMenu();
    
    };
};

var application = new Application();
application.run();

На выходе получаем следующие сообщения:

MENU
---
BREAKFAST
K&B`s Pancake Breakfast, 2.99 -- Pancakes with scrambled eggs, and toast
Regular Pancake Breakfast, 2.99 -- Pancakes with fried eggs, sausage
Blueberry Pancakes, 3.49 -- Pancakes made with fresh blueberries
Waffles, 3.59 -- Waffles, with your choice of blueberries or strawberries
LUNCH
Vegetarian BLT, 2.99 -- (Fakin) Bacon with lettuce & tomato on whle wheat
BLT, 2.99 -- Bacon with lettuce & tomato on whle wheat
Soup of the day, 3.29 -- Soup of the day, with a side of potato salad
Hotdog, 3.05 -- A hot dog, with saurkraut, relish, onions, topped with cheese

Обратите внимание на метод printMenu клиента меню-коллекций Waitress. Это своего рода способ перегрузки методов. Давайте рассмотрим еще один пример. На этот раз паттерн Итератор будет рассмотрен совместно с паттерном Компоновщик. Давайте рассмотрим пример на основе составления меню. Также стоит отметить один интересный подход. Как известно, в Java для большинства коллекций уже реализованы итераторы, для их создания достаточно для коллекции вызвать метод iterator(). Так как в javascript все же основной структурой данных является массив (хотя есть и литеральные объекты), то и мы можем реализовать своего рода поддержку "стандартного" итератора. Например вот таким образом:

Array.prototype.iterator = function () {
    var self = this, position = 0, undefined;
    return {
        next:function () {
            return self[position++];
        },
        hasNext: function () {
            if (position >= self.length) {
                return false;
            } 
            else {
                return true;
            }
        },
        remove: function () {
            if (position <= 0) {
                throw new Error("You can`t an item until you`ve done at least one next()");
            }
            if (self[position - 1] !== undefined) {
                for (var i = position - 1; i < (self.length - 1); i++) {
                    self[i] = self[i + 1];
                }
                self[self.length - 1] = undefined;
            }
        }
    };
};

Как видим, мы реализуем интерфейс итератора. Итак. Вот следующая реализация паттерна.

Array.prototype.iterator = function () {
    var self = this, position = 0, undefined;
    return {
        next:function () {
            return self[position++];
        },
        hasNext: function () {
            if (position >= self.length) {
                return false;
            } 
            else {
                return true;
            }
        },
        remove: function () {
            if (position <= 0) {
                throw new Error("You can`t an item until you`ve done at least one next()");
            }
            if (self[position - 1] !== undefined) {
                for (var i = position - 1; i < (self.length - 1); i++) {
                    self[i] = self[i + 1];
                }
                self[self.length - 1] = undefined;
            }
        }
    };
};

/* abstract class MenuComponent */

var MenuComponent = function () {

    this.add = function (menuComponent) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.remove = function (menuComponent) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getChild = function (i) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getName = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getDescription = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getPrice = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.isVegetarian = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.print = function () {
        throw new Error("UnsupportedOperationException");
    };

}

/* Menu element */

var MenuItem = function (name, description, vegetarian, price) {

    var name = name,
        description = description,
        vegetarian = vegetarian,
        price = price;
    
    this.getName = function () {
        return name;
    };
    
    this.getDescription = function () {
        return description;
    };
    
    this.getPrice = function () {
        return price;
    };
    
    this.isVegetarian = function () {
        return vegetarian;
    };
    
    this.print = function () {
        var item =  " " + this.getName() + 
                    (this.isVegetarian() ? "(v)" : "") + 
                    ", " + this.getPrice() + 
                    "   -- " + this.getDescription();
        console.log(item);
    };

};

MenuItem.prototype = new MenuComponent();
MenuItem.prototype.constructor = MenuItem;

/* Menu */

var Menu = function (name, description) {

    var menuComponents = [],
        name = name,
        description = description;

    this.add = function (menuComponent) {
        menuComponents.push(menuComponent);
    };
    
    this.remove = function (menuComponent) {
        var index = menuComponents.indexOf(menuComponent);
        if (index >= 0) {
            menuComponents.splice(index, 1);
        };
    };
    
    this.getChild = function (i) {
        return menuComponents[i];
    };
    
    this.getName = function () {
        return name;
    };
    
    this.getDescription = function () {
        return description;
    };
    
    this.print = function () {
        var item =  this.getName() + ", " +    this.getDescription();
        console.log(item);
        console.log("----------------------");
        
        var iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            var menuComponent = iterator.next();
            menuComponent.print();
        }
    };

};

Menu.prototype = new MenuComponent();
Menu.prototype.constructor = Menu;

/* Client */

var Waitress = function (allMenus) {

    var allMenus = allMenus;
    
    this.printMenu = function () {
        allMenus.print();
    };

};

/* test application */

var Application = function () {
    this.run = function () {
    
        var pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast"), 
            dinerMenu = new Menu("DINER MENU", "Lunch"), 
            cafeMenu = new Menu("CAFE MENU", "Dinner"),
            dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
            
        var allMenus = new Menu("ALL MENUS", "All menus combined");
        
        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
        
        pancakeHouseMenu.add(new MenuItem("K&B`s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99));
        pancakeHouseMenu.add(new MenuItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99));
        pancakeHouseMenu.add(new MenuItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49));
        pancakeHouseMenu.add(new MenuItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59));
    
        dinerMenu.add(new MenuItem("Vegetarian BLT", "(Fakin) Bacon with lettuce & tomato on whle wheat", true, 2.99));
        dinerMenu.add(new MenuItem("BLT", "Bacon with lettuce & tomato on whle wheat", false, 2.99));
        dinerMenu.add(new MenuItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29));
        dinerMenu.add(new MenuItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese", false, 3.05));
        dinerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
        
        dinerMenu.add(dessertMenu);        
        dessertMenu.add(new MenuItem("Apple Pie", "Apple pie with a flakey crust, topped with vanilla icecream", true, 1.59));
        
        cafeMenu.add(new MenuItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries", true, 3.99));
        cafeMenu.add(new MenuItem("Soup of the day", "A cup of the soup of the day, with a side salad", false, 3.69));
        cafeMenu.add(new MenuItem("Burrito", "A large burrito, with whole pinto beans, salsa, guacamole", true, 4.29));
        
        var waitress = new Waitress(allMenus);
        
        waitress.printMenu();
    
    };
};

var application = new Application();
application.run();

На выходе получим следующие результаты:

ALL MENUS, All menus combined
----------------------

PANCAKE HOUSE MENU, Breakfast
----------------------
K&B`s Pancake Breakfast(v), 2.99   -- Pancakes with scrambled eggs, and toast
Regular Pancake Breakfast, 2.99   -- Pancakes with fried eggs, sausage
Blueberry Pancakes(v), 3.49   -- Pancakes made with fresh blueberries
Waffles(v), 3.59   -- Waffles, with your choice of blueberries or strawberries

DINER MENU, Lunch
----------------------
Vegetarian BLT(v), 2.99   -- (Fakin) Bacon with lettuce & tomato on whle wheat
BLT, 2.99   -- Bacon with lettuce & tomato on whle wheat
Soup of the day, 3.29   -- Soup of the day, with a side of potato salad
Hotdog, 3.05   -- A hot dog, with saurkraut, relish, onions, topped with cheese
Pasta(v), 3.89   -- Spaghetti with Marinara Sauce, and a slice of sourdough bread

DESSERT MENU, Dessert of course!
----------------------
Apple Pie(v), 1.59   -- Apple pie with a flakey crust, topped with vanilla icecream

CAFE MENU, Dinner
----------------------
Veggie Burger and Air Fries(v), 3.99   -- Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Soup of the day, 3.69   -- A cup of the soup of the day, with a side salad
Burrito(v), 4.29   -- A large burrito, with whole pinto beans, salsa, guacamole

На этом все. Всем удачи! :)