Прототипическое 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
, фабрики или какую-либо другую реализацию, которую вы не хотите допускать, это инициализация объекта