Прототип ОО в JavaScript
TL;DR:
Нужны ли фабрики / конструкторы в ОО? Можем ли мы сделать переключение парадигмы и полностью отбросить их?
BackStory:
В последнее время я занимался созданием прототипных ОО в JavaScript и обнаружил, что 99% ОО, выполняемых в JavaScript, навязывают ему классические ОО-шаблоны.
Мой подход к прототипу ОО состоит в том, что он включает в себя две вещи. Статический прототип методов (и статических данных) и привязка данных. Нам не нужны фабрики или конструкторы.
В 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-прототипа OO?
- Это хорошая идея?
- Соответствует ли это идеям и концепциям, заложенным в прототип ООП?
- Не будет ли использование каких-либо конструкторов / фабричных функций кусать меня в задницу где-нибудь? Можем ли мы сойти с рук, не используя конструкторы. Существуют ли какие-либо ограничения при использовании вышеуказанной методологии, когда нам потребуются фабрики для их преодоления.
4 ответа
Согласно вашему комментарию, вопрос в основном "нужны знания конструктора?" Я чувствую, что это так.
Игрушечный пример будет хранить частичные данные. На заданном наборе данных в памяти при сохранении я могу выбрать только сохранение определенных элементов (либо ради эффективности, либо в целях обеспечения согласованности данных, например, значения по сути бесполезны после их сохранения). Давайте возьмем сеанс, где я сохраняю имя пользователя и количество раз, когда они нажимали на кнопку справки (из-за отсутствия лучшего примера). Когда я сохраняю это в моем примере, я не использую количество кликов, так как я сохраняю его в памяти сейчас, и в следующий раз я загружаю данные (в следующий раз, когда пользователь входит в систему, подключается или что-то еще), я инициализирую значение с нуля (предположительно до 0). Этот конкретный вариант использования является хорошим кандидатом для логики конструктора.
Ааа, но вы всегда можете просто вставить это в статический прототип: Object.create({name:'Bob', clicks:0});
Конечно, в этом случае. Но что, если значение сначала не всегда равно 0, а скорее требует вычислений? Uummmm, скажем, пользователи стареют в секундах (при условии, что мы сохранили имя и DOB). Опять же, элемент, который мало используется, сохраняется, так как его все равно придется пересчитывать при извлечении. Итак, как вы храните возраст пользователя в статическом прототипе?
Очевидный ответ - логика конструктора / инициализатора.
Есть много других сценариев, хотя я не чувствую, что идея тесно связана с js oop или каким-либо конкретным языком. Необходимость логики создания сущностей присуща тому, как компьютерные системы моделируют мир. Иногда элементы, которые мы храним, будут простым извлечением и внедрением в схему, подобную оболочке прототипа, а иногда значения являются динамическими, и их необходимо будет инициализировать.
ОБНОВИТЬ
Хорошо, я собираюсь попробовать более реальный пример, и чтобы избежать путаницы, предположим, что у меня нет базы данных и мне не нужно сохранять какие-либо данные. Допустим, я делаю пасьянс-сервер. Каждая новая игра будет (естественно) новым экземпляром Game
прототип. Для меня ясно, что здесь требуется логика инициализатора (и многое из этого):
Например, в каждом игровом экземпляре мне понадобится не просто колода статических / жестко закодированных карт, а колода с произвольным перемешиванием. Если бы он был статичным, пользователь каждый раз играл бы в одну и ту же игру, что явно нехорошо.
Мне также может понадобиться запустить таймер, чтобы закончить игру, если игрок выбежал. Опять же, не то, что может быть статичным, так как моя игра имеет несколько требований: количество секунд обратно пропорционально зависит от количества игр, в которых выиграл подключенный игрок (опять же, нет сохраненной информации, только сколько для этого подключения) и пропорционально сложности перемешивания (существует алгоритм, который по результатам перемешивания может определять степень сложности игры).
Как вы делаете это со статическим Object.create()
?
Я не думаю, что логика конструктора / фабрики вообще необходима, если вы меняете свое отношение к объектно-ориентированному программированию. В моем недавнем исследовании этой темы я обнаружил, что прототипическое наследование больше подходит для определения набора функций, которые используют конкретные данные. Это не чуждое понятие для тех, кто обучен классическому наследованию, но загвоздка в том, что эти "родительские" объекты не определяют данные для обработки.
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
Цель состоит в том, чтобы создать фабрику для животных, чтобы я мог создавать львов, тигров и медведей (о мой) простым вызовом функции. И, я думаю, для большинства программистов это естественный способ мышления - многословность приведенного выше кода безобразна и, похоже, лишена элегантности. Я не решил, является ли это просто из-за классической тренировки, или это фактическая ошибка вышеупомянутого метода.
Теперь о наследовании.
Я всегда понимал наследование в 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
и, таким образом, наш человек - серфер, а не просто скучное старое животное.
Итак, в заключение (TLDR):
Функциональность псевдоклассического конструктора была добавлена в JavaScript, чтобы сделать программистов, обученных классическому ООП, более комфортными. Это ни в коем случае не является необходимым, но означает отказ от классических понятий, таких как видимость членов и (тавтологически) конструкторы.
В ответ вы получаете гибкость и простоту. Вы можете создавать "классы" на лету - каждый объект сам является шаблоном для других объектов. Установка значений для дочерних объектов не повлияет на прототип этих объектов (т.е. если я использовал var child = Object.create(myHuman)
, а затем установить child.walk = 'not yet'
, animal.walk
было бы незатронутым - действительно, проверьте это).
Простота наследования поистине ошеломляет. Я много читал о наследовании в JavaScript и написал много строк кода, пытаясь это понять. Но это действительно сводится к объектам, наследуемым от других объектов. Это так просто, и все new
Ключевое слово делает запутать это.
Эту гибкость трудно использовать в полной мере, и я уверен, что мне еще предстоит это сделать, но она есть, и с ней интересно ориентироваться. Я думаю, что большая часть причины того, что он не использовался для большого проекта, заключается в том, что он просто не понимается так, как мог бы, и, ИМХО, мы заперты в классических моделях наследования, которые мы все изучили, когда мы учили C++, Java и др.
редактировать
Я думаю, что сделал довольно хорошее дело против конструкторов. Но мой аргумент против фабрик нечеткий.
После дальнейшего созерцания, во время которого я несколько раз шлепнулся к обеим сторонам забора, я пришел к выводу, что фабрики также не нужны. Если animal
(выше) дали другую функцию initialize
было бы тривиально создать и инициализировать новый объект, который наследуется от animal
,
var myDog = Object.create(animal);
myDog.initialize(4, 'Meow');
Новый объект, инициализированный и готовый к использованию.
@Raynos - Ты совершенно обидел меня за это. Я должен готовиться к 5 дням, ничего не делая продуктивно.
Короткий ответ на ваш вопрос "Нужны ли фабрики / конструкторы в прототипах ОО?" нет. Фабрики / Конструкторы служат только одной цели: инициализировать вновь созданный объект (экземпляр) в определенное состояние.
При этом он часто используется, потому что некоторым объектам нужен какой-то код инициализации.
Давайте использовать предоставленный вами компонентный код компонента. Типичный объект - это просто набор компонентов и несколько свойств:
var BaseEntity = Object.create({},
{
/* Collection of all the Entity's 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's 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() должен быть передан второй параметр, чтобы установить эти значения. Мой ответ был бы просто: разве это не то же самое, что вызов конструктора или использование фабрики? Это просто еще один способ установки состояния объекта.
Теперь с вашей реализацией, где вы обрабатываете (и справедливо) все прототипы как набор статических методов и свойств, вы инициализируете объект, назначая значения свойств данным из источника данных. Может не использовать new
или какой-то тип фабрики, но это код инициализации.
Подведем итог: в JavaScript прототип ООП - new
не требуется - фабрики не нужны - обычно требуется код инициализации, который обычно выполняется через new
, фабрики или другая реализация, которую вы не хотите допустить, инициализирует объект
Пример статически клонируемого "Типа":
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), и когда вы это сделаете, вы будете использовать статический объект для создания целевого объекта. И тогда это становится таким же, как в предыдущем примере.