Очередной способ организации ООП в JS

в 6:44, , рубрики: javascript, node.js, Веб-разработка, ооп, ооп js

Введение

Как недавно было сказано в публикации в «Честные приватные свойства в прототипе», существует два лагеря JavaScript-разработчиков:

  • те, что готовы терпеть префиксы, как обозначение сокрытия свойствметодов;
  • те, что не готовы терпеть псевдо-инкапсуляцию.

Я отношу себя ко второму лагерю и решаю проблему объявлением всего класса в его конструкторе, что позволяет использовать private/public в любой комбинации с static.

Например, 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));

Проблема

Всё бы хорошо, но ООП не ограничивается открытымзакрытым, посему поводу было сломано немало голов и разработано не мало способов решения проблемы защищённых свойств классов, большая часть которых в своей основе использует соглашения (опять возвращаемся к псевдо-инкапсуляции и префиксам), немалая ещё и вводит свой синтаксис. Во время поисков даже удалось увидеть библиотеку использующую eval() в своей основе, а мы знаем:

Eval is evil!

Но как ни странно, последняя была наилучшей (на свой субъективный взгляд) из всех рассмотренных по следующим критериям:

  • никаких соглашений, реальная защищённость;
  • никакого специфического синтаксиса, только нативный JS.

Исследование

После недолгого изучения исходников было обнаружено, что «защищённость» обеспечивалась перемещением приватного кода в дочерний класс через регулярные выражения, .toSource(), eval() и магию. Естественно быстро это чудо инженерной мысли работать не могло, да и жертвовать private ради protected не очень-то интересно.

Решение

Первый этап

Было решено поломать голову над тем, что может быть у всех классов иерархии и в то же время быть абсолютно недоступно пользователям этой иерархии. На третий день голову-таки озарила мысль, как раз перед тем, как хоронить всю затею и довольствоваться тем, что есть: Параметр конструктора!

Второй этап

На этом первая часть мыслительного процесса была завершена, следующим этапом было придумать как скрывать этот самый дополнительный аргумент от пользователей кода. И с этой проблемой нам поможет язык и креатив:

  1. Сохраняем конструктор класса.
  2. Перезаписываем конструктор класса.
  3. Создаём приватную переменную(объект).
  4. Используем Function.prototype.bind.apply() на сохранённом конструкторе с параметром [null, savedPrivateObject].

Но производить столько действий вручную — долго, а ведь хороший разработчик — ленивый-разработчик(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));
    } 
}());

«А как же наследование?» или четвёртый этап

Для решения этой проблемы прежде всего нужен функционал для определения внедрена ли наша защищённая секция, это решается добавлением статического метода для класса, например, isProtectedInjected(). Теперь, когда мы можем получить информацию о наличии внедрения, мы можем подумать о том, как внедрять один и тот же объект в класс и всех его наследников. Для этого нам как минимум нужно иметь возможность получать оригинальный класс до внедрения в него защищённого объекта, посему будем добавлять статический метод возвращающий оригинальный класс. Само же наследование вполне стандартно для JavaScript, за исключением создания дополнительной переменной защищённых данных и внедрения её в классы, требующие внедрения.

Последний этап или «добавляем удобства»

Самое время привести код реализующий некоторые возможности ООП к объектно-ориентированному стилю. Добавим глобальному встроенному классу Function методы injectProtected(), extend() и методы заглушки isProtectedInjected, getNative(). Это поможет упростить себе жизнь, т.к. после этого любой класс будет иметь этот набор функций.

Результат

(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));

Пример использования

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 = Computer.injectProtected().extend(Computer); // <- That's it!
}(module));

Ссылки

Библиотеки:

Автор: saksmt

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js