If there is a custom collection of elements, implementing an iterator helps get that special order of elements
in a standardized way without looking under the hood. The following example is used to print out a recipe in such a way that
ingredients are displayed in the order that they need to be mixed, regardless of the order that they were added when a recipe
was recorded. The "magic" is happening when ingredients are added to a collection, they are being sorted at the time of creation
of the collection. The end user of the collection does not know it. That was my choice of implementing it that way, I could have
done it differently, but the user does not care. They give me the name for the recipe, a set of ingredients, and want it printed
out nicely.
First, service classes. CLass Ingredient describes each ingredient, whether it's dry or wet, and stores how much
of it is needed for a recipe. Class Recipe takes in the name of the dish, array of Ingredients, and array of strings for directions.
Then, in the printRecipe method, it gets an enumerator for the collection of ingredients, and lists each one of them until there are
no more ingredients to show.
class Ingredient {
name: string;
isDry: boolean;
unitOfMeasure: string;
units: string;
constructor(
name: string,
isDry: boolean,
unitOfMeasure: string,
units: string
) {
this.name = name;
this.isDry = isDry;
this.unitOfMeasure = unitOfMeasure;
this.units = units;
}
displayIngredient = () =>
`${this.name} (${this.isDry ? "dry" : "wet"}), ${this.units} ${
this.unitOfMeasure.toLocaleLowerCase() === "unit"
? ""
: this.unitOfMeasure
}`;
}
class Recipe {
ingredients: IngredientsCollection;
directions: Array;
dishName: string;
constructor(
dishName: string,
ingredients: Array,
directions: Array
) {
this.dishName = dishName;
this.ingredients = new IngredientsCollection(ingredients);
this.directions = directions;
}
printRecipe(): void {
console.log(this.dishName.toUpperCase());
let ingrenum = this.ingredients.getIterator(); //<-- getting iterator
console.log("\nINGREDIENTS:");
while (ingrenum.hasNext()) {
console.log(ingrenum.next().displayIngredient());
}
console.log("\nDIRECTIONS: ");
this.directions.forEach((d) => console.log(d));
}
cookRecipe(): void {
//lol not this time
}
}
So what is that getIterator method? It is coming from IIngredientEnumerable interface and is implemented by
the IngredientsCollection class, which acts as a container.
interface IIngredientIterable {
getIterator(): IIngredientIterator;
}
class IngredientsCollection implements IIngredientIterable {
private allIngredients: Array;
constructor(allIngredients: Array) {
if (allIngredients === undefined || allIngredients.length === 0)
throw new Error("Cannot iterate over empty ingredient collection");
this.allIngredients = allIngredients;
}
getIterator(): IIngredientIterator {
return new IngredientIterator(this.allIngredients);
}
}
getIterator returns an object that provides utility methods needed to navigate the underlying collection.
There is no requirement for what methods should be, as long as they provide a convenient way to navigate for users.
The following iterator interface implements three methods, one for advancing to the next Ingredient, one to check
if there is indeed a next Ingredient, and one to return the current Ingredient.
interface IIngredientIterator {
next(): Ingredient;
hasNext(): boolean;
currentItem: Ingredient;
}
The following class is the actual iterator (yay, we finally got to it!) and it implements the methods in
the interface above.
class IngredientIterator implements IIngredientIterator {
private allIngredients: Array;
private currentIndex: number;
currentItem: Ingredient;
constructor(allIngredients: Array) {
this.allIngredients = allIngredients;
this.sortIngredientsByTypeThenName();
this.currentIndex = 0;
this.currentItem = allIngredients[0];
}
next(): Ingredient {
if (!this.hasNext()) return null;
this.currentIndex++;
this.currentItem = this.allIngredients[this.currentIndex];
return this.currentItem;
}
hasNext(): boolean {
return this.currentIndex < this.allIngredients.length - 1;
}
private sortIngredientsByTypeThenName() {
let sorted = this.allIngredients.filter(
(x) => x.isDry == true
) as Ingredient[];
sorted.sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0));
let wetIngredients = this.allIngredients.filter(
(x) => x.isDry === false
) as Array;
wetIngredients.sort((a, b) =>
a.name > b.name ? 1 : a.name < b.name ? -1 : 0
);
sorted.push(...wetIngredients);
this.allIngredients.length = 0;
this.allIngredients.push(...sorted);
}
}
In the scenario above, you can think of it as a vehicle on the road. The road in this case is the
Array<Ingredient> allIngredients
in class IngredientIterator
. It is the road because our vehicle can only drive
on it, not anywhere left or right, but just on it. The next()
method of interface IIngredientIterator
is our transmission. It specifies
where we can go, in our case only forward. The other two methods are windows in the car, we can see where we are and whether there is road ahead. Finally, a class
that implements the IIngredientIterable
is the driver. He/she summons the vehicle and can drive on the road (iterate over an Array
of Ingredients
),
to the next Ingredient
(by calling next()
on IIngredientIterator
method) while knowing where he/she is at all
times (currentItem
property and hasNext()
method of the IIngredientIterator
interface).
Here is the calling code and the result. All that it needed to do was to print out dry ingredients first, then wet ingredients, regardless of how the recipe was
originally recorded.
let ingrFlour = new Ingredient("flour", true, "cup", "1.5");
let ingrSugar = new Ingredient("sugar", true, "cup", "0.5");
let ingrBakingPowder = new Ingredient("baking powder", true, "tsp", "1.5");
let ingrSalt = new Ingredient("salt", true, "tsp", "0.5");
let ingrOil = new Ingredient("oil", false, "cup", "1/3");
let ingrVanilla = new Ingredient("vanilla extract", false, "tbsp", "2");
let ingrRicotta = new Ingredient("ricotta cheese", false, "cup", "1.5");
let ingrMilk = new Ingredient("milk", false, "tbsp", "5");
let ingrEgg = new Ingredient("egg", false, "unit", "1");
let ingrBlueberries = new Ingredient("blueberries", false, "cup", "1/3");
let arrayOfIngre = [
ingrBakingPowder,
ingrBlueberries,
ingrEgg,
ingrFlour,
ingrMilk,
ingrOil,
ingrRicotta,
ingrSalt,
ingrSugar,
ingrVanilla,
] as Array;
let muffins = new Recipe("Ricotta blueberry muffins", arrayOfIngre, [
"mix dry ingredients",
"mix wet ingredients",
"mix together",
"put in muffin pan",
"bake at 375 for 32 minutes or until ready",
]);
muffins.printRecipe();
Output, note how all dry ingredients come in one group and all wet ingredients come in another. Ingredients are sorted
by name within a group.
RICOTTA BLUEBERRY MUFFINS
INGREDIENTS:
baking powder (dry), 1.5 tsp
flour (dry), 1.5 cup
salt (dry), 0.5 tsp
sugar (dry), 0.5 cup
blueberries (wet), 1/3 cup
egg (wet), 1
milk (wet), 5 tbsp
oil (wet), 1/3 cup
ricotta cheese (wet), 1.5 cup
vanilla extract (wet), 2 tbsp
DIRECTIONS:
mix dry ingredients
mix wet ingredients
mix together
put in muffin pan
bake at 375 for 32 minutes or until ready
The example above may seem slightly complicated. Just remember the main idea. There is a container that has some data.
User can navigate the container using pre-determined methods for them, but they have no idea how it works and what is under the hood.
The same as public transportation, you get a ticket to ride the bus. The stops are predetermined and someone else is driving it, but
you can get off at any stop, and you see each stop as the bus goes along its route. You have no idea how the bus' engine works, you
cannot drive it, you cannot change its course, but you consume its services.
Here is another, slightly simpler example. The following code generates Fibonacci sequence based on users' input.
The same structure, a container, then a class for the actual iterator, then some calling code:
interface IIterable {
getIterator(): IIterator;
}
class FibonacciSequence implements IIterable { //container
numberOfDigits: number;
constructor(numberOfDigits: number) {
this.numberOfDigits = numberOfDigits;
}
getIterator(): IIterator {
return new FibonacciEnumerator(this.numberOfDigits);
}
}
interface IIterator {
current(): number;
moveNext(): boolean;
}
class FibonacciEnumerator implements IIterator {
numberOfDigits: number;
currentPosition: number;
previousTotal: number;
currentTotal: number;
constructor(numberOfDigits: number) {
this.numberOfDigits = numberOfDigits;
this.reset();
}
current(): number {
return this.currentTotal;
}
moveNext(): boolean {
let newTotal = this.previousTotal + this.currentTotal;
this.previousTotal = this.currentTotal;
this.currentTotal = newTotal;
this.currentPosition++;
return this.currentPosition <= this.numberOfDigits;
}
private reset(): void {
this.currentPosition = 0;
this.previousTotal = 0;
this.currentTotal = 1;
}
}
let f = new FibonacciSequence(10);
let e = f.getIterator();
let output = "Result: ";
while (e.moveNext()) {
output += e.current() + " ";
}
console.log(output);
Result: 1 2 3 5 8 13 21 34 55 89