Encapsulation Part 2: Collections

Last time I gave a brief overview of encapsulation and gave you some code to show how to use it to protect data (namely, the scores in your score card). In this post I want to show you a generic data structure for collections and also answer this statement that some of you may have thought:

How is that object oriented programming? It’s just a function returning an object.

Possibly some readers

Let’s start with the basics:

What is a Collection?

Essentially, a collection is an encapsulated array. Usually one can add items to them, remove items from the and iterate over them. Sometimes they can do more (or less) and they can be more specific to individual situations.

Now you know what a collection is, I’ll show you how to make a basic one.

A Basic Collection

As before, we have a function that returns an object and some private data that we need to protect (the array itself).

function makeCollection(initial) {

    'use strict';

    var collection = {},
        items = [];

    // ...

    return collection;

}

That initial argument will take an array and populate the hidden items array. To make this a little more interesting, I’m going to prevent the collection adding an item that has already been added. This isn’t a requirement and it’s easy enough to remove if it’s not something you want. For consistency with some other JavaScript data structures, addItem should return the instance so it can be chained and the removeItem method should return whether or not an item way removed. We need some more utility functions here and I’ll re-use the util object from the previous post to do that – check there if don’t recognise something or can’t tell where a variable was created.

function makeCollection(initial) {

    // ...

    function getIndex(item) {
        return items.indexOf(item);
    }

    function contains(item) {
        return getIndex(item) > -1;
    }

    function addItem(item) {

        if (!contains(item)) {
            items.push(item);
        }

        return collection;

    }

    function removeItem(item) {

        var index = getIndex(item);
        var exists = index > -1;

        if (exists) {
            items.splice(index, 1);
        }

        return exists;

    }

    function itemExists(index) {

        index = util.Number.toPosInt(index);

        return index >= 0 && index < items.length;

    }

    function getItem(index) {

        return itemExists(index)
            ? items[index]
            : undefined;

    }

    // ...

    util.Object.assign(collection, {
        contains,
        addItem,
        removeItem,
        getItem
    });

    // ...

}

We should also create a way to access the array. Generally speaking, when you create an encapsulated data structure based on another one, you should create a way of exposing the original data, allowing a developer to put the data into another structure if they need to. Always be sure to return a copy of your data rather than the original itself, so that it is still protected.

Also, while we're adding sensible functionality, we should do something with that initial argument and expose the size of the items array.

var util = (function () {

    // ...

    var utilities = {
        // ...
        Array: {}
    };

    // Firefox has `Array.forEach` and it works exactly the same as this
    // function. No reason not to use it, if we can.
    var forEach = Array.forEach || function (array, handler, context) {
        Array.prototype.forEach.call(array, handler, context);
    };

    // ...

    function isArrayLike(array) {
        return array !== undefined && array !== null && isNumeric(array.length);
    }

    // ...

    assign(utilities.Array, {
        forEach,
        isArrayLike
    });

    // ...

}());

function makeCollection(initial) {

    // ...

    // Still a shallow clone - in the next post in this series, I'll show you a
    // deep clone method.
    function toArray() {
        return [].concat(items);
    }

    function getSize() {
        return items.length;
    }

    // ...

    util.Object.assign(collection, {
        // ...
        toArray,
        getSize
    });

    // ...

    if (util.Array.isArrayLike(initial)) {
        util.Array.forEach(initial, addItem);
    }

    // ...

}

Iteration

Iterating over the collection should be a part of the code. The reason that this needs its own heading is that there are a couple of ways to do this. The most obvious thing to do is to create an interation method:

function makeCollection(initial) {

    // ...

    function each(handler, context) {

        items.some(function (item, i) {
            return handler.call(context, item, i) === false;
        });

    }

    // ...

    util.Object.assign(collection, {
        // ...
        each
    });

    // ...

}

This is a good idea, but developers usually want more. After all, we have for and while loops and this collection is just an array of items, why can't we use those loops? Well, the short answer is "it would expose the data". If that doesn't bother you, you can use the same trick that jQuery uses (although you wouldn't want an internal items array, you'd just iterate over the numeric properties of collection).

function makeCollection(initial) {

    // ...

    var push = Array.prototype.push;

    // ...

    function addItem(item) {

        if (!contains(item)) {
            push.call(collection, item);
        }

        return collection;

    }

    // ...

}

// This collection could now be used like this:

var collection = makeCollection(['zero', 'one', 'two']);
collection.length; // -> 3
collection[1]; // -> "one"

// ... however, it does cause these issues:

collection[1] = 'uno';
collection[1]; // -> "uno" - did we want to allow that to be changed?
collection.length = 2;
collection[2]; // -> "two" - an array would have dropped this item.
collection[3] = 'three';
collection[3]; // -> "three"
collection.length; // -> 2 - an array would have updated this value.

// ... this may be one of the reasons the jQuery object used to have a "size"
// method.

Luckily, there is a better solution: the iteration protocol. Not every browser understands this protocol yet, but it's very easy to test support and also easy to implement.

function makeCollection(initial) {

    // ...

    if (window.Symbol) {

        collection[Symbol.iterator] = function (index) {

            index = +index || 0;

            function next() {

                return index < items.length
                    ? {value: items[index]}
                    : {done: true};

            }

            return {
                next
            };

        };

    }

    // ...

}

The iteration protocol allows us to loop over the items using for ... of. It works the same way for our collection as it does for a normal array and it looks like this:

// Iterate over an array.
var array = ['zero', 'one', 'two'];
for (var arrayItem of array) {
    console.log(arrayItem);
}
// logs "zero"
// logs "one"
// logs "two"

// Iterate over the collection.
var collection = makeCollection(array);
for (var collectionItem of collection) {
    console.log(collectionItem);
}
// logs "zero"
// logs "one"
// logs "two"

Pagination

We might not want to paginate every collection, but there is an obvious advantage to being able to paginate some of them. This handles the second part of the introduction: how we can use the existing collection code as a base for a paginated version.

To create the pages, we need to be able to split an array into smaller arrays of a certain size. If we build a utility function to do that then the paginated collection methods mainly deal with selecting the correct page.

var util = (function () {

    // ...
    
    var utilities = {
        // ...
        Function: {}
    };

    // ...

    var identity = function (x) {
        return x;
    };

    var arrayFrom = Array.from || function (array, map, context) {

        if (typeof map !== 'function') {
            map = identity;
        }

        return Array.prototype.map.call(array, map, context);

    };

    // ...

    function chunk(array, size, map, context) {

        var chunked = [];
        var arr = arrayFrom(array, map, context);
        var i = 0;
        var il = arr.length;
        var amount = +size || 1;

        while (i < il) {

            chunked.push(arr.slice(i, i + amount));
            i += amount;

        }

        return chunked;

    }

    // ...

    assign(utilities.Array, {
        // ...
        chunk
    });

    assign(utilities.Function, {
        identity
    });

    // ...

}());

Now that we can break our items into smaller arrays, we need to be able to set a page size. Here you can see the basic steps of "functional inheritance": call the previous function, add methods and return the result.

function makePaginatedCollection(initial) {

    'use strict';

    var collection = makeCollection(initial);
    var pages = [];
    var pageSize = 1;

    function createPages() {

        pages = util.Array.chunk(collection.getItems(), pageSize)
            .map(makeCollection);

    }

    function setPageSize(size) {

        pageSize = util.Number.toPosInt(size) || 1;
        createPages();

    }

    function getPagesSize() {
        return pages.length;
    }

    util.Object.assign(collection, {
        createPages,
        setPageSize,
        getPagesSize
    });

    return collection;

}

The collection returned from makePaginatedCollection still has the methods from the makeCollection function which is usually pretty handy, although we do need to make a modification to the addItem and removeItem methods since we now need to re-create the pages when the items are modified. We should also update the iterator to loop over the pages rather than the items. This shows how to replace a method and create a method that calls the "super" method.

function makePaginatedCollection(initial) {

    // ...

    function addItem(item) {

        if (collection.contains(item)) {

            collection.addItem(item);
            createPages();

        }

        return collection;

    }

    function removeItem(item) {

        var removed = collection.removeItem(item);

        if (removed) {
            createPages();
        }

        return removed;

    }

    if (window.Symbol) {

        collection[Symbol.iterator] = function (index) {

            index = +index || 0;

            function next() {

                return index < pages.length
                    ? {value: pages[index]}
                    : {done: true};

            }

            return {
                next
            };

        };

    }

    util.Object.assign(collection, {
        // ...
        addItem,
        removeItem
    });

    // ...

}

Now we've got that covered, we need to add functionality to actually access the pages.

function makePaginatedCollection(initial) {

    // ...
    
    var currentPage = 0;

    // ...

    function getCurrentPageIndex() {
        return currentPage;
    }

    function getCurrentPage() {
        return pages[currentPage];
    }

    function getFirstPage() {

        currentPage = 0;

        return pages[currentPage];

    }

    function getLastPage() {

        currentPage = pages.length - 1;

        return pages[currentPage];

    }

    function getNextPage() {

        var index = currentPage + 1;

        if (index < getPagesSize()) {
            currentPage = index;
        }

        return pages[index];

    }

    function getPreviousPage() {

        var index = currentPage - 1;

        if (index >= 0) {
            currentPage = index;
        }

        return pages[index];

    }

    // ...

    util.Object.assign(collection, {
        // ...
        getCurrentPageIndex,
        getCurrentPage,
        getFirstPage,
        getLastPage,
        getNextPage,
        getPreviousPage
    });

    // ...

}

With this particular paginated collection, the get*Page methods update the internal currentPage variable so it's possible to always call getNextPage for example. Looping back to the start or end of the pages is done with the default operator. It looks a little like this:

var paged = makePaginatedCollection([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
paged.setPageSize(3);
paged.getCurrentPage(); // .getItems() = [0, 1, 2]
paged.getNextPage() || paged.getFirstPage(); // .getItems() = [3, 4, 5]
paged.getNextPage() || paged.getFirstPage(); // .getItems() = [6, 7, 8]
paged.getNextPage() || paged.getFirstPage(); // .getItems() = [9, 10]
paged.getNextPage() || paged.getFirstPage(); // .getItems() = [0, 1, 2]

With a few simple and easy-to-understand methods, the paginated data can be accessed.

Final Words

A basic collection can be a useful structure to implement in your application, but I tend to find that more specific data structures can be more useful (such as the score card I showed last time).

I've got all this code up on GitHub so you can see it all together. Next time I'll show you a cool technique to make an encapsulated object a little more exciting.

Leave a Reply

Your email address will not be published. Required fields are marked *