Комментарии 17
ООП обещает компонируемость, т.е. возможность переиспользовать отдельные объекты в разных ситуациях и даже в разных проектах. Однако если класс наследуется от другого класса, чтобы переиспользовать наследника, нужно скопировать все зависимости, базовый класс и все его зависимости, и его базовый класс….
Переиспользовать отдельный объект === переиспользовать класс?
Как Вы собираетесь сохранять стейтмент дерайвед класса во внешнем контексте?
Условимся, что LBC умеет динамически «кастить» не связанные классическим наследованием классы, но как будет вести себя класс-наследник, после выхода из текущего контекста?
В случае, если целью LBC является расширение типов, Вы рискуете получить ту же историю, которая есть сейчас в Ruby, когда Вы открываете код, и не можете на сто процентов доверять ему, потому что не знаете наверняка, какая функциональность включена в тот или иной класс. Так в Ruby пытались решить проблему выражения.
Второе:
Как Вы технически собираетесь реализовать динамическую расширяемость типов при строгой типизации в языке со статической проверкой типов? У Вас есть готовое решение? Очевидно, что Extends это «красивый» прототип.
Иначе, если LBC не призвано расширять типы, а только приводить их к каким-то определенным трейтам (назовем это по Rust'амански), то для чего нужен LBC, если можно реализовать трейт для этого класса? Разница лишь в том, когда выполнять проверку на совместимость (при компиляции в случае трейтов, либо в рантайме, потому как Вы оговариваете возможность LBC работать с Base-классом, которого еще не существует).
В случае с LBC, трейтом якобы выступает публичный интерфейс Base-класса? Но что тогда должна возвращать Extends, в случае несоответствия интерфейсов? Мне не ясно, как это реализовать в рамках Java, я уже не говорю про C++.
Это очень похоже на дополнение к сочинению одного известного автора про «трушное» ООП — альтернативой существующему и работающему предлагаете сомнительные решения.
Все вышесказанное верно, если я правильно понял, что Вы хотели донести в статье.
Как Вы технически собираетесь реализовать динамическую расширяемость типов при строгой типизации в языке со статической проверкой типов?
Готовое решение по типизации описано в документации TS по миксинам. По сути автор статьи и описал реализацию миксинов.
Надёжность типов достигается запретом на использование protected / private методов в миксине, если он экспортируется из файла. Иначе бы возникла коллизия закрытых методов. Хотя и так не гарантируется, что public методы не будут конфликтовать между собой.
Как Вы собираетесь сохранять стейтмент дерайвед класса во внешнем контексте?
Что вы имеете в виду?
В случае, если целью LBC является расширение типов
Оригинальный тип, который был передан в LBC, не меняется, просто создаётся новый тип, наследующийся от переданного. Так что я не совсем понимаю, как это может вызвать те же проблемы, которые вызывают Ruby-миксины. Разве что вы о том, что мы не можем знать заранее, какие ещё методы есть в конкретном базовом классе кроме заявленных в интерфейсе. Но это можно решить с помощью приватного наследования с явным реэкспортом имён, как в плюсах.
Как Вы технически собираетесь реализовать динамическую расширяемость типов при строгой типизации в языке со статической проверкой типов?
Так динамической расширяемости, кажется, и нет. После применения LBC B
к классу A
получается класс, объекты которого имеют известный тип: A & B
(либо B
, если применять приватное наследование).
Что тогда должна возвращать Extends, в случае несоответствия интерфейсов?
Ошибку компиляции? Не понял вопроса.
Ещё насчёт реализации в статически типизированных языках — мне подсказали, что, считай, ровно такой же механизм в плюсах называется Policy, и был как следует проработан Андреем Александреску (Modern C++ Design, 1 глава).
Если кратко, Policy — это класс с шаблонной базой.
…вот виновник сегодняшнего торжества (осторожно, 5 строчек на JS):
```javascriptfunction Extends(clazz) {
return class extends clazz {
//…
}
}
Идея не новая. В Typescript она называется mixins.
Возможно, что и в JS есть что-то подобное. Я встречал реализацию подобной идеи через Object.assign(...)
Да, действительно, это TS-овские миксины и есть. В JS (ES2015 и позже) есть классы, так что там они будут выглядеть точно так же. Напишу об этом в самом начале, спасибо.
Основной смысл статьи в том, что обычное наследование можно полностью заменить миксинами, и все от этого только выиграют.
Совсем заменить наследование не получится:
- в TS запрещано в миксине объявлять protected / privated методы (как я написал выше). Так же мы не можем переопределять protected методы родителя
- Нет гарантий, что несколько миксинов не будут перетирать public методы друг друга.
Мне кажется, что такой подход имеет смысл, когда у нас есть базовый класс, который можно опционально расширять. И заранее нельзя сказать какие расширения понадобятся
В TS запрещено, но ничего не мешает нам придумать такой язык, в котором можно)Окей, здесь сложно. Нужно каким-то образом заводить локальные скоупы имён для каждого миксина в цепочке, чтобы при этом late-bound this всё ещё работало. Что-то похожее наopen
/override
из Kotlin должно помочь.- То, перетирают ли они методы друг друга, зависит от порядка их применения. Если это нежелательно, нужно придумать какой-нибудь механизм явного disambiguation. Вообще это похоже на проблему с коллизией параметров конструктора.
О, насчёт перетирания можно ещё сделать так. Рассмотрим следующий пример:
interface Base {
a(): void
}
class BaseImpl implements Base {
a(): void {
b()
}
b(): void {
log('BaseImpl')
}
}
class Derived extends Base {
b() {
a()
log('Derived')
}
}
Если мы выполним (new (Derived(Base))()).b()
, это должно вывести сначала BaseImpl
, а потом Derived
. То есть, для перегрузки недоступны методы, которые не указаны в интерфейсе базового класса.
С другой стороны, что за жесть будет происходить, если мы передадим эту штуку в метод, который ожидает Раз метод ожидает BaseImpl
?..BaseImpl
, он на самом деле ожидает самый большой интерфейс, которому соответствует BaseImpl
, то есть
interface IBaseImpl {
a(): void
b(): void
}
И объекты из Derived(Base)
ему соответствуют, просто метод b
уже не тот.
Что мне не нравится в этом решении — что интерфейс уже не является просто шаблоном для проверки структуры объекта, он влияет на резолв перегрузки. Возможно, тогда стоит разделить две сущности: интерфейсы как шаблоны объектов и интерфейсы как фильтр для базового класса?
Или я не так понял, но у меня такой код вызывает вечный цикл вызовов a => b => a => b…
interface IBase {
a(): void
}
class BaseImpl implements IBase {
a(): void {
this.b()
}
b(): void {
console.log('BaseImpl')
}
}
type Constructor = new (...args: any[]) => IBase;
function withDerived<TBase extends Constructor>(Base: TBase){
return class Derived extends Base {
b() {
this.a() // тут будет вечный цикл вызовов
console.log('Derived')
}
}
}
const Derived = withDerived(BaseImpl);
const derivedObj = new Derived();
derivedObj.b();
Да, в TS само собой будет косвенная рекурсия. Это пример на выдуманном языке, построенном на миксинах.
Смысл как раз в том, чтобы для this
внутри BaseImpl
и внутри Derived
были разные методы b
. Знаю, это не совсем late-bound this, но смысл как раз в том, чтобы он был late-bound только для методов, указанных в Base
. Потому я и говорю, что в таком случае мне не нравится, что интерфейс начнёт влиять на рантайм.
Критика ООП звучит уже наверное лет 15. Но внятной альтернативы пока ни кто не предложил. Я имею ввиду такой, что принципиально изменит ситуацию, а не просто заменит одни проблемы на другие. Как вы думаете, почему?
Да. Если не больше. Точно так же, как и критика реляционных бд. Все, что предлагается, как "супер новое" решение, которое наконец то расширит грани сознания оказывается не жизнеспособно.
Птшите уже в каком то одном упоротои стиле…
"Окей, здесь сложно. Нужно каким-то образом заводить локальные скоупы нэймов для каждого миксина в чайне, чтобы при этом late-bound this всё ещё воркало. Что-то похожее на open/override из Kotlin маст помочь."
Напоминает страшные баяны начала 90, когда вся эта псевдо сленговость хлынула как из канализации… Типа. " тичинг на основе взаимного филинга и андестендинга"…
Треш конкретный.
Однако если класс наследуется от другого класса, чтобы переиспользовать наследника, нужно скопировать все зависимости, базовый класс и все его зависимости, и его базовый класс…. Т.е. хотели банан, а вытащили гориллу, а потом и джунгли.Идиотия цветёт пышнейшим цветом. Слушайте, если какой-то идиот так «спроектировал» абстракции, так вы его расстреляйте да и всё.
Очевидно же что, во-первых «банан, абизъяна и джунгли» — это про библиотеки, а во-вторых, если вы всё таки про наследование, то это «банан, ягода, плод» или же «банан, растение, живое» и т.д., тут уж к биологам обращайтесь, они действительно спроектируют годные абстракции.
Ваша проблема в том, что вы пытаетесь спроектировать абстракции, но совершенно не понимаете что это. У вас получается чудовищное «наследование», где кирпич это родитель землекопа, а землекоп это наследник рояля и т.д. и т.п. Логика напрочь нарушена. Потому то ваши абстракции и текут.
Чиним наследование?