Encapsulation Part 3: Objects

So far I’ve covered encapsulating arrays but objects are a far more common data type to encapsulate. The basic idea is to allow properties to be set, accessed, checked and removed without giving direct access to the object itself. You’ve almost certainly seen structures like this before. For consistency, setting will return the instance and deleting will return a boolean depending on success. I’m also re-using the util object from the previous posts (Overview and Collections), so check them if you don’t recognise some of the methods or variables.

First, some new utility methods:

var util = (function () {

    // ...

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

    // ...

    function owns(object, property) {

        return object
            ? Object.prototype.hasOwnProperty.call(object, property)
            : false;

    }

    // Inspired by jQuery.isPlainObject()
    function isPlainObject(object) {

        var isPlain = object !== null && typeof object === 'object' &&
            object !== window && !object.nodeType;

        if (isPlain) {

            try {

                if (!object.constructor ||
                        !owns(object.constructor.prototype, 'isPrototypeOf')) {
                    isPlain = false;
                }

            } catch(ignore) {
            }

        }

        return isPlain;

    }

    // ...

    assign(utilities.Object, {
        // ...
        isPlainObject,
        owns
    });

    // ...

}());

… then the constructor function:

function makeAccess(initial) {

    'use strict';

    var access = {};
    var data = {};

    function setData(property, value) {

        if (typeof property === 'string') {
            data[property] = value;
        }

        return access;

    }

    function getData(property) {
        return data[property];
    }

    function hasData(property) {
        return util.Object.owns(data, property);
    }

    function deleteData(property) {

        var had = hadData(property);

        delete data[property];

        return had;

    }

    function addData(additional) {
        util.Object.assign(data, additional);
    }

    util.Object.assign(access, {
        setData,
        getData,
        hasData,
        deleteData
    });

    if (util.Object.isPlainObject(initial)) {
        addData(initial);
    }

    return access;

}

Pretty straight-forward, huh? We’re not done yet. Remember last time when I said that it’s always worth adding a way of exposing a copy of the underlying data structure? That means adding a way to clone an object. Now, I know what you’re thinking:

util.Object.assign({}, data);

Your thoughts

… but that only works with shallow objects – any nested objects are simply referenced so that will break our attempts to protect the data. We need a much better way of cloning an object (and if you’ve been following this series of posts, you’ve probably been waiting for this method). It can take a lot of code to correctly clone an object, but we already have a most of it in the other util methods.

var util = (function () {

    // ...

    function clone(source) {

        var copy = {};

        Object.getOwnPropertyNames(source).forEach(function (property) {

            var orig = source[property];
            var desc = Object.getOwnPropertyDescriptor(source, property);
            var value = (isArrayLike(orig) || isPlainObject(orig))
                ? clone(orig)
                : orig;

            Object.defineProperty(copy, property, assign(desc, {value}));

        });

        return isArrayLike(source)
            ? arrayFrom(copy)
            : copy;

    }

    // ...

    assign(utilities.Object, {
        // ...
        clone
    });

    // ...

}());

function makeAccess(initial) {

    // ...

    function toObject() {
        return util.Object.clone(data);
    }

    util.Object.assign(access, {
        // ...
        toObject
    });

    // ...

}

Before you ask: yes – util.Object.clone will correctly copy deeply nested arrays as well as objects.

Magic Methods

Since the property argument in setData has to always be a string and properties can be dynamically generated in JavaScript, we can create magic methods to allow users to access the data in a more interesting way. This technique has a couple of other advantages that I’ll explain in a moment but right now I’ll show how to do it. There are two ways to create the methods: manually and automatically. Manual creation sucks (and has a major flaw), but I’ll show you how to do it so you get the idea of it.

var util = (function () {

    // ...

    var utilities = {
        // ...
        RegExp: {},
        String: {}
    };

    // ...

    var escapeRegExp = function (string) {

        string = String(string);

        return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');

    };

    // ...

    function camlise(string, hyphens) {

        var str = String(string);
        var chars = typeof hyphens === 'string'
            ? hyphens
            : '-_';
        var breaker = new RegExp('[' + escapeRegExp(chars) + ']([a-z])', 'gi');

        return string.replace(breaker, function (ignore, start) {
            return start.toUpperCase();
        });

    }

    function toUpperFirst(str) {

        var string = String(str);

        return string.charAt(0).toUpperCase() + string.slice(1);

    }

    // ...

    assign(utilities.RegExp, {
        escape: escapeRegExp
    });

    assign(utilities.String, {
        camlise,
        toUpperFirst
    });

    // ...

}());

function makeAccess(initial) {

    // ...

    function methodise(property) {
        return util.String.toUpperFirst(util.String.camelise(property));
    }

    function setData(property, value) {

        var method;

        if (typeof property === 'string' && property.toLowerCase() !== 'data') {

            data[property] = value;
            method = methodise(property);

            access['set' + method] = function (value) {
                return setData(property, value);
            };

            access['get' + method] = function (value) {
                return getData(property, value);
            };

            access['has' + method] = function (value) {
                return hasData(property, value);
            };

            access['delete' + method] = function (value) {
                return deleteData(property, value);
            };

        }

        return access;

    }

    // ...

}

Now whenever data is added, 4 methods are created so that the developer can use them.

var access = makeAccess({
    one: 1,
    two_three: 23
});

access.getOne(); // -> 1
access.getTwoThree(); // -> 23

The major flaw with doing it manually is that the methods don’t exist until data is added (and whoever writes the data structure might chose to delete the methods when data is removed to keep the object tidy). This can be problematic – consider this example:

if (!access.hasFour()) {
    access.setFour(4);
}

In that case, the first line would throw a TypeError because hasFour does not exist so we’d be attempting to execute undefined. If we could get past that, setFour doesn’t exist either (although that would create hasFour for us). We can’t predict what properties a developer may wish to add to their access object and the methods aren’t exactly “magic” either. Luckily, there is a better solution (although not all browsers recognise it yet): the Proxy Object. Using the Proxy Object we can check the properties that the developer is trying to access and return them something useful (like the results of one of our existing *Data methods). This is the previous technique, but from the other direction. Therefore, we need to “hyphenate” rather than “camelise”. I’ll heavily comment the function in the Proxy Object in case you’re not familiar with it.

var util = (function () {

    // ...

    function hyphenate(str, hyphen) {

        str = String(str);

        if (typeof hyphen !== 'string') {
            hyphen = '-';
        }

        return str.replace(/([a-z])([A-Z])/g, function (ignore, lower, upper) {
            return lower + hyphen + upper.toLowerCase();
        });

    }

    function toLowerFirst(str) {

        var string = String(str);

        return string.charAt(0).toLowerCase() + string.slice(1);

    }

    // ...

    assign(utilities.String, {
        hyphenate,
        toUpperFirst
    });

    // ...

}());

function makeAccess(initial) {

    // ...

    function decamelise(string) {
        return util.String.hyphenate(util.String.toLowerFirst(string), '_');
    }

    // ...

    access = new Proxy(access, {

        get: function (target, name) {

            var value;

            // `rule` contains an array of matches for the camelCase method.
            // "setAlphaBravo" becomes ["setAlphaBravo", "set", "AlphaBravo"]

            var rule = name.match(/^([a-z]+)(\w+)/);
            var property;

            // Very important is this first check. If the property already
            // exists then simply return it. This allows us to cache methods to
            // save having to work them out multiple times and allows us to
            // define our own methods - this technique's most powerful feature.
            // We also don't need to check that the property isn't "data"
            // because we won't override those methods.

            if (util.Object.owns(target, name)) {

                value = target[name];

            // Our next check ensures that we've got the kind of method name
            // that we're expecting. If not, these if statements don't pass and
            // we just return `undefined` (like any other JavaScript object
            // would).

            } else if (rule && rule.length) {

                property = decamelise(rule[2]);

                switch (rule[1]) {

                case 'get':

                    target[name] = function () {
                        return target.getData(property);
                    };

                    break;

                // For consistency with `setData`, `setAlphaBravo` should return
                // the instance ...

                case 'set':

                    target[name] = function (val) {
                        return target.setData(property, val);
                    };

                    break;

                case 'has':

                    target[name] = function () {
                        return target.hasData(property);
                    };

                    break;

                // ... and `deleteAlphaBravo` should return a boolean.

                case 'delete':

                    target[name] = function () {
                        return target.deleteData(property);
                    };

                    break;

                }

                value = target[name];                

            }

            return value;

        }

    });

    // ...

}

Advantages

With the Proxy Object in our toolbox, we gain a a fair few advantages.

Dynamic Methods

The methods we’re executing don’t need to exist until we try to access them. Not only does this keep the object clean (if you care about such things) but it allows us to check for data that hasn’t been set yet.

var access = makeAccess({
    one: 1,
    two_three: 23
});

access.getOne(); // -> 1
access.getTwoThree(); // -> 23

if (!access.hasFour()) {
    access.setFour(4);
}

access.toObject(); // -> {one: 1, two_three: 23, four: 4}

Access Dynamic Data

As mentioned in the Proxy Object comments, we no longer have to prevent the developer adding a property called “data” – the property can be added and the getData method won’t be overridden. This is actually true of any method, which allows us to define our own method and add functionality behind-the-scenes.

var person = makeAccess();

person.getFullName = function () {
    return person.getFirstName() + ' '  + person.getLastName();
};

person.getLastNameLength = function () {
    return (person.getLastName() || '').length;
};

person.setFirstName('John').setLastName('Smith');
person.getFullName(); // -> "John Smith"
person.getLastNameLength(); // -> 5

In this example, we don’t have “full_name” or “last_name_length” properties and we don’t want to, since other methods can affect them. We can access dynamic data so that isn’t a problem.

We only take advantage of our *Data method wrappers when the property that a developer is trying to access starts with “set”, “get”, “has” or “delete” – any other property will be treated as normal.

person.age; // -> undefined
person.age = 30;
person.age; // -> 30

Validate Incoming Data

Because we can set the methods on the fly, we can take advantage of the validation tricks we used in the score card example:

person.setAge = function (age) {

    if (util.Number.isNumeric(age)) {
        person.setData('age', +age);
    }

};

person.setAge('30');
person.getAge(); // -> 30

Disadvantages

That’s right – any technique that’s got that many positives must have a few negatives (welcome to web development). I can only really think of four downsides:

  • Limited browser support.
  • Additional overhead.
  • Hard to debug.
  • Hard to document.

Limited Browser Support

I mentioned this at the beginning but there are very few browsers that understand this functionality (at time of writing, only Firefox and IE Edge). That’s the kind of thing that will improve as time goes on, but right now, it’s not cross-browser. To make matters worse, it’s also not possible to pollyfill Proxy. This is probably the biggest limitation to using this technique.

Additional Overhead

Calling a function rather than accessing a property will always be more expensive. We also have to execute a function that runs a regular expression match every time we want to access a property (even if the property already exists). If you’ve been paying attention to my coding style throughout this series, you’ll see that such overheads aren’t something that I worry about. However, there are some situations where every process counts and efficiency is key; this technique will hinder that efficiency.

Hard to Debug

This is a real problem if someone isn’t used to this technique of data access. When trying to work out what the getLastName function returns or where it’s created, most developers will start by searching for “getLastName” which will only exist when returning a value. It can also be confusing when someone does recognise the technique, they may not realise that getLastNameLength is a special method rather than a magic one. Also, due to the power of JavaScript, a special method can be added at any point, further confusing the matter.

Hard to Document

JSDoc is a common documenting program used with JavaScript. You’ve probably seen the style before.

/**
 * Creates a new Something.
 * 
 * @constructor Something
 */
function Something() {

    /**
     * @private
     * @type {Number} Internal number.
     */
    this._number = 0;

}

Something.prototype = {

    /**
     * Public access to the "_number" property.
     * 
     * @return {Number} Internal number
     */
    getNumber: function () {
        return this._number;
    }

};

There are a few of these documenters around for various other languages. phpdoc documents PHP code, for example. However, where phpdoc has @property-read and @property-write (which aren’t really fit for this purpose), JSDoc has no such equivalent. If your IDE has auto-completion, it will struggle to detect the magic methods … unless you fancy writing documentation comments like this:

/**
 * @method setFirstName
 * @memberof person
 * @param {String}
 */
/**
 * @method setLastName
 * @memberof person
 * @param {String}
 */
/**
 * @method getFullName
 * @memberof person
 * @return {String}
 */
/**
 * @method getLastNameLength
 * @memberof person
 * @return {Number}
 */
/**
 * @method setAge
 * @memberof person
 * @param {String|Number}
 */
/**
 * @method getAge
 * @memberof person
 * @return {Number}
 */
var person = makeAccess();

… and those are just the methods that I used in this post – you’d probably need to document them all.

Personally, I don’t use JSDoc, I prefer NDoc but I have to admit that NDoc doesn’t have a way to document magic methods either. I’d love to know if there is a documentation language that does but I can’t see it happening.

Final Words

I hope you’ve enjoyed my brief look into encapsulation and I hope it’s given you some inspiration or more knowledge of programming techniques. I’ve been keeping my GitHub repository up-to-date so code for all the examples can be found there if you would like to see the code collated (or the documentation for any of the util methods). I’ve also got a version of the magic methods that work with the prototype property so the methods can be passed down through inheritance.

Leave a Reply

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