Прототипическое OO в JavaScript

TL; ДР:

Нужны ли нам фабрики/конструкторы в прототипическом OO? Можем ли мы сделать переключатель парадигмы и полностью отказаться от них?

BackStory:

В последнее время я занимался разработкой прототипического OO в JavaScript и обнаружил, что 99% OO, выполненного в JavaScript, заставляют в него классические OO-шаблоны.

Мое взятие прототипического ОО состоит в том, что оно включает в себя две вещи. Статический прототип методов (и статических данных) и привязка данных. Нам не нужны фабрики или конструкторы.

В JavaScript это объектные литералы, содержащие функции и Object.create.

Это означало бы, что мы можем моделировать все как статическую схему/прототип и абстракцию привязки данных, которая предпочтительно подключается прямо к базе данных в стиле документа. То есть объекты извлекаются из базы данных и создаются путем клонирования прототипа с данными. Это означало бы, что нет логики конструктора, нет фабрик, нет new.

Пример кода:

Псевдо пример:

var Entity = Object.create(EventEmitter, {
    addComponent: {
        value: function _addComponent(component) {
            if (this[component.type] !== undefined) {
                this.removeComponent(this[component.type]);
            }

            _.each(_.functions(component), (function _bind(f) {
                component[f] = component[f].bind(this);
            }).bind(this));

            component.bindEvents();

            Object.defineProperty(this, component.type, {
                value: component,
                configurable: true
            });

            this.emit("component:add", this, component);
        }
    },
    removeComponent: {
        value: function _removeComponent(component) {
            component = component.type || component;

            delete this[component];

            this.emit("component:remove", this, component);
        }
    }
}

var entity = Object.create(Entity, toProperties(jsonStore.get(id)))

Небольшое объяснение:

Конкретный код является подробным, поскольку ES5 является подробным. Entity выше - проект/прототип. Любой фактический объект с данными будет создан с помощью Object.create(Entity, {...}).

Фактические данные (в этом случае компоненты) непосредственно загружаются из магазина JSON и вводятся непосредственно в вызов Object.create. Конечно, аналогичная модель применяется для создания компонентов, и только базы данных, которые проходят Object.hasOwnProperty, хранятся в базе данных.

Когда объект создается впервые, он создается с пустым {}

Актуальные вопросы:

Теперь мои актуальные вопросы

  • Примеры с открытым исходным кодом прототипического ОО JS?
  • Это хорошая идея?
  • Согласован ли он с идеями и концепциями прототипического ООП?
  • Не будет ли использовать какие-либо конструкторы / factory функции где-нибудь укусить меня? Можем ли мы действительно уйти от использования конструкторов. Существуют ли какие-либо ограничения с использованием вышеуказанной методологии, где нам нужны фабрики для их преодоления.

Ответ 1

В соответствии с вашим комментарием, что вопрос в основном "является ли необходимым знание конструктора?" Я чувствую, что это так.

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

Aahh, но вы всегда можете просто вставить это в статический прототип: Object.create({name:'Bob', clicks:0}); Конечно, в этом случае. Но что, если сначала значение не всегда было 0, а скорее требовалось вычисление. Uummmm, скажем, возраст пользователей в секундах (при условии, что мы сохранили имя и DOB). Опять же, элемент, который мало используется, сохраняется, так как в любом случае его нужно будет пересчитать при извлечении. Итак, как вы сохраняете возраст пользователя в статическом прототипе?

Очевидным ответом является логика конструктора/инициализатора.

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

UPDATE

Хорошо, я собираюсь попробовать более реальный пример, и, чтобы избежать путаницы, предположим, что у меня нет базы данных и не нужно сохранять какие-либо данные. Скажем, я делаю пасьянс. Каждая новая игра будет (естественно) новым экземпляром прототипа Game. Мне ясно, что их логика инициализации требуется здесь (и много ее):

Например, мне понадобится на каждом игровом экземпляре не только статическая/жестко закодированная колода карт, но и случайная перетасованная колода. Если бы он был статичным, пользователь играл бы ту же игру каждый раз, что явно не хорошо.

Мне также может понадобиться запустить таймер, чтобы закончить игру, если у игрока закончится игра. Опять же, не то, что может быть статичным, так как в моей игре есть несколько требований: количество секунд обратно связано с количеством игр, которые подключенный игрок выиграл до сих пор (опять же, никакой сохраненной информации, сколько всего для этого соединения), и пропорционален сложности перетасовки (есть алгоритм, который согласно результатам перетасовки может определять степень сложности игры).

Как вы это делаете со статическим Object.create()?

Ответ 2

Я не думаю, что логика конструктора / factory необходима вообще, если вы измените то, как вы думаете об объектно-ориентированном программировании. В моем недавнем исследовании этой темы я обнаружил, что прототипное наследование дает больше возможностей для определения набора функций, которые используют конкретные данные. Это не внешняя концепция для тех, кто обучен классическому наследованию, но проблема заключается в том, что эти "родительские" объекты не определяют данные, которые будут использоваться.

var animal = {
    walk: function()
    {
        var i = 0,
            s = '';
        for (; i < this.legs; i++)
        {
            s += 'step ';
        }

        console.log(s);
    },
    speak: function()
    {
        console.log(this.favoriteWord);
    }
}

var myLion = Object.create(animal);
myLion.legs = 4;
myLion.favoriteWord = 'woof';

Итак, в приведенном выше примере мы создаем функциональность, которая идет вместе с животным, а затем создаем объект, который имеет эту функциональность, вместе с данными, необходимыми для завершения действий. Это неудобно и странно для любого, кто привык к классическому наследованию в течение какого-то времени. Он не имеет ни одной теплой нечеткости публичной/частной/защищенной иерархии видимости членов, и я буду первым, кто признает, что это заставляет меня нервничать.

Кроме того, мой первый инстинкт, когда я вижу вышеупомянутую инициализацию объекта myLion, заключается в создании factory для животных, поэтому я могу создавать львов, тигров и медведей (о мой) с помощью простой функции вызов. И, я думаю, что естественный образ мышления для большинства программистов - многословие вышеприведенного кода уродливо и, кажется, лишено элегантности. Я не решил, просто ли это из-за классической подготовки, или это фактическая ошибка вышеупомянутого метода.

Теперь, к наследованию.

Я всегда понимал, что наследование в JavaScript является сложным. Навигация внутри и снаружи прототипной цепи не совсем ясен. Пока вы не используете его с помощью Object.create, который берет на себя все функциональное перенаправление с новым ключом из уравнения.

Предположим, что мы хотели расширить этот объект animal и сделать человека.

var human = Object.create(animal)
human.think = function()
{
    console.log('Hmmmm...');
}

var myHuman = Object.create(human);
myHuman.legs = 2;
myHuman.favoriteWord = 'Hello';

Это создает объект, который имеет human как прототип, который, в свою очередь, имеет animal в качестве прототипа. Достаточно легко. Нет неправильного направления, нет "нового объекта с прототипом, равным прототипу функции". Простое прототипное наследование. Это просто и понятно. Полиморфизм тоже прост.

human.speak = function()
{
    console.log(this.favoriteWord + ', dudes');
}

В связи с тем, как работает цепочка прототипов, myHuman.speak будет найдена в human до того, как она будет найдена в animal, и, таким образом, наш человек является серфингом, а не просто скучным старым животным.

Итак, в заключение (TL;DR):

Функциональность псевдоклассического конструктора была скорее привязана к JavaScript, чтобы сделать этих программистов обученными в классическом ООП более комфортным. Это никоим образом не нужно, но это означает отказ от классических понятий, таких как видимость элементов и (тавтологически) конструкторов.

В результате вы получаете гибкость и простоту. Вы можете создавать "классы" на лету - каждый объект, сам, шаблон для других объектов. Установка значений для дочерних объектов не повлияет на прототип этих объектов (т.е. Если я использовал var child = Object.create(myHuman), а затем установил child.walk = 'not yet', animal.walk не был бы затронут - действительно, проверьте его).

Простота наследования - честно ошеломляющая. Я много читал о наследовании в JavaScript и написал много строк кода, пытающихся понять это. Но это действительно сводится к тому, что объекты наследуются от других объектов. Это так просто, и все ключевое слово new делает путаницу, что вверх.

Эту гибкость трудно использовать в полной мере, и я уверен, что мне еще предстоит это сделать, но она есть, и это интересно для навигации. Я думаю, что большая часть причин, по которым он не использовался для большого проекта, состоит в том, что он просто не понимается так хорошо, как это могло бы быть, и, ИМХО, мы заперты в классические модели наследования, которые мы все узнали, когда мы преподавались С++, Java и т.д.

Edit

Я думаю, что я сделал довольно хороший аргумент против конструкторов. Но мой аргумент против фабрик нечеткий.

После дальнейшего созерцания, в течение которого я несколько раз набрасывался на обе стороны забора, я пришел к выводу, что фабрики также не нужны. Если animal (выше) была предоставлена ​​другая функция initialize, было бы тривиально создавать и инициализировать новый объект, который наследует от animal.

var myDog = Object.create(animal);
myDog.initialize(4, 'Meow');

Новый объект, инициализированный и готовый к использованию.

@Raynos - Ты полностью заманил меня на этом. Я должен готовиться к 5 дням, абсолютно ничего не делая.

Ответ 3

Пример статического клонирования "Тип":

var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Теперь мы можем клонировать этот тип в другом месте, да? Конечно, нам нужно использовать var myType = Object.Create(MyType), но потом мы закончили, да? Теперь мы можем просто myType.size, и это размер вещи. Или мы могли бы прочитать цвет или изменить его и т.д. Мы не создали конструктор или что-нибудь еще, не так ли?

Если вы сказали, что там нет конструктора, вы ошибаетесь. Позвольте мне показать вам, где находится конструктор:

// The following var definition is the constructor
var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Потому что мы уже ушли и создали все, что хотели, и мы уже определили все. Это все конструктор. Поэтому, даже если мы только клонируем/используем статические вещи (это то, что я вижу в приведенных выше фрагментах), у нас все еще есть конструктор. Просто статический конструктор. Определив тип, мы определили конструктор. Альтернативой является эта модель построения объекта:

var MyType = {}
MyType.size = Sizes.large

Но в конце концов вы захотите использовать Object.Create(MyType), и когда вы это сделаете, вы будете использовать статический объект для создания целевого объекта. И тогда он становится таким же, как в предыдущем примере.

Ответ 4

Короткий ответ на ваш вопрос "Нужны ли нам фабрики/конструкторы в прототипическом OO?" нет. Фабрики/Конструкторы обслуживают только одну цель: инициализировать вновь созданный объект (экземпляр) в конкретное состояние.

При этом часто используется, потому что некоторым объектам нужен какой-то код инициализации.

Позвольте использовать код объекта на основе компонента, который вы указали. Типичный объект - это просто набор компонентов и несколько свойств:

var BaseEntity = Object.create({},
{
    /* Collection of all the Entity components */
    components:
    {
        value: {}
    }

    /* Unique identifier for the entity instance */
    , id:
    {
        value: new Date().getTime()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    /* Use for debugging */
    , createdTime:
    {
        value: new Date()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    , removeComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }

    , addComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }
});

Теперь следующий код создаст новые объекты на основе "BaseEntity"

function CreateEntity()
{
    var obj = Object.create(BaseEntity);

    //Output the resulting object information for debugging
    console.log("[" + obj.id + "] " + obj.createdTime + "\n");

    return obj;
}

Достаточно прямо вперед, пока вы не перейдете к ссылке на свойства:

setTimeout(CreateEntity, 1000);
setTimeout(CreateEntity, 2000);
setTimeout(CreateEntity, 3000);

выходы:

[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)

Так почему это? Ответ прост: из-за наследования на основе прототипа. Когда мы создали объекты, не было никакого кода для установки свойств id и createdTime в фактическом экземпляре, как это обычно делается в конструкторах/фабриках. В результате при доступе к ресурсу он вытягивается из цепи прототипа, которая заканчивается как одно значение для всех объектов.

Аргумент к этому заключается в том, что Object.create() должен быть передан второй параметр для установки этих значений. Мой ответ был бы просто следующим: Разве это не так, как вызов конструктора или использование factory? Это просто другой способ установки состояния объекта.

Теперь с вашей реализацией, где вы обрабатываете (и по праву) все прототипы как набор статических методов и свойств, вы инициализируете объект, назначая значения свойств данным из источника данных. Он может не использовать new или некоторый тип factory, но это код инициализации.

Подводя итог: В прототипе прототипа ООП - new не требуется - Фабрики не нужны - Обычно требуется код инициализации, который обычно выполняется через new, фабрики или какую-либо другую реализацию, которую вы не хотите допускать, это инициализация объекта