Как стать автором
Обновить

Комментарии 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 и позже) есть классы, так что там они будут выглядеть точно так же. Напишу об этом в самом начале, спасибо.


Основной смысл статьи в том, что обычное наследование можно полностью заменить миксинами, и все от этого только выиграют.

Совсем заменить наследование не получится:


  1. в TS запрещано в миксине объявлять protected / privated методы (как я написал выше). Так же мы не можем переопределять protected методы родителя
  2. Нет гарантий, что несколько миксинов не будут перетирать public методы друг друга.

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

  1. В TS запрещено, но ничего не мешает нам придумать такой язык, в котором можно) Окей, здесь сложно. Нужно каким-то образом заводить локальные скоупы имён для каждого миксина в цепочке, чтобы при этом late-bound this всё ещё работало. Что-то похожее на open/override из Kotlin должно помочь.
  2. То, перетирают ли они методы друг друга, зависит от порядка их применения. Если это нежелательно, нужно придумать какой-нибудь механизм явного 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, когда вся эта псевдо сленговость хлынула как из канализации… Типа. " тичинг на основе взаимного филинга и андестендинга"…
Треш конкретный.

Однако если класс наследуется от другого класса, чтобы переиспользовать наследника, нужно скопировать все зависимости, базовый класс и все его зависимости, и его базовый класс…. Т.е. хотели банан, а вытащили гориллу, а потом и джунгли.
Идиотия цветёт пышнейшим цветом. Слушайте, если какой-то идиот так «спроектировал» абстракции, так вы его расстреляйте да и всё.
Очевидно же что, во-первых «банан, абизъяна и джунгли» — это про библиотеки, а во-вторых, если вы всё таки про наследование, то это «банан, ягода, плод» или же «банан, растение, живое» и т.д., тут уж к биологам обращайтесь, они действительно спроектируют годные абстракции.
Ваша проблема в том, что вы пытаетесь спроектировать абстракции, но совершенно не понимаете что это. У вас получается чудовищное «наследование», где кирпич это родитель землекопа, а землекоп это наследник рояля и т.д. и т.п. Логика напрочь нарушена. Потому то ваши абстракции и текут.
«Наследование лишних методов» — это не проблема ООП, это проблема конкретного дизайна, который нарушает принципы ООП и SOLID. Если кратко: нельзя наследовать лист от стека, они принципиально разные. А совпадают у них только отдельные интерфейсы (непример, iterable, sizable).
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории