-
-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Поддержка типов в контекстах #256
Comments
Я видимо плохо объяснил в чём суть... this.$ имеет тип $mol_view_context, который соединяется из всех корневых неймспейсов:
Далее мы можем объявить пару классов наиболее естественным образом, без нагромождения интерфейсов, инъекторов, ресолверов и пр:
Теперь любой класс выше по иерархии может переопределить зависимость для всех вложенных компонент:
bbb() будет использовать нашу версию $my_aaa. TS при этом проверит:
Разумеется работает и автодополнение. При этом мы не выходим за рамки стандартных возможностей TS. Я думаю сделать так, чтобы можно было переопределять вложенный контекст в упрощённой форме:
Так же думаю лучше вынести это дело из $mol_view в $mol_object, чтобы работало не только с визуальными компонентами. |
Смущает то, что это все завязано на соглашение по именованию модулей, получается mol вещь в себе, как такое использовать с npm тем же? Еще получается, все доступно для всех, this.$ это как копия реестра глобального пространства имен. |
Реестр никак не завязан на именование. Именование - оно для автоматического подтягивания зависимостей. |
Там контексты не будут поддерживаться в ts, если не будет соглашений по импортам: что все обращения только через $-неймспейс, никаких import / export. |
А, ну это да. |
Все же, при вашем подходе, отвественность по инциализации лежит на компоненте, нет автоматизированного IoC. Например, что будет если в: subContext.$my_aaa = class extends this.$.$my_aaa { } Будет изменена сигнатура конструктора? Ts ведь не отловит это и ошибка всплывет только в рантайме: class $my_bbb extends $mol_view {
@ $mol_mem()
aaa() {
return new this.$.$my_aaa
}
} PS Хотя это и не очень страшно, ts не отлавливает только такие случаи: namespace $ {
export class A {
constructor(p: string) { }
a() { }
}
}
class B {
constructor() { }
a() { }
}
$.A = B Но все же, как быть с зависимостями, необходимыми для инициализации класса. PS2: Вроде смог подобрать рабочую конструкцию: namespace $ {
export let a: {
some: string;
}
}
class A {
constructor(public some: string) { }
}
$.a = new A('test') |
В $mol конструктор обычно пустой, ибо все вычисления делаются лениво. А конфигурирование происходит через переопределение свойств. через свойства же и происходит получение данных, если они требуются. И обычно, если одному объекту требуется другой, то он сам его по умолчанию и создаёт. Даже если разумного дефолта нет - просто создаётся мок, декларирующий интерфейс и упрощающий тестирование. |
Тут смущает один момент. Переопределение свойств где происходит, сразу после инициализации класса? Если да, то тогда проблема остается - знание о всех зависимостях класса сосредотачивается в точке его инстанциирования, а размазывание этой ответственности по фабрикам - уменьшение возможностей кастомизации. Если нет, то размазывается целостность класса. Отдельно создаем инстанс и где-то спустя N строк - переопределяем методы. Логичнее, если информации о создании класса будет где-то в нем самом, как в DI. Т.к. может быть куча деталей, которые требуются для его работы, но фабрике о них знать не требуется вовсе. А в вашем случае получается компонент-фабрика будет знать. Почему кстати через переопределение свойств, обычно через конструктор, он для этого и предназначен. |
Переопределение происходит сразу после создания. Есть и фабрика:
К сожалению, тайпскрипт не позволяет сделать так:
А возможность переопределения любого свойства даёт:
|
Да, я тоже с таким сталкивался в flow, хотелось case классов из scala. Фейсбуковцам пофиг. Переопределение мне не нравится мутабельностью и не атомарностью, а так норм. Про фабрику я имел в виду не make, а то, что о параметрах, которые передаются в make, знает сущность, которая объект $my_foo создает, т.е. параметры она не использует в вычислениях результата, они ей нужны только для создания $my_foo. PS. Хотя в flow вроде починили |
Ну, владельцу объекта виднее как его настраивать же. |
Я все пытаюсь объяснить, что эта ответственность по инстанциированию чего-либо должна быть на каркасе, а не забиваться вручную в объекте-владельце. В противном случае в слои, где используется зависимость, просачиваются детали реализации этой зависимости - аргументы конструктора или make. Пусть A - определяет в контексте класс C Тут 2 проблемы:
|
Так в том-то и дело, что ни у какого класса нет "обязательных параметров" и нет никакой превентивной инициализации. :-) "А" объявляет в контексте только себя. "C" только себя. "В" создавая экземпляр "С" указывает лишь те параметры, что нужны лично ему. Об остальных параметрах думать не его забота. Если мы хотим всем экземплярам "С" в контексте прописать какие-то параметры, то мы так и пишем:
Теперь, когда В создаст C у него уже будут прописаны D и E, а он добавит к ним ещё и нужный ему foo. |
Настройка через наследование решает, однако ценой генерации классов в рантайме, даже если у меня просто строчка настройки, нужно метод создавать. Вместо new C('some') Нужно делать: class C1 extends C {
some: () => 'some'
} Невозможность использовать конструктор - это ограничение же, примерно как в React можно передать только props в конструктор. |
Ну а что бы вы сделали, например, в конструкторе? Кроме как переложить аргументы в свойства там делать особо и нечего. |
Ну да, я бы просто объявил бы контракт: class A {
constructor(public some: string) {}
} Ощущение, что вы боретесь с убогостью js/ts, придумывая разные нестандартные выкрутасы. |
Я исхожу из следующих посылок:
|
Ваш подход я бы назвал di на контексто-переопределяемых, наследуемых синглтонах. По п.3. Если нет подготовки во вне, значит она упрятана в класс. Понятно, что в голой пустыне запустить такой класс проще, однако запускается все в какой-то среде, почему бы ее не сделать богатой, а не переносить эту ответственность на прикладной код. В полноценном di не надо создавать объекты, только объявить зависимость. Зачем создавать в коде что либо прикладному программисту, если достаточно декларировать, а каркас все сам сделает. Представьте, что ts автоматом бы создавал зависимости при инициализации класса. В общем то цели одни и теже, а вот почему выбран этот механизм, непонятно, реализовать проще? Вопрос не связанный c mol: А когда кастомизировать сущность наследованием, а когда новым инстансом и настройкой через конструктор? Ведь каждый инструмент для своих целей, у вас получается второй не используется, в силу некоторых упрощений. |
Это сервис локатор же. Ну да, и реализовать проще, понять проще, поддерживаются не только классы, но и функции и просто константы. Хотя я согласен - было бы лучше иметь фабрики, а не классы. Хотя разница между ними в js лишь в слове new. Второй используется, только настройка через свойства. Используется для instanse-специфичных настроек. |
Похоже, да, правда в каноничном service locator один реестр без контекстов и типизации нет, инфраструктурный код просачивается в приложение. У вас без первых 2х недостатков. А когда настройки instance-специфичны, а когда наследованием надо? В mol мне как-то грань эту трудно уловить пока. |
Если для группы инстансов должна быть одна и та же настройка не зависящаяя от точки инстанцирования, то лучше вынести в отдельный класс и наследовать эту настройк через прототипы, а не станавливать каждому инстансу. |
А почему лучше, это как-то оптимальнее работает? Или просто неудобно каждый раз передавать настройку, т.к. нет инструментов для автоматизации этого? Или есть еще какой-то кейс использования такого (облегчение преобразования из tree, например) |
Быстрее, ест меньше памяти, да и удобней. |
Так то оно так, только вот мозг надо переломать, если привык использовать инверсию контроля. Про tree еще более менее понятно из статьи Идеальный UI фреймворк. Но про все остальное не очень. Может быть статью напишите про особенности реализации паттернов, которые использованы в mol? Или какое-нибудь сравнение, что б например, люди привыкшие к реакту/ангулару смогли адаптироваться? Именно про архитектурные приемы. |
Так это тоже инверсия контроля. Просто она ненавязчивая - по умолчанию компонент сам всё делает. А компонент выше по иерархии может приказать "сюда не ходи - туда ходи" :-) В ближайшее время как-раз собираюсь что-то такое написать, да. |
А в чем по-вашему заключается ненавязчивость? Мне кажется, правильнее говорить, компонент с нуль-конфигурацией, когда не надо настраивать среду для его работы, в нем уже есть инфраструктурный код, обеспечивающий связывание. По мне, ненавязчивость - это когда нет
Это похоже на иерархический di в ангуларе. Я хочу сказать, что почти на все что вы делаете, уже есть аналоги (может, кроме tree). И тут для меня важнее понять ход мыслей, почему выбран такой нетрадиционный способ. Чем это лучше классического di, привычного SOLID и т.д. |
Ну, если совсем без завязок на инфраструктуру - приходится много кода писать. Ненавязчивость я имел ввиду именно DI. Хочешь - провайди зависимость, не хочешь - не провайди. А вот у Ангуляра навязчивость DI сильная - приходится делать много хитрых пасов (интерфейсы объявлять, повайдеров регистрировать, инъекторы указывать), чтобы всё заработало. В |
Если бы this.$ было в спецификации языка, я бы наверное согласился. Но так это не di, а сервис локатор. Отличие в явной завязанности на инфраструктуру, пускай простой и небольшой, но делающей невозможным запускать компоненты где-то еще, кроме mol. Об этом много статей, например вот.
Не все так сложно, умолчания тоже работают. В целом конечно, ангулар не пример для подражания, т.к. там плохо переосмыслили backend-подходы, когда привносили их с java/c#. Legacy-мышление. В моем велосипеде не надо ничего регистрировать: Например, autocomplete /* @jsx lom_h */
export function AutocompleteView(
_: IAutocompleteProps,
service: AutocompleteService
) {
return <div>
<div>
Filter:
<input value={service.nameToSearch} onInput={service.setValue}/>
</div>
Values:
<AutocompleteResultsView searchResults={service.searchResults} />
</div>
} Я просто объявил service: AutocompleteService и среда его настроила и передала компоненту. |
В принципе никакой завязки на инфраструктуру. Просто инъекция сервис локатора. Реактовые контексты - это ж то же самое. |
В mol_view же не в таком виде используется, там как раз есть завязка. @ $mol_mem
context( next? : $mol_view_context ) {
return next || $ as any
}
get $() {
return this.context()
}
set $( next : $mol_view_context ) {
this.context( next )
}
context_sub() {
return this.context()
}
Реактовые контексты плохой пример, как и сам реакт - это недоразумение. Но в случае компонент-функций, которых в mol нет, в реакте это уже похоже на di. const PropTypes = require('prop-types');
const Button = ({children}, context) =>
<button style={{background: context.color}}>
{children}
</button>;
Button.contextTypes = {color: PropTypes.string}; Если убрать псевдотипизацию в Button.contextTypes, то будет вполне себе нативно. |
Ну, как $mol_view внутри себя использует инъектированный локатор - это уже дело $mol_view :-) Внешне апи эквивалентно тому, что я писал. |
Идея тут возникла, а что если написать плагин для ts или бабела (он теперь может в ts), который бы Это дало бы возможность абстрагироваться от факта наличия контекстов и уменьшило бы вероятность ошибки в случаях использования контекста. Присвоение бы осталось прежнее, через |
Но завязало бы на бабель. Это на мой взгляд ещё хуже. |
У вас и так все на ts завязано, какая разница - поменять ts на бабел. Анализатор вообще отдельно можно держать. Фейсбуку осталось добавить в flow поддержку ts и он убьет конкурента, будет в 3 раза быстрее и выведение типов лучше. Ладно, можно и не менять, просто в ts пока нет нормальных плагинов. Но, что бы тоже самое сделать, можно попробовать свой компилятор построить на его основе через compiler api. |
Реестр или service locator дешево и сердито, но типы не заработают при таком подходе, как и в vue, react. Чуть в лучшую сторону здесь отличается angular.
Нужен полноценный di, который бы поддерживал компоненты.
Синглтоны, контексты, плагины в mol_view - это все частные случаи, которые могут быть реализованы в едином подходе на di. В общем то суть di - не какое-то там удобное unit-тестирование, как некоторые считают, а автоматизация превращения всего приложения в точки расширения.
Тут можно вывести некоторые общие принципы:
Типы
Метаданные выводятся из типов, которыми описываются зависимости компонента
Зависимости компонента - по большей части реальные классы
Интерфейсы как зависимости
Интерфейсы через babel/ts плагины возможны примерно так:
Но, если честно, с этим больше проблем, чем плюсов. Полноценных интерфейсов в js нет, а в ts/flow поддержка только в compile-time, а надо в run-time, поэтому ради типизации приходится так "обманывать" flow/ts.
Да и обязательная регистрация зависимостей ради интерфейсов и даже компонент, как в angular - это клиника.
Generics влияют на метаданные
Я использую css in js, на нем и покажу что это:
Не уверен, насколько это чисто, но кода меньше чем с декораторами и типы поддерживаются.
Иерархичность
Нижележащий компонент наследует зависимости вышележащего, но может переопределить что-либо, не влияя на вышележащий.
Open/close принцип через алиасинг
Вышестоящий контейнер может переопределить для всех нижестоящих любую зависимость, зная ее интерфейс / класс.
Цель такая же как в вашем tree - дать удобные точки расширения для всего по-умолчанию, только применимо к любой сущности в программе, а не только компонента.
Нет прямой работы с контекстом
В вашем примере:
this.context - это типоопасная свалка, а прямая работа с ней черевата локином к конкретному фреймворку (хотя у вас и так локин от вашей экосистемы).
Работа с контекстом должна быть только через регистрацию/объявление классов/функций/интерфейсов. По мне так всякие ключи строковые не имеют смысла, да и подобных решений как грязи (тот же vue).
Например, как я попытался адаптировать эти идеи к реакту:
Через babel plugin создается мета для конструкторов и компонент:
Ради поддержки flow, прибитого к долбаному реакту, первый аргумент нельзя было использовать для описания контекста, поэтому пришлось научить babel отличать компоненты от функций.
Подменяя createElement на свой, я могу управлять инициализацией компонента и помимо свойств, прокидывать контекст в точном соотвествии с типом в аргументе. При этом компоненты - чистые функции + простые метаданные.
The text was updated successfully, but these errors were encountered: