Another way to organize OOP in JS

Introduction


As recently stated in a publication in Honest Private Properties in a Prototype , there are two camps for JavaScript developers:
  • those that are willing to tolerate prefixes as a designation for hiding properties / methods;
  • those who are not ready to endure pseudo-encapsulation.


I relate to the second camp and solve the problem by declaring the entire class in its constructor, which allows using private / public in any combination with static .

For example, Computer.js :

(function (module) {
    'use strict';
    var privateStaticVariable = 1;
    function Computer() {
        var privateVariable = 5;
        this.publicVariable = 8;
        Computer.publicStaticVariable = 1;
        this.getAnswer = function () {
            return 
                privateStaticVariable +
                Computer.publicStaticVariable +
                this.publicVariable *
                privateVariable
            ;
        };
    }
    module.exports = Computer;
}(module));


Problem


Everything would be fine, but OOP is not limited to open / closed, therefore many goals were broken and quite a few ways to solve the problem of protected properties of classes were developed, most of which basically use conventions (again, back to pseudo-encapsulation and prefixes), a lot more and introduces its syntax. During the searches, we even managed to see a library using eval () at its core, but we know:
Eval is evil!
But strangely enough, the latter was the best (in its subjective opinion) of all those examined by the following criteria:
  • no agreements, real security;
  • no specific syntax, only native JS.

Study


After a short study of the source code, it was discovered that "security" was provided by moving the private code to the child class through regular expressions, .toSource () , eval () and magic. Naturally, this miracle of engineering could not work quickly, and donating private for protected is not very interesting.

Decision


First stage

It was decided to puzzle over what all classes of the hierarchy might have and at the same time be completely inaccessible to users of this hierarchy. On the third day, a thought struck his head, just before burying the whole undertaking and being content with what is: The constructor parameter !

Second phase

On this, the first part of the thought process was completed, the next step was to figure out how to hide this very additional argument from users of the code. And with this problem, language and creativity will help us:
  1. We save the class constructor.
  2. We rewrite the class constructor.
  3. Create a private variable (object).
  4. We use Function.prototype.bind.apply () on the saved constructor with the [null, savedPrivateObject] parameter .

But doing so many actions manually is a long time, and a good developer is a lazy developer ( ObjectOriented.js ):

(function () {
    /**
    * Inject protected-data object to class
    * @private
    * @param Class {Function} Class
    * @param protectedData {Object} Protected-data object
    * @return {Function} Result class
    */
    function injectProtected(Class, protectedData) {
        return (function (Native) {
            function Overridden() {
                var args = Array.prototype.map.call(arguments, function (value) { return [value]; });
                args.unshift(protectedData);
                args.unshift(null);
                return (new (Function.prototype.bind.apply(Native, args))());
            }
            Overridden.prototype = new Native({});
            return Overridden;
        }(Class));
    } 
}());

"What about inheritance?" Or the fourth stage

To solve this problem, first of all, we need functionality to determine whether our protected section is implemented, this is solved by adding a static method for the class, for example, isProtectedInjected () . Now that we can get information about the existence of an implementation, we can think about how to implement the same object in the class and all its descendants. To do this, we at least need to be able to get the original class before embedding a protected object in it, therefore we will add a static method that returns the original class. Inheritance itself is quite standard for JavaScript, with the exception of creating an additional protected data variable and injecting it into classes that require implementation.

The last stage or “add convenience”

It's time to bring the code that implements some of the OOP features to an object-oriented style. Add the integrated global class Function methods injectProtected () , extend () and stub methods isProtectedInjected , getNative () . This will help simplify your life, because after that, any class will have this set of functions.

Result


(function (Function) {
    'use strict';
    /**
     * Check if protected-data was injected
     * @returns {boolean}
     */
    Function.prototype.isProtectedInjected = function () {
        return false;
    };
    /**
     * Inject protected-data object to class
     * @private
     * @param Class {Function} Class
     * @param protectedData {Object} Protected-data object
     * @return {Function} Result class
     */
    function injectProtected(Class, protectedData) {
        return (function (Native) {
            function Overridden() {
                var args = Array.prototype.map.call(arguments, function (value) { return [value]; });
                args.unshift(protectedData);
                args.unshift(null);
                return (new (Function.prototype.bind.apply(Native, args))());
            }
            Overridden.prototype = new Native({});
            Overridden.getNative = function () {
                return Native;
            };
            Overridden.isProtectedInjected = function () {
                return true;
            };
            return Overridden;
        }(Class));
    }
    /**
     * Get native class without injection of protected
     * @returns {Function} Class
     */
    Function.prototype.getNative = function () {
        return this;
    };
    /**
     * Extend from @a ParentClass
     * @param {Function} ParentClass
     * @return {Function} Result class
     */
    Function.prototype.extend = function (ParentClass) {
        var protectedData = {},
            parent,
            me = this.getNative();
        if (ParentClass.isProtectedInjected()) {
            ParentClass = injectProtected(ParentClass.getNative(), protectedData);
        }
        parent = new ParentClass();
        me.prototype = parent;
        me.prototype.constructor = me;
        protectedData.parent = parent;
        if (me.isProtectedInjected()) {
            me = injectProtected(me, protectedData);
        }
        me.prototype = parent;
        me.prototype.constructor = me;
        return me;
    };
    /**
     * Injects protected-data object to class
     * @example
     *  function SomeClass(protectedData/\* , ... *\/) {
     *      protectedData.protectedMethod = function () {};
     *      protectedData.protectedVariable = 'Access only from children and self';
     *      /\* ...Realization... *\/
     *  }.injectProtected()
     * @returns {Function}
     */
    Function.prototype.injectProtected = function () {
        return injectProtected(this, {});
    };
}(Function));

Usage example


Computer.js :
(function (module) {
    'use strict';
    var privateStaticVariable = 1;
    function Computer(protectedData) {
        var privateVariable = 5;
        this.publicVariable = 8;
        protectedData.badModifier = 0.1;
        Computer.publicStaticVariable = 1;
        this.getAnswer = function () {
            return (
                privateStaticVariable +
                Computer.publicStaticVariable +
                this.publicVariable *
                privateVariable
            ) * protectedData.badModifier;
        };
    }
    module.exports = Computer.injectProtected(); // <- That's it!
}(module));

FixedComputer, js :
(function (module) {
    'use strict';
    var Computer = require('Computer');
    function FixedComputer(protectedData) {
        Computer.call(this); // Super analogue
        protectedData.badModifier = 1;
    }
    module.exports = FixedComputer.injectProtected().extend(Computer); // <- That's it!
}(module));

References


Libraries:

Also popular now: