Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 33

Очень ждал что будет раскрыт какой-то прикладной смысл этого упражнении. Но так и не нашёл :(

Рекурсивные конструторы и наследование от экземпляров -- разве не прикладной смысл ?
Зачем они нужны, например:
- наследование от экземпляров отражает развитие жизненного цикла
- рекурсивные конструторы позволяют создавать зависимые типы
Много чего можно придумать, если включить фантазию )

"Рекурсивные конструторы и наследование от экземпляров" звучит как инструмент или решение, но для каких прикладных задач? Для какой фичи? Можете привести пример "из жизни" когда это требуется?

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

Я, возможно, чего-то не понимаю.

А прототипное наследование - это не наследование от экземпляра? Про прикладной смысл согласен - он около нулевой. Все ждал, что написано будет что-то вроде "можно использовать return для раннего выхода из функции-конструктора". Статья больше похожа на графоманию, по моему мнению.

Согласен, стиль описания старинный. Мало кода, много букв.
Но я так вижу, мне так хотелось оставить. Поставил +1 комментарию )

Если говорить про то что "под капотом" реализации классов JS - то да.

Но если на уровне TypeScript или ES6 - то там есть явно выделенные классы, и экземпляр объекта можно получить созданием из класса, как в языках с "правильным" ООП. И если нужно получить два объекта с одинаковым классом, мы берем и создаем два объекта. Опять же оговорюсь, на этому уровне абстракции нам пофигу что это синтаксический сахар пад прототипным наследованием.

const obj1 = new MyClass();
...
const obj2 = new MyClass();

А вот зачем нам может понадобиться создавать новый объект из существующего объекта? Зачем из почти настоящего ООП нам обратно нырять в не совсем настоящую реализацию?

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

Да, всё так.
Речь про жизненный цикл больше. Если мы всё же хотим более комплексную логику, отслеживать что наши конструкторы созданы нашим кодом, например, что их логика осталась без изменений и т.п.. Или ещё банальней -- мы хотим видеть результаты трансформаций и иметь для всех конструкторов валидную проверку на instanceof то мы можем так завести фабрику конструкторов, в которой будет реализована аналогичная модель проверки.
Можно и иначе, конечно, без усложнений. Для меня сам факт того, что экземпляры классов могут быть конструкторами уже показательный, это много чего позволяет сделать интересного. Может быть не очень практически нужного, потому, что никто к такому подходу привыкать не захочет -- но это другое.

Попробую.
Представьте себе, что Типы -- это данные, а Интерфейсы -- это Поведение, краткое описание алгоритма как чёрного ящика. Это простая логика, придуманная давным давно, до JS и до TS тем более. Если на код в таком понимании смотреть, то становится логичным иметь эти возможности.
Конкретно, про оба тезиса:

  • Наследование от экземпляра даёт возможность хранить в этом экземпляре логику предыдущих итераций жизненного цикла данных. То есть, допустим, мы приняли порцию данных в Endpoint, это экземпляр данных "первого рода". Далее через какой-нибудь интерфейс -- по сути то есть любой метод и конструктор -- над этими данными произведена трансформация. Логично, что данные в этом случае "унаследованы" от экземпляра "первого рода". То есть, имея Тип исходных данных и Интерфейс их трансформации как конструктор мы можем этот конструктор унаследовать от экземпляра. Это и есть исходный способ работы прототипного насследования, как аппликативный функтор: конструктор применяется к существующим данным -- прототипу, -- и аргументам -- то есть новым данным для обогащения нового экземпляра.

  • "Рекурсивные конструкторы" -- это игра слов, тут тоже критика весьма конструктивна, т.к. совсем же ничего не пояснил, в самом деле.

    И, то есть, что здесь происходит, с учётом предыдущего объяснения про наследование. Конретно, представим ситуацию, что нам не нужно менять "модель", то есть не нужно менять саму "структуру" данных, но при этом необходимо получить новый экземпляр этих самых данных для какой-нибудь конкретной операции. То есть, допустим, у нас экземпляр второго рода -- приняли данные и сделали DTO -- и теперь мы хотим это DTO положить в лог или в БД. При этом структура данных у нас уже удовлетворительная, но нужен новый экземпляр, например для логов мы хотим установить в undefined значение поля password и нам структурно не нужно изменять объект DTO, но для этого конкретного поля нужно установить его значение через .definePropery в get () { return undefined;}для отражения назначения. В этом случае согласно SOLID мы же уже описали все поля ранее, в исходном DTO, и нам не нужен структурно иной объект т.к. поле password у нас string | undefined, но само значение мы пока не установили, и тут нам то есть нужен такой же самый тип, который является наследником своей предыдущей версии. И, получается, что нам для него не нужен новый конструктор, но информация о том, что это новая версия вполне может пригодиться для трейсинга и отслеживания ошибок. Аналогично с БД, нам лишь какой-то набор полей нужно изменить, не меняя структуру самого объекта -- это тоже то есть уже SOLID-но на типах. То есть это уже в Runtime так будет, а не только Ahead of Time. И тогда так же логично вполне становится понятно зачем абстрактные классы вообще существуют.

Конечно, это всё антигуманно, и никто так делать не будет. Но так писать код в целом вполне возможно при наличии развитого инструментария, чтобы не выписывать весь boilerplate для этого вспомогательного кода, способный отразить такую "более строгую" логику работы с интерфейсами и типами, чем та, которая присутствует сегодня.

И, да, предвижу вопросы про "а пробовал ли сам" и "что с Performance".
Пробовал. С Performance, конечно, так себе.
Только вот суть в том, что сам код радикально отличается от того, что я вижу в 99.9(9) % существующих проектов. Его кратно меньше, он иначе структурирован, т.к. логика развития данных и алгоритмов это "деревья" Trie из прошлого в будущее. То есть именно то, как оно должно быть, и ничего другого. Код получается более компактным и на этом возникает экономия -- глупостей меньше, оптимальностей болье. Чем-то напоминает ФП, но такое, "на стероидах", объектно-функциональное и, конечно, пространство для маневра в виде Аспектно Ориентированного Программирования, т.к. по сути во все замыкания легко добавить Аспекты -- то еть отражения текущего состояни процесса. И тогда, допустим, основываясь на предыдущем примере про пароль, для конструктора логов при откладке выставлен аспект переключен в "покажи пароль", и пароль отображается. Т.е., это могут быть realtime изменения, без перезагрузки и т.п.

Безусловно, всё это можно реализовать иначе. Через спагетти-макароны от и DI/IoC-контейнеров, где "привычка" раскладывать всё именно так порождает простыни import-ов для кратно меньшего объёма самого кода -- это то, что я вижу в Nest.js и т.п. Возможно просто проекты не удачные. Но, блин -- это сложно, иметь столько "расписаний" каждый раз на каждый чих. Можно иначе, исходно оно выглядит намного сложней, но в итоге получается сильно проще.

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

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

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

Для этого уже существуют паттерны, не нарушающие ООП - статические фабричные методы или отдельный FlyweightFactory.

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

Да в принципе конструктор может вернуть все что угодно, в том числе и объект другого класса. Но, имхо, это пахнет говнокодом.

class MyClass1 {
    name='MyClass1'
}

class MyClass2 {
    constructor() {
        return new MyClass1()
    }
}

const obj = new MyClass2();

console.log(obj)

MyClass1 {name: 'MyClass1'}

console.log(obj instanceof MyClass1);
true
console.log(obj instanceof MyClass2);
false


в вашем примере чуть иначе можно, и всё получится:

const ogp = Object.getPrototypeOf;

class MyClass1 {
	get name () {
		return 'MyClass1';
	}
}

class MyClass2 {
	get name () {
		return 'MyClass2';
	}
	constructor () {
		const proto = ogp(this);
		Object.setPrototypeOf(proto, new MyClass1);
	}
}

const obj = new MyClass2();

console.log(obj);								// MyClass2 {}
console.log(obj.name);							// MyClass2
console.log(obj.hasOwnProperty('name'));		// false
console.log(ogp(obj).hasOwnProperty('name'));	// true

// -----------------------------------------------------------

console.log(ogp(ogp(obj)).name);				// MyClass1

// -----------------------------------------------------------

console.log(obj instanceof MyClass1);			// true 
console.log(obj instanceof MyClass2);			// true

заметьте, что name в данном случае это свойство экземпляра, если вы хотите функцию переименовать, то через Define Property нужно к её указателю обратиться и задать новое имя. Кстати оно не обязательно должно быть строкой. Но может я не очень понял, что вы хотели получить, просто как можно ещё "поиграться".

Мне кажется, или вы только что "изобрели" наследование? 🙂

const ogp = Object.getPrototypeOf;

class MyClass1 {
	get name () {
		return 'MyClass1';
	}
}

class MyClass2 extends MyClass1 {
	get name () {
		return 'MyClass2';
	}
}

const obj = new MyClass2();

console.log(obj);								// MyClass2 {}
console.log(obj.name);							// MyClass2
console.log(obj.hasOwnProperty('name'));		// false
console.log(ogp(obj).hasOwnProperty('name'));	// true

Но может я не очень понял, что вы хотели получить, просто как можно ещё "поиграться".

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

Задаём вопрос ChatGPT, получаем более краткое и понятное разъяснение.

Но в общем и целом этот функционал не нужен, за его использование надо бить по рукам. Вместо него следует использовать factory function.

Вы сейчас серьёзно ?
Конструктор -- это и есть фабрика.
В случае конструкторов, создающих объект-конструктор -- это фабрика фабрик.
Только "настоящая", с проверкой на instanceof в отличие от того, к чему вы наивно привыкли слушая свои нейросети, которые ничего подобного не видели и не напишут вам, я проверял -- они пока не знают, что так можно, потому, что этого кода никто никогда не писал.

Если отжать от тонны воды, останется примерно следующее:

В JavaScript конструктор может вернуть объект вместо this, и обычно это избыточно, но есть один уникальный случай: он может вернуть функцию или класс, то есть другой конструктор. Это позволяет создавать рекурсивные конструкторы и наследовать от экземпляров - то, что по-другому реализовать нельзя. Всё остальное использование return в конструкторах можно заменить фабриками или обычным кодом.

Выглядит так, что он может просто вернуть объект произвольного класса - и, возможно, разных классов в зависимости от аргументов - то есть работать как фабрика. Есть ли в этом практический смыл и удобство - не знаю, на JS не пишу )

Не уверен, что понял, про что речь.
Вы имеете в виду Type Families, где все классы являются наследниками одного исходного экземпляра ? https://en.wikipedia.org/wiki/Type_family

В JS же вроде duck typing - можно возвращать объекты никак не связанных классов, достаточно чтобы они реализовывали общие интерфейсы.

Ааа, вы про это. Да, есть такое ограничение для прямых указателей, т.е. в JS пока нет перегрузки операции Assign.
Но если работать с полями объектов через get-еры/set-еры и добавить чуточку магии мета-программирования для Symbol.toPrimitive, то всю TS логику ограничений на типы можно примерно в 50 строк компактненько добавить в Runtime, т.е. добавить строгость и ограничения прям в туда. Таким образом на TS логика не изменится, а логика JS в Runtime'е будет ей соответствовать.
С учётом того, что, например IEEE 754 в целом отражает состояние вне зависимости от языка -- это особенно полезно будет запрещать творить дичь, складывать примитивы с объектами и т.п.. В общем строгая типизация со всеми её плюсами и минусами для полей объектов вполне возможна.
То есть мы можем сделать так, что в полях объектов для Данных хранятся Объекты. И то есть можно получить вполне себе Номинальную Типизацию. Но да, только для полей объектов. Но не очень сложно ведь. И объяснение "как" простое -- через Symbol.hasInstance проверять является ли экземпляр наследником класса MyNumber, и, таким образом всё становится намного интересней, веселей и действительно гибким, гораздо более динамическим чем исходный такой весь из себя "динамический" JS.
И вот тут уже становятся весьма необходимыми все эти преимущества языков, считающихся более развитыми, т.к. мы в самом деле уже давно можем это не просто смоделировать, а более того -- смоделировать так, как нам нужно.
Т.е., такой "дизайнерский" JavaScript под задачи конкретного проекта. Нужна нам, допустим, System F -- да пожалуйста. Нужна λC -- да тоже без проблем, конечно, забирайте, грузите вагонами: https://en.wikipedia.org/wiki/Lambda_cube

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

Совершенно верно. Это то, как есть сейчас. С этой логикой нет никаких существенных проблем. Её обычно на всё хватает. Этим пользуются, к тому же есть private модификаторы в самом JS уже тоже.

Просто вы описываете Назначение Экземпляра.

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

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

В JS ведь нет интерфейсов. TS всё таки попытка ограничить JS чтобы это более менее просто проверять типы. Интерфейс это неплохо, да и можно использовать хитрый union тип или Record вместо простого интерфейса, но это все-таки далёкая аппроксимация динамической природы JS. Если бы TS хотел отразить эту динамичность, то ИМХО тип должен изменяться вместе с изменением объекта.

да, и это достаточно просто объяснить TypeScript-у, буквально одна строка

export type Proto<P, T> = Pick<P, Exclude<keyof P, keyof T>> & T;

здесь, получается, что мы заменяем в прототипе типа те свойства, которые определены в самом порождаемом типе, то есть "переопределены"

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

Да, так понятней, Спасибо!
Мы про одно и то же, с разных сторон, получается, написали )

да, примерно так )
мне хотелось отразить муки творчества )
но, да, +1 к комментарию )

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

да, согласен, учту, мои "оправдания" никому не интересны, всем всё равно, у всех своих хватает страданий и мыслей и т.п. +1 коментарию.

чуть поменял статью для читателя, но, наверное уже поздно, да и ладно

но, согласитесь, что категорично делить на "чёрное и белое" в целом тоже, не отражает позицию большинства, возможно просто вы не моя Целевая Аудитория, кому-то "художественный свист" вполне приемлем, и можно было просто не читать буквы и смотреть сразу на код )

Так что, категорично объяснять как себя "вести в приличном обществе" -- оно, конечно, безусловно -- вполне можно, но вести или нет -- это выбор персонажа.

Так а про сам результат что скажете ? Стиль понятно, страдает. Результат вам как, понравился? Или вы это раньше уже видели?

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

да, тут же в примере не описаны аргументы, так как это фабрика фабрик, то по сути мы можем конструировать разные фабрики в зависимости от аргументов, но при этом ещё дополнительно можем проверить, что они instanceof самой фабрики.

Хороший кейс. Но выглядит как вредительство =)

Получается, что в любом месте у вас может быть изменен объект, созданный в другом месте. Да, для синглотона это ровно то что ожидается. Но одно дело, когда вы пишете код явно понимая, что работаете с синглтоном. И совсем другое, когда у вас уже есть база кода, написанная без учета синлотона. Где-то вы получите ну совсем неожиданное поведение, и у какого-то клиента спишутся деньги за услуги другого =( Напоминает шутку про "define true=false # мучайтесь, уроды"

Ну эта проблема никак не связана именно с созданием через new - с фабриками было бы то же самое. Да даже созданные через new вроде бы "независимые" объекты могли обращаться к какому-то общему ресурсу. В общем, если для экземпляров какого-то класса вдруг появилось переиспользование, то наверно автор знал что делал.

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

Допустим, что у нас в JS строгая типизация для объектов, сделать это очень просто, с учётом нынешних фич языка в метапрограммировании: get/set + Proxy.
То есть все свойства объектов проверяются на instanceof , а значит:

  • Можно сделать так, что в свойствах объектов будет разрешено хранить только объекты. То есть даже для примитивов числа, строки и булевы можно будет хранить как экземпляры их конструкторов: new Number, new String, new Boolean, или производные этих конструкторов через Class Extends

  • Таким образом для "строгости" вводится правило, что положить новое значение свойство объекта можно лишь в том случае, если это значение того же самого типа. Для этого достаточно сравнить образец, уже существующий в свойстве с тем, что нам поступает в value для set -ера. И тут мы вольны выбирать можно ли для Extended полей класть базовый тип или нет, но в целом это легко пишется.

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

И тогда, получается, если вам нужны рекурсивные конструкторы в JS, то их можно получить как в примере из статьи. Но лучше взять тот GIST, там все нюансы учтены.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации