Encapsulation Part 1: Overview

Encapsulation is one of the four major principles of object-oriented programming (the others are abstraction, inheritance and polymorphism which I might cover in a later post). The basic idea behind encapsulation is to provide access to data while still hiding or protecting it. In this series of blog posts, I’ll show how this is done; this post will describe the basic idea and provide an example, next post will have a more generic collection and a variant with pagination and the third post in this series will show a clever way of encapsulating objects.

Before I show how this is done, a sensible question might be:

Why would anyone want to do this?

Someone asking a sensible question

The main reason is to protect data but the technique also provides a couple of other advantages. By “protect the data” I mean that no-one can alter it without the developer knowing about it. So, for example, you could have a number in your code, but as a normal number, it can be easily corrupted or increased by too much. A simple object to protect the number might look something like this:

function makeCounter() {

    'use strict';

    var number = 0;

    return {

        increase: function () {
            number += 1;
        },

        decrease: function () {
            number -= 1;
        },

        getNumber: function () {
            return number;
        }

    };

}

This is essentially the same as the privilaged method)

Now your number can be increased or decreased by one but not two and it can’t be accidentally set to a string. You can do the same thing with any data structure: booleans, strings … or arrays …

Consider the following challenge: you need to keep track of the scores from a game, at any time you will need to know the highest, lowest and average score. A naive programmer might think:

I’ll just keep the scores in an array!

Naive programmer

This is the core of the solution, but not the complete one. The naive programmer is probably thinking of code like this:

var scores = [];

// As a result is found
scores.push(result);

This is a logical approach, but the scores variable is exposed. We can easily change it (and by “we” I mean some other developer who didn’t realise that they weren’t supposed to change it). Let’s assume that the other developer wants the most recent result for some reason, but accidently writes this:

// scores -> [100, 200, 150, 300]
var mostRecent = scores.pop();
// mostRecent -> 300
// scores -> [100, 200, 150]

This changes the maximum and the average and removes a result from the scores – that wasn’t supposed to happen. Since you (the original developer) weren’t expecting the length of the scores to reduce, it’s possible that there’s code in the game that assumes the number of results will never reduce. Here’s another frustratingly common scenario:

scores.push('300');

Now the scores contains a string when it was supposed to only contain numbers. In JavaScript, the plus operator (+) is overloaded so it will add numbers and concatenate strings. This isn’t a problem if you know you’re always dealing with one or the other, but can you imagine trying to work out the average by adding together the values before dividing by the number of results?

// scores -> [100, 200, 150, '300']

function deriveAverage() {

    var total = 0;
    var average = 0;

    if (scores.length) {

        scores.forEach(function (result) {
            total += result;
        });

        average = total / scores.length;

    }

    return average;

}

var average = deriveAverage();
// Expected: 187.5
// Actual: 112575

Would your code be smart enough to notice that? the deriveAverage function is still returning number, it’s just considerably different from our expectations. Sure, you could coerce the results in deriveAverage so they’re always numbers, you could check that the average is between the maximum and minimum scores, but doesn’t that sound messy to you?

Encapsulation to the Rescue

Remember the first paragraph of this post? We can protect data from unwanted tinkering and accidental manipulation by encapsulating it. Here’s a better solution that will handle all the problems I highlighted. We start with a constructor function to make our new data structure and we save a few variables that we need to be able to access at any moment.

function makeScoreCard() {

    'use strict';

    var scoreCard = {};
    var scores = [];
    var max = -Infinity;
    var min = Infinity;
    var average = 0;

    // ...

    return scoreCard;

}

You can already see how some of the methods will look:

function makeScoreCard() {

    // ...

    function getMax() {
        return max;
    }

    function getMin() {
        return min;
    }

    function getAverage() {
        return average;
    }

    // ...

    util.Object.assign(scoreCard, {
        getMax,
        getMin,
        getAverage
    });

    // ...

}

Huh? Oh – I’ll explain util.Object.assign in a moment.

We should also expose the scores so that the other developer can get the most recent score and we should be able to tell how many scores we have.

function makeScoreCard() {

    // ...

    function getSize() {
        return scores.length;
    }

    // A shallow copy of an array - in the third part of this series I'll show
    // you a function that creates a deep copy of an array or an object.
    function getScores() {
        return [].concat(scores);
    }

    // ...

    util.Object.assign(scoreCard, {
        // ...
        getSize,
        getScores
    });

    // ...

}

Our score card now allows the other developer to write his code without breaking yours:

var scores = makeScoreCard();
// I'll explain addScore a little lower down.
scores.addScore(100).addScore(200).addScore(150).addScore(300);
scores.getSize(); // -> 4
scores.getScores(); // -> [100, 200, 150, 300]

// Now for that other developer's ... *ahem* ... "contribution":
var mostRecent = scores.getScores().pop();
// mostRecent -> 300
// scores.getSize() -> 4
// scores.getScores() -> [100, 200, 150, 300]

It’s not as good as protecting stupid people from themselves, but with the simple technique of encapsulation, we’ve managed to protect our application from a stupid developer. Now the bit you’ve been waiting for: the addScore method. In it’s most simple form, we just need to add the score (and return the instance to allow for chaining).

function makeScoreCard() {

    // ...

    function addScore(score) {

        scores.push(score);

        return scoreCard;

    }

    // ...

    util.Object.assign(scoreCard, {
        // ...
        addScore
    });

    // ...

}

… but our addScore method should do more. We should prevent a string getting passed in and calculate the other variables that we may need at any time. Since some of the functions to do the calculations are very generic, I’m going to create a util object to contain those. (I’ll be re-using the util object in the rest of the series so watch out for it).

var util = (function () {

    'use strict';

    var utilities = {
        Number: {},
        Object: {}
    };

    var assign = Object.assign || function (source, ...objects) {

        objects.forEach(function (object) {

            Object.keys(object).forEach(function (key) {
                source[key] = object[key];
            });

        });

        return source;

    };

    function isNumeric(number) {
        return !isNaN(parseFloat(number)) && isFinite(number);
    }


    function sum(...numbers) {

        return numbers.reduce(function (prev, curr) {
            return prev + curr;
        }, 0);

    }

    function average(...numbers) {

        return numbers.length
            ? sum(...numbers) / numbers.length
            : 0;

    }

    assign(utilities.Number, {
        average,
        isNumeric,
        sum
    });

    assign(utilities.Object, {
        assign
    });

    return utilities;

}());

function makeScoreCard() {

    // ...

    function addScore(score) {

        var value = +score;

        if (util.Number.isNumeric(value)) {

            scores.push(value);
            max = Math.max(max, value);
            min = Math.min(min, value);
            average = util.Number.average(...scores);

        }

        return scoreCard;

    }

    // ...

}

You can probably see why we initially set max and min to -Infinity and Infinity now – an initial score of 0 would set both of them.

Going Further

Now we have a perfectly valid score card … or do we? Does it do everything you need it to do? At the moment, we ignore non-numeric values, but you might need to throw an error instead:

function makeScoreCard() {

    // ...

    function addScore(score) {

        var value = +score;
        
        if (util.Number.isNumeric(value)) {
            // ...
        } else {

            throw new TypeError('addScore expects a numeric value, given ' +
                score);

        }

        return scoreCard;

    }

    // ...

}

At the moment we accept any number, but you might need to fix the scores so that they’re at 4 decimal places:

var util = (function () {

    // ...

    function toPosInt(number) {
        return Math.abs(Math.floor(number));
    }

    function precisionateNumber(number, precision, method) {

        var places = toPosInt(precision) || 0,
            coefficient = Math.pow(10, places);

        if (typeof Math[method] !== 'function') {
            method = 'round';
        }

        return Math[method](number * coefficient) / coefficient;

    }

    function round(number, precision) {
        return precisionateNumber(number, precision, 'round');
    }

    function floor(number, precision) {
        return precisionateNumber(number, precision, 'floor');
    }

    function ceil(number, precision) {
        return precisionateNumber(number, precision, 'ceil');
    }

    // ...

    assign(utilities.Number, {
        // ...
        toPosInt,
        round,
        floor,
        ceil
    });

    // ...

}());

function makeScoreCard() {

    // ...

    function addScore(score) {

        var value = util.Number.round(score, 4);

        if (!isNaN(value)) {
            // ...
        }

        return scoreCard;

    }

    // ...

}

If that’s something you want to do, I’d recommend that you have the number of decimal places set as a configuration value.

function makeScoreCard(config) {

    // ...

    var settings = {
        dp: 0
    };

    // ...

    function addScore(score) {

        var value = util.Number.round(score, settings.dp);

        // ...

    }

    // ...

    if (config && util.Number.isNumeric(configconfig.dp)) {
        settings.dp = util.Number.toPosInt(config.dp);
    }

    // ...

}

Maybe you know that points in your game are always given in multiples of 10 so you want to discard scores that don’t match:

function makeScoreCard() {

    // ...

    function addScore(score) {

        var value = +score;

        if (util.Number.isNumberic(value) && value % 10 === 0) {
            // ...
        }

        return scoreCard;

    }

    // ...

}

The possibilities are endless and you can easily tweak the data structure to handle your specific requirements without having to modify the code for the game itself. Your scores are still protected, no-one can affect them without using one of your methods (or writing a new one) and stupid developers can’t break your game (well … they can’t break your score card, the game itself … stupid developers always seem to find a way).

Final Words

Encapsulation is one of the easiest of the four major principles to understand, but it’s often forgotten in JavaScript. I hope that this post has given you some inspiration for how to use it in your own projects.

Next time I’ll show a more generic collection and how to extend it to add pagination functionality. All the code that I’ve written here is up on GitHub so you can see the complete code and fork away if you want to play with any examples.

Leave a Reply

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