In the field of programming, there exists a concept called ‘design patterns’. Design patterns are established solutions to recurring challenges in the field of software design.
A sample challenge that requires the use of a pattern is the case of an API endpoint that returns a list of items. It may be fine for the endpoint to always return all the items that the database has for every API call to the endpoint, but it gets to a point where the list of items to return would be unnecessarily large or some of the items in the list returned may not be needed by the client making a request to the API. What happens now? The endpoint would need to implement the pagination pattern.
Implementing the pagination pattern for the endpoint demands that the endpoint presents inputs so that consumers of the endpoint can fill in the number of items they want returned from the database. It can also provide inputs for consumers to fill in the “start” and “end” position in the database from which items should be returned. This solves the challenge of returning large volumes of data that may not be needed by the client.
# API endpoint before implementing the pagination pattern
GET http://api.com/v1/items
{
items: [
{
name: "Item 1",
price: 20
},
{
name: "Item 2",
price: 10
},
...998 more item objects
]
}
# API endpoint after implementing the pagination pattern
GET http://api.com/v1/items?limit=2&skip=3
{
"items": [
{
"name": "Item 4",
"price": 13
},
{
"name": "Item 5",
"price": 24
}
],
"totalCount": 1000,
"nextPage": 3,
"previousPage": 1,
"currentPage": 2
}
The endpoint that implements the pagination pattern returns only two item objects as requested by the client via the limit query parameter and as such, has less item objects than the endpoint that does not implement the pattern. The good thing is that this pattern (and all patterns in general) are programming language-independent. This pattern can be implemented in whatever language or framework you choose.
The Iterator Design Pattern
Programming languages have different types of data structures. Some are primitives (boolean and int) and some are made up of combining one or more primitives (collections such as arrays and objects). The challenge here is that programmers want a consistent pattern that they can use to traverse (move/go through) any collection irrespective of what the collection is (sets, maps, arrays, lists, trees). While traversing the collection, programmers also want to be able to do two things:
- get the item at the current point in the traversal
- determine whether the traversal has reached the end of the collection or not
However the traversal is done (from the start to the end, breadth-first, from the last to the end etc) is not the main concern. The main concern is to have a consistent pattern that will achieve 1 and 2 above for any collection.
As a JavaScript programmer, you may wonder why this is a challenge because you can run a for…index loop to achieve this. Have it in mind that design patterns are language-independent. In other programming languages, collections such as linked lists and trees exist and they do not have indices like JavaScript arrays. The iterator design pattern proposes a solution to resolve this. For an in-depth explanation of the iterator design pattern, you can check out Refactoring Guru’s article 1.
The iterator design pattern basically states that for its implementation,
collections should be able to produce an object (called the iterator
.) This
iterator object should have a method (let’s call the method getCurrent
). When
the method is executed, it should return an object with two fields (let’s call
them isDone
and currentItem
.) The isDone
field holds a truthy value that
tells the client (the client is whatever code is using the iterator object) if
the traversal has reached the end of the collection and there is no other item
to traverse to. The currentItem
field holds the value of the collection item
at the current position of the traversal. A collection that implements the
iterator design pattern is said to be an iterable.
JavaScript’s Implementation of the Iterator Design Pattern
JavaScript implements the iterator pattern via the iteration protocol 2 on some data structures (arrays, strings maps). This makes these data structures iterables.
For any collection (or data structure) to be an iterable in JavaScript, it must
be able to produce an iterator object, just as the iterator pattern has
demanded. Data structures in JavaScript such as arrays, strings and maps, have a
function property that when called, returns an iterator object. The name of that
function property is Symbol.iterator
and it can be executed on an array
instance in the same way that push
can be executed on an array instance.
const arr = [1, 2, 3];
console.log(typeof arr["push"]); // "function"
const pushReturnVal1 = arr.push(4); // pushReturnVal1 = 4
const pushReturnVal2 = arr["push"](5); // pushReturnVal2 = 5
console.log(typeof arr[Symbol.iterator]); // "function"
const iteratorObject = arr[Symbol.iterator]();
Drawing from the iterator design pattern, iteratorObject
in the code snippet
above should have a method that when called, will return an object with two
fields. iteratorObject
has that method and it is called next
.
console.log(iteratorObject.next()); // { value: 1, done: false }
console.log(iteratorObject.next()); // { value: 2, done: false }
Isn’t this exciting? It rightly returns an object with two fields - value
and
done
. value represents the item at the current position of the traversal
(index 0, 1) and done holds a truthy value that tells the client if there are
any more items to return (true
) or not (false
). If iteratorObject.next
is
called a total of 5 times, the return value of the next function for any run
after the 5th run will be { value: undefined, done: true }
. The behaviour of
this Symbol.iterator property of arrays, strings and maps in JavaScript is
what makes it possible for developers to use the for…of syntax to iterate
over them.
Implementing the Iterator Pattern on a JavaScript Object
One of the amazing things about the iteration protocol in JavaScript is that the pattern can be applied to other data structures. If an object is created and
- it has the Symbol.iterator property which when called, returns an object with a next method and
- this next method in turn returns an object with a done and/or value property and
- the done and value properties align with the demands of the iterator pattern
then that object is an iterable.
In the following code snippet, we try to make a non-array object an iterable.
const obj = {
// A Symbol.iterator property that is a function
[Symbol.iterator]: function () {
let num = 0;
// The function returns an object with a function called `next`
return {
next: function () {
num = num + 1;
// `next` returns an object with the fields `done` and/or `value` when called
return {
// both `done` and `value` are optional, but one of them must be returned
done: num > 3 ? true : false,
value: num > 3 ? undefined : num,
};
},
};
},
};
// 1. using the iterator (for...of)
for (const num of obj) {
console.log(num); // 1 2 3
}
// 2. using the spread operator
console.log(...obj); // 1 2 3
// 3. using the iterator object
const iterator = obj[Symbol.iterator]()
while(true)){
const iteratorResult = iterator.next()
console.log(iteratorResult.value) // 1 2 3
if(iterator.done == false) break;
}
The object obj
created is not an array, but because we have implemented the
iterator pattern on it, we can iterate over it using for…of and also use the
spread operator on it, making it an iterable. This would not be possible if we
had not implemented the iterator pattern on it. These contructs (for…of and
the spread operator) cause JavaScript to call the next method on the iterable
continually (internally, as we don’t see the explicit call) until the method
returns a value of false
for the done
field. For each call to next, the
value of value
is returned to the client and we see it in logged.
With the iterator pattern implemented, we can use the for…of or the iterator object itself to itereate over a range of values in a collection. The algorithm with which the iterator uses to return values is left to the implementor of the pattern. The main concern is having a consistent iterface that clients can rely on to retrieve values from the collection while traversing it.
Blog post image - Business illustrations by Storyset