Там пока не видно решений этих проблем. Конечно, что-нибудь придумают, может где-то даже лучше чем в атомах. Просто это путь проб и ошибок, который им придется отчасти повторить за винтажем.
У фронтенда есть некоторые особенности перед бэкендом, из-за которых с таким трудом тут внедряется классический DI:
1. Большая сложность композиции, например, композиция из 10 компонент (если считать их зависимостями) вполне норм, а на бэке (если по SOLID), больше 3х зависимостей считается не очень хорошо.
2. Интенсивный рефакторинг: чем ближе к ui, тем чаще вносятся изменения, поэтому дополнительный обслуживающий код (типы, регистрация в composition root) замедляет разработку в больше степени, нежели на бэкенде.
3. Сильная иерархичность условного MVC и необходимость наличия состояния в сервисах (хоть и странно звучит), из-за которой возникает потребность более гибко управлять скоупами зависимостей и временем их жизни. Например, как в ангуларовском hierarchical-dependency-injection.
4. Необходимость использования типов для объявления зависимостей в паре с несовершенными средствами интроспекции js/ts (нельзя ассоциировать интерфейс со значением в run-time)
Если просто копировать решения из бэкенда, как это сделано в inversify (калька с C#/Ninject), то результат будет не очень хороший. Как лучше, пока никто не знает, все экспериментируют.
За отсутствием типизации и вывода типов в React.context, идея хорошая, но все-таки кажется, что бойлерплейта еще много.
1. Все интерфейсы дочерних компонент приходится объединять в интерфейсе родительского. При рефакторинге родителя, вместе с чилдами надо двигать и их интерфейсы.
class A extends React.Component<{
services: Services<A> & Services<B> & Services<C>}>
}> { ... }
2. Необходимо примешивать services к каждому компоненту
class A extends React.Component<{
services: Services<A> & Services<B> & Services<C>}>
render() {
<B services={services}/><C services={services}/>
}
}>
3. Все зависимости по-умолчанию жесткие. Добавили в сторонней библиотеке зависимость компоненту, а в 10 приложухах, использующих ее, теперь надо пойти в composition root и зарегить эту зависимость. Что-бы сделать зависимость мягкой, надо прикладывать усилия:
class B extends React.Component<{prop: string} & $Logger>
services: $Logger = {...this.props.services, logger: this.props.services.logger || new Logger(this.props.services)}
render() {
// ...
}
}>
4. Как быть с зависимостями сервисов в compostion root? Вручную им их передавать? Ведь нельзя же сделать
const services = { logger: new Logger(services) }
Вы в реальном более-менее сложном приложении использовали такой подход?
Суть примерно такая: в реакт добавили 2 спец компонента: Timeout и Loading.
1. если код оборачиваешь в Timeout, то он во время асинхронной подгрузки любого дочернего — покажет крутилку
2. если какой-нить дочерний тоже обернут в Timeout, то он будет иметь приоритет и верхний Timeout его проигнорирует
3. если код обернуть в Loading, то он вытащит статус загрузки isLoading со всего поддерева дочерних компонент
4. В рендере можно сделать throw new Promise и реакт перехватит исключение, проверит что пришел промис и перерендерит компонент при ресолве промиса. Если к этому прикрутить кэш, то код выглядит как синхронный.
Печально, что это все сильно реакт усложняет. Команде реакта можно было, например, сделать свой аналог mobx-а, запихнуть туда файберы, обработку асинхронности на исключениях и работу со стейтом.
Такое разделение ответственности позволило бы использовать эти фичи в клонах, вроде preact/inferno и других vdom и real dom библиотеках. Даже отдельного пакета можно не выкладывать, держать в проекте, а сейчас это все прибито к react-dom.
Добавив эту функциональность, они вступили на путь из граблей, по которым уже прошел vintage со своими атомами за последние года 2, есть множество проблем такого подхода. Например:
Что делать, если не promise, а observable?
Как сделать в реакте параллельную загрузку, если бросание эксепшена в render приостанавливает выполнение, а за ним может быть еще один асинхронный запрос.
Как делать retry в случае ошибки?
Как автоматизировать инвалидацию кэша, который в демках скорее всего достаточно примитивный, на каком-нибудь simple-cache-provider?
Такой ли это банальный манки-патчинг в этом случае? Тут патчинг используется в тех же целях, что и конструктор.
1. Типобезопасность остается, т.к. ts проверяет интерфейс заменяемых методов
2. Патч находится всегда рядом с местом создания экземпляра класса и применяется на новый экзепляр, одновременно несколько патчей тут наложить невероятно, читабельность остается приемлемой, т.к. патч рядом с созданием.
3. Патчинг app.$ похож на один из паттернов DI — ambiant context, который как раз в примерах vintage используется. Его реализация намного проще любого di, основанном на инжекции в конструктор, а возможности не хуже.
4. Класс проектируется сразу с дефолтными реализациями зависимостей, которые потом можно легко переопределить.
Инжекция через конструктор в DI хоть и делает код тестируемым, но ценой копипаста. В нормальных языках есть что-то вроде scala case classes или kotlin data classes, которые упрощают настройку объекта.
В ts такое поведение нормально не сымитировать, поэтому патчинг используется для настройки экземпляра класса. Это не иммутабельно и если этим правилом пренебречь, можно нарушить безопасность. Тут вопрос приоритетов, можно ли этим пренебречь, ведь взамен упрощается и унифицируется настройка экземпляров классов.
Сахар в ts не способствует улучшению читабельности. При создании объекта нет названий аргументов:
class A {
constructor(public v?: string = '') {}
}
var a = new A('test')
Что б создание объекта читалось чуть лучше, можно условиться, что аргумент — объект, то это уже ведет к еще большему копипасту аргументов и их типов:
class A {
public v: string
constructor(opts: {v?: string}) { this.v = opts.v || '' }
}
var a = new A({v: 'test'})
А вот патчинг:
class A {
public v: string = ''
}
var a = new A()
a.v = 'test'
Ну да, но при этом весь результирующего кода Svelte и Vue в десятки раз меньше.
Нужно объективно сравнивать степень автоматизации в реальных проектах, в todomvc нет даже обработки ошибок и серверного взаимодействия.
Мне в реальности нужно знать насколько хорошо фреймворк автоматизирует
1. Реактивность, как сделан observable state, автотрекинг зависимостей без on/off/subscribe/observe (mobx, cellx, mol_atom), работа с сервером: крутилка, обработка ошибок, retry
2. Контексты, DI или что-то вроде, что бы не прокидывать все через пропсы
3. Компоненты, как расширить уже имеющийся компонент (принцип open/close), переиспользовать с другой копией стейта
4. Как задать стили компонента и стили компонента в контексте чего-либо, как динамически управлять стилями и темизацией из js
5. Типизация (насколько хорошо используется вывод типов вместо прописывания аннотаций, работает ли в шаблонах/стилях?)
Svetle простая штука для небольших проектов, но тут пока до уровня vue ему далеко.
Хотелось бы конечно видеть полноценный todomvc (вроде такого) и сравнивать, насколько вышеперечисленные фишки просто делаются в коде.
Насчет «tree shaking friendly», что вы имеете ввиду?
Проблема отбрасывания лишнего кода техническая и она в природе импортов в js, поэтому приходится особым образом собирать и использовать эти модули. Это и есть friendly =).
Например, сейчас часто модули фреймворка собираются вместе в index.js, что мешает tree shaking-у. Но, в том же lodash, мы же можем так сделать: import _add from 'lodash/fp/add', есть даже плагин, которые такие импорты делает из обычного import _ from 'lodash'.
Если фреймверк маленького размера это как правило сказывается и на функциональности.
КПД фреймворка зависит от идеи в его основе, тут уж кто как угадает. Мне кажется, если автор смог такой генератор написать, то смог бы и обычный фреймворк не хуже, только tree shaking friendly.
Вообще, это как бы и называется ванилла JS.))) Вы если бы писали на ванилле, не использовали бы DOM API и т.п.?
Я бы наделал хелперов над DOM, т.к. он слишком низкоуровневый.
Писал где-то выше. Любое дублирование кода, можно оптимизировать
Посмотрим как у автора получится. Мне это видится более сложной задачей, чем написать нормальный фреймворк. Опосредованно получается, мы не сразу пишем оптимальный код фреймворка, а пишем генератор, который должен давать оптимальный код.
Да, согласен preact побольше на данном примере, но размер фреймворка постоянен, его суть в общем переиспользуемом каркасе. Если фреймворк удачный, бизнес код почти не содержит лишнего и не требует кодогенерации.
А вот кодогенерация создает некоторый избыточный код на каждый компонент. Да, svetle-todomvc 22кб без сжатия и минификации, но код изобилует портянками createElement/appendNode, т.е. низкоуровневой работой с DOM, которая слабо реюзается, на каждый if в шаблоне тоже генерятся приличные портянки функций.
Что будет, если приложение состоит уже из двух и более компонентов, похожих на todomvc?
Есть предел масштаба приложения, где svetle по размеру будет выигрывать, думаю его даже можно высчитать.
Странная все-таки тенденция. Вместо оптимизации на уровне языка и компилятора, каждый фреймворк обзаводится собственным компилятором, который работает только для подмножества идиом этого фреймворка.
Временно выиграть в гонке производительности это поможет, но пока не появится нормальный инструментарий, который для любого фреймворка сможет сгенерить подобный код.
Это выглядит как очередная стадия взросления экосистемы, где пока нет нормальных стандартов, компилятора с нормальным tree shaking и языка с мощными возможностями метапрограммирования.
Почему так на этой производительности все сосредоточились: react и vue далеко не самые быстрые, но пока в большинстве задач их производительности хватает.
Если отбросить оптимизацию, которая часто приводит к увеличению объема кода, чем принципиально такая кодогенерация лучше обычной сборки какого-нибудь todomvc на preact ролапом (кода кстати тоже около 3кб в gz)?
Среда разработки не дает контроля за выполением этого самого SOLID
Что значит не дает контроля за выполнением? Проверка типов есть, интерфейсы есть. Испортил реализацию — приложение не соберется.
Почему быстро бросится в глаза, а в SOLID значит не быстро? Это разве не от строгости языка зависит?
Как без DI быть с абстракциями, когда где-то наверху надо сказать, замени на 10м уровне вложенности класс X на Y, не влезая в иерархию деталей нижних уровней.
В этой статье я не планировал сравнивать все подходы для работы с состоянием. Мне интересно развивать идею маскировки реактивности под классы, которую использует mobx. Альтернативу его я и описывал: либу в 10кил, которая почти как mobx, только автоматизирует наиболее частые задачи синхронизации данных. Задача атомов узкоспециализированная: работа с реактивностью, кэш и инкапсуляция канала связи.
Relay, Apollo — достаточно тяжелые решения. Кроме того, что могут атомы, они дают optimistic updates, проверку целостности по схеме, сервер. Но если эти навороты не нужны, кода будет значительно меньше.
Т.к. атомы — легковесная абстракция между fetch и компонентами, то вместо fetch вполне может быть какой-нибудь graphql-js или apollo-client. Тут как раз больше возможностей для масштабирования.
На мой взгляд, логично переложить часть функциональности из Relay-подобных решений, в mobx-подобные решения. Общеизвестных библиотек для работы с состоянием все-таки меньше чем фреймворков для рендеринга. Вместо биндингов к реакту и прочим, можно было бы сделать пару биндингов от graphql к этим библиотекам.
С виду те же метаданные, только отделенные от классов.
Кода больше надо писать, указывать зависимости вручную.
Как обеспечить типобезопасность, ведь классы аргументов передаются отдельно в deps.
Или bootstrap-код теперь сам компилятор генерит?
Если дело только в скорости было, то можно было просто отказаться от тормозного Reflect.defineMetadata и генерировать более оптимальные метаданные.
Зачем делать зависимости только явными?
Для реального использования не годится, разве что в образовательных целях.
'user, dictionary' — строковая типизация.
getState('user.name'); — строковые селекторы, что будет с путями при рефакторинге стейта?
Store.addHandlers — как быть, если нужна копия стора с другими путями в стейте?
Redux сложный? По-моему, как раз одно из самых простых решений (если сравнить реализацию с тем же mobx), наивная реализация пишется строк в 100.
State-tree контейнеров десятки в js, зачем еще один? Вот mobx-state-tree неплох, разве он такой сложный, что надо писать свой велосипед?
Попробуйте избавиться от синглтонов, в редуксе как раз все через фабрики создается: createStore и т.д.
Попробуйте использовать типы, без типов рефакторинг приложения — как хождение по минному полю. Сделайте так, что б при изменении в объекте user свойства name на firstName, typescript или flow подсвечивали ошибку в селекторе. В том же mst типы выводятся.
1. Как у вас достигается типобезопасность, если все зависимости в строковых ключах описаны. DI же популярен в Java, C#, PHP и применяется для больших архитектур, а там без типов никак. А для небольших проектов и DI не нужен. Хорошие DI это Ninject C#, InversifyJS, AngularDI, там все на типах как раз.
2. Как во время рефакторинга узнать, что за реализация стоит за тем или иным строковым ключем-зависимостью? В какой папке искать Salad? Как при написании кода а не в run-time, узнать, что существует класс, который пытается резолвить DI?
3. Как в большом проекте вы решаете проблему дублирующихся имен классов? Есть какие-то составные имена, соглашения по именованию, неймспейсы?
4. Задача подгрузки кусков кода в DI не очень понятное решение. Разве подгрузка чанков бандла такая частая задача, что ее надо добавлять в ядро DI, задача которого собирать код, а не загружать из сети? К примеру, можно разделять приложение на несколько микросервисов или явно прописать загрузку в странице через роутинг и фетч, это же обычно достаточно в одном месте приложения сделать.
5. Добавили асинхронность, как обрабатывать ошибки загрузки, делать тот же retry, рисовать loading, пока код подгружается, отменять загрузку, если во время загрузки первой страницы перешли на вторую?
6. Конвенций для создания инстансов придумывать не надо, достаточно начать использовать typescript или flow+babel и генерить метаданные из аргументов в конструкторе, а через new внутри DI создавать инстансы, как делает Inversify или angular.di, разве нет? Зачем портянки невалидируемых конфигураций (в Java/Spring или PHP/Symfony для этого есть мощные плагины для idea), если можно метаданные вытаскивать из типов/интерфейсов?
7. Как вашу реализацию заставить работать с компонентами, тем же реактом? В Inversify есть хотя бы inversify-react.
8. При применении DI к компонентам вылезает проблема контроля времени жизни зависимостей. Например, есть компонент User и его зависимость UserSettings. Что будет если отобразить User, поменять что-то в UserSettings, и закрыть и снова открыть User?
9. Также есть проблема контроля разделяемости инстансов. Что будет, если есть список User, изменения в UserSettings затронут все User или только один? Поэтому в ангуларе придумали hierarchical-dependency-injection.
Я правда ни разу не ФП-шник, но понял из статьи, что в check была зависимость от getReservedSeats, выносим ее выше и check зависит уже от результата getReservedSeats. Просто перенесли сложность из одного места в другое.
Похоже на то, как в js, асинхронщину в виде стримов или async/await выносят выше, что б в функции оставалась чистая бизнес логика.
Я так понимаю, это делается что б было проще переиспользовать и тестировать чистые функции.
Но нечистые все-равно же никуда не денутся, они просто выше окажутся, их тестировать и мочить тоже надо, смысл тогда какой в переносе, только в упрощении слоя с чистой логикой?
В статье вроде подразумевается, что этот подход лучше, чем DI и SOLID, а чем лучше — не понятно. Как здесь решить проблему с контекстами, когда детали верхнего уровня не знают о деталях нижних уровней. В DI можно определить реализацию интерфейса выше и не прокидывать его вниз, а просто прописать зависимость в конструкторе.
Как простому фронтендщику, мне было бы гораздо понятнее, если б хотя бы два todomvc запилили на dependency rejection и на dependency injection, и сравнили плюсы и минусы.
В ад превратить можно при любом подходе.
5 в цепочке это что-то много, обычно не надо длинной цепочки. Есть dumb Button в отдельной либе и есть несколько его вариантов в вашем приложении — OkButton, CancelButton, например.
Не делают же HOC(HOC(HOC(Component))), обычно одного обертывания достаточно.
Задачи одинаковые решаются, только на tree-наследовании можно автоматизировать создание методов-точек расширения. Конечно, если не соблюдать принцип подстановки Лисков, то да, наследованием можно очень быстро выстрелить в ногу (был Button, а наследник стал Link или Input).
Наследование можно превратить в мощный инструмент, просто без него в компонентах сейчас решают проблемы иначе. В шаблонных подходах проектируют кучу точек расширения в виде свойств dumb компонента или делают его максимально простым, а в приложении потом через HOC интегрируют. Либо делают smart-компоненты-миниприложения с кучей свойств для кастомизации, вроде flatpickr. Когда нужно, рефачат его и добавляют еще свойств.
А тут все внутренности сразу открыты для расширения, причем автоматически. Что можно переопределить, решает не автор компонента, а тот, кто его использует. Захотел что-то поменять в стороннем компоненте — просто унаследуйся и переопредели.
Наследование позволяет те же задачи решать красивее, без рефакторинга исходного компонента и не создавая кучи свойств, без разделения на smart/dumb.
Там пока не видно решений этих проблем. Конечно, что-нибудь придумают, может где-то даже лучше чем в атомах. Просто это путь проб и ошибок, который им придется отчасти повторить за винтажем.
1. Большая сложность композиции, например, композиция из 10 компонент (если считать их зависимостями) вполне норм, а на бэке (если по SOLID), больше 3х зависимостей считается не очень хорошо.
2. Интенсивный рефакторинг: чем ближе к ui, тем чаще вносятся изменения, поэтому дополнительный обслуживающий код (типы, регистрация в composition root) замедляет разработку в больше степени, нежели на бэкенде.
3. Сильная иерархичность условного MVC и необходимость наличия состояния в сервисах (хоть и странно звучит), из-за которой возникает потребность более гибко управлять скоупами зависимостей и временем их жизни. Например, как в ангуларовском hierarchical-dependency-injection.
4. Необходимость использования типов для объявления зависимостей в паре с несовершенными средствами интроспекции js/ts (нельзя ассоциировать интерфейс со значением в run-time)
Если просто копировать решения из бэкенда, как это сделано в inversify (калька с C#/Ninject), то результат будет не очень хороший. Как лучше, пока никто не знает, все экспериментируют.
За отсутствием типизации и вывода типов в React.context, идея хорошая, но все-таки кажется, что бойлерплейта еще много.
1. Все интерфейсы дочерних компонент приходится объединять в интерфейсе родительского. При рефакторинге родителя, вместе с чилдами надо двигать и их интерфейсы.
2. Необходимо примешивать services к каждому компоненту
3. Все зависимости по-умолчанию жесткие. Добавили в сторонней библиотеке зависимость компоненту, а в 10 приложухах, использующих ее, теперь надо пойти в composition root и зарегить эту зависимость. Что-бы сделать зависимость мягкой, надо прикладывать усилия:
4. Как быть с зависимостями сервисов в compostion root? Вручную им их передавать? Ведь нельзя же сделать
Вы в реальном более-менее сложном приложении использовали такой подход?
1. если код оборачиваешь в Timeout, то он во время асинхронной подгрузки любого дочернего — покажет крутилку
2. если какой-нить дочерний тоже обернут в Timeout, то он будет иметь приоритет и верхний Timeout его проигнорирует
3. если код обернуть в Loading, то он вытащит статус загрузки isLoading со всего поддерева дочерних компонент
4. В рендере можно сделать throw new Promise и реакт перехватит исключение, проверит что пришел промис и перерендерит компонент при ресолве промиса. Если к этому прикрутить кэш, то код выглядит как синхронный.
Печально, что это все сильно реакт усложняет. Команде реакта можно было, например, сделать свой аналог mobx-а, запихнуть туда файберы, обработку асинхронности на исключениях и работу со стейтом.
Такое разделение ответственности позволило бы использовать эти фичи в клонах, вроде preact/inferno и других vdom и real dom библиотеках. Даже отдельного пакета можно не выкладывать, держать в проекте, а сейчас это все прибито к react-dom.
Добавив эту функциональность, они вступили на путь из граблей, по которым уже прошел vintage со своими атомами за последние года 2, есть множество проблем такого подхода. Например:
Что делать, если не promise, а observable?
Как сделать в реакте параллельную загрузку, если бросание эксепшена в render приостанавливает выполнение, а за ним может быть еще один асинхронный запрос.
Как делать retry в случае ошибки?
Как автоматизировать инвалидацию кэша, который в демках скорее всего достаточно примитивный, на каком-нибудь simple-cache-provider?
1. Типобезопасность остается, т.к. ts проверяет интерфейс заменяемых методов
2. Патч находится всегда рядом с местом создания экземпляра класса и применяется на новый экзепляр, одновременно несколько патчей тут наложить невероятно, читабельность остается приемлемой, т.к. патч рядом с созданием.
3. Патчинг app.$ похож на один из паттернов DI — ambiant context, который как раз в примерах vintage используется. Его реализация намного проще любого di, основанном на инжекции в конструктор, а возможности не хуже.
4. Класс проектируется сразу с дефолтными реализациями зависимостей, которые потом можно легко переопределить.
Инжекция через конструктор в DI хоть и делает код тестируемым, но ценой копипаста. В нормальных языках есть что-то вроде scala case classes или kotlin data classes, которые упрощают настройку объекта.
В ts такое поведение нормально не сымитировать, поэтому патчинг используется для настройки экземпляра класса. Это не иммутабельно и если этим правилом пренебречь, можно нарушить безопасность. Тут вопрос приоритетов, можно ли этим пренебречь, ведь взамен упрощается и унифицируется настройка экземпляров классов.
Сахар в ts не способствует улучшению читабельности. При создании объекта нет названий аргументов:
Что б создание объекта читалось чуть лучше, можно условиться, что аргумент — объект, то это уже ведет к еще большему копипасту аргументов и их типов:
А вот патчинг:
Мне в реальности нужно знать насколько хорошо фреймворк автоматизирует
1. Реактивность, как сделан observable state, автотрекинг зависимостей без on/off/subscribe/observe (mobx, cellx, mol_atom), работа с сервером: крутилка, обработка ошибок, retry
2. Контексты, DI или что-то вроде, что бы не прокидывать все через пропсы
3. Компоненты, как расширить уже имеющийся компонент (принцип open/close), переиспользовать с другой копией стейта
4. Как задать стили компонента и стили компонента в контексте чего-либо, как динамически управлять стилями и темизацией из js
5. Типизация (насколько хорошо используется вывод типов вместо прописывания аннотаций, работает ли в шаблонах/стилях?)
Svetle простая штука для небольших проектов, но тут пока до уровня vue ему далеко.
Хотелось бы конечно видеть полноценный todomvc (вроде такого) и сравнивать, насколько вышеперечисленные фишки просто делаются в коде.
Проблема отбрасывания лишнего кода техническая и она в природе импортов в js, поэтому приходится особым образом собирать и использовать эти модули. Это и есть friendly =).
Например, сейчас часто модули фреймворка собираются вместе в index.js, что мешает tree shaking-у. Но, в том же lodash, мы же можем так сделать: import _add from 'lodash/fp/add', есть даже плагин, которые такие импорты делает из обычного import _ from 'lodash'.
КПД фреймворка зависит от идеи в его основе, тут уж кто как угадает. Мне кажется, если автор смог такой генератор написать, то смог бы и обычный фреймворк не хуже, только tree shaking friendly.
Я бы наделал хелперов над DOM, т.к. он слишком низкоуровневый.
Посмотрим как у автора получится. Мне это видится более сложной задачей, чем написать нормальный фреймворк. Опосредованно получается, мы не сразу пишем оптимальный код фреймворка, а пишем генератор, который должен давать оптимальный код.
А вот кодогенерация создает некоторый избыточный код на каждый компонент. Да, svetle-todomvc 22кб без сжатия и минификации, но код изобилует портянками createElement/appendNode, т.е. низкоуровневой работой с DOM, которая слабо реюзается, на каждый if в шаблоне тоже генерятся приличные портянки функций.
Что будет, если приложение состоит уже из двух и более компонентов, похожих на todomvc?
Есть предел масштаба приложения, где svetle по размеру будет выигрывать, думаю его даже можно высчитать.
Временно выиграть в гонке производительности это поможет, но пока не появится нормальный инструментарий, который для любого фреймворка сможет сгенерить подобный код.
Это выглядит как очередная стадия взросления экосистемы, где пока нет нормальных стандартов, компилятора с нормальным tree shaking и языка с мощными возможностями метапрограммирования.
Почему так на этой производительности все сосредоточились: react и vue далеко не самые быстрые, но пока в большинстве задач их производительности хватает.
Если отбросить оптимизацию, которая часто приводит к увеличению объема кода, чем принципиально такая кодогенерация лучше обычной сборки какого-нибудь todomvc на preact ролапом (кода кстати тоже около 3кб в gz)?
Почему быстро бросится в глаза, а в SOLID значит не быстро? Это разве не от строгости языка зависит?
Как без DI быть с абстракциями, когда где-то наверху надо сказать, замени на 10м уровне вложенности класс X на Y, не влезая в иерархию деталей нижних уровней.
Relay, Apollo — достаточно тяжелые решения. Кроме того, что могут атомы, они дают optimistic updates, проверку целостности по схеме, сервер. Но если эти навороты не нужны, кода будет значительно меньше.
Т.к. атомы — легковесная абстракция между fetch и компонентами, то вместо fetch вполне может быть какой-нибудь graphql-js или apollo-client. Тут как раз больше возможностей для масштабирования.
На мой взгляд, логично переложить часть функциональности из Relay-подобных решений, в mobx-подобные решения. Общеизвестных библиотек для работы с состоянием все-таки меньше чем фреймворков для рендеринга. Вместо биндингов к реакту и прочим, можно было бы сделать пару биндингов от graphql к этим библиотекам.
Кода больше надо писать, указывать зависимости вручную.
Как обеспечить типобезопасность, ведь классы аргументов передаются отдельно в deps.
Или bootstrap-код теперь сам компилятор генерит?
Если дело только в скорости было, то можно было просто отказаться от тормозного Reflect.defineMetadata и генерировать более оптимальные метаданные.
Зачем делать зависимости только явными?
'user, dictionary' — строковая типизация.
getState('user.name'); — строковые селекторы, что будет с путями при рефакторинге стейта?
Store.addHandlers — как быть, если нужна копия стора с другими путями в стейте?
Redux сложный? По-моему, как раз одно из самых простых решений (если сравнить реализацию с тем же mobx), наивная реализация пишется строк в 100.
State-tree контейнеров десятки в js, зачем еще один? Вот mobx-state-tree неплох, разве он такой сложный, что надо писать свой велосипед?
Попробуйте избавиться от синглтонов, в редуксе как раз все через фабрики создается: createStore и т.д.
Попробуйте использовать типы, без типов рефакторинг приложения — как хождение по минному полю. Сделайте так, что б при изменении в объекте user свойства name на firstName, typescript или flow подсвечивали ошибку в селекторе. В том же mst типы выводятся.
2. Как во время рефакторинга узнать, что за реализация стоит за тем или иным строковым ключем-зависимостью? В какой папке искать Salad? Как при написании кода а не в run-time, узнать, что существует класс, который пытается резолвить DI?
3. Как в большом проекте вы решаете проблему дублирующихся имен классов? Есть какие-то составные имена, соглашения по именованию, неймспейсы?
4. Задача подгрузки кусков кода в DI не очень понятное решение. Разве подгрузка чанков бандла такая частая задача, что ее надо добавлять в ядро DI, задача которого собирать код, а не загружать из сети? К примеру, можно разделять приложение на несколько микросервисов или явно прописать загрузку в странице через роутинг и фетч, это же обычно достаточно в одном месте приложения сделать.
5. Добавили асинхронность, как обрабатывать ошибки загрузки, делать тот же retry, рисовать loading, пока код подгружается, отменять загрузку, если во время загрузки первой страницы перешли на вторую?
6. Конвенций для создания инстансов придумывать не надо, достаточно начать использовать typescript или flow+babel и генерить метаданные из аргументов в конструкторе, а через new внутри DI создавать инстансы, как делает Inversify или angular.di, разве нет? Зачем портянки невалидируемых конфигураций (в Java/Spring или PHP/Symfony для этого есть мощные плагины для idea), если можно метаданные вытаскивать из типов/интерфейсов?
7. Как вашу реализацию заставить работать с компонентами, тем же реактом? В Inversify есть хотя бы inversify-react.
8. При применении DI к компонентам вылезает проблема контроля времени жизни зависимостей. Например, есть компонент User и его зависимость UserSettings. Что будет если отобразить User, поменять что-то в UserSettings, и закрыть и снова открыть User?
9. Также есть проблема контроля разделяемости инстансов. Что будет, если есть список User, изменения в UserSettings затронут все User или только один? Поэтому в ангуларе придумали hierarchical-dependency-injection.
Похоже на то, как в js, асинхронщину в виде стримов или async/await выносят выше, что б в функции оставалась чистая бизнес логика.
Я так понимаю, это делается что б было проще переиспользовать и тестировать чистые функции.
Но нечистые все-равно же никуда не денутся, они просто выше окажутся, их тестировать и мочить тоже надо, смысл тогда какой в переносе, только в упрощении слоя с чистой логикой?
В статье вроде подразумевается, что этот подход лучше, чем DI и SOLID, а чем лучше — не понятно. Как здесь решить проблему с контекстами, когда детали верхнего уровня не знают о деталях нижних уровней. В DI можно определить реализацию интерфейса выше и не прокидывать его вниз, а просто прописать зависимость в конструкторе.
Как простому фронтендщику, мне было бы гораздо понятнее, если б хотя бы два todomvc запилили на dependency rejection и на dependency injection, и сравнили плюсы и минусы.
Я не знаю ваших реальных задач, но по-моему все-таки лучше избегать optional.
Можно словари использовать, нормализовывать данные сразу, иначе где-нибудь все равно боком вылезет. Со строгой типизацией хотя бы flow/ts подскажут.
Если нет объекта, который надо отобразить — это исключительная ситуация, зачем что-то проверять?
dumb — простые компоненты, где только представление, обычно чистые функции. Подробнее в статье Presentational and Container Components
.
5 в цепочке это что-то много, обычно не надо длинной цепочки. Есть dumb Button в отдельной либе и есть несколько его вариантов в вашем приложении — OkButton, CancelButton, например.
Не делают же HOC(HOC(HOC(Component))), обычно одного обертывания достаточно.
Задачи одинаковые решаются, только на tree-наследовании можно автоматизировать создание методов-точек расширения. Конечно, если не соблюдать принцип подстановки Лисков, то да, наследованием можно очень быстро выстрелить в ногу (был Button, а наследник стал Link или Input).
А тут все внутренности сразу открыты для расширения, причем автоматически. Что можно переопределить, решает не автор компонента, а тот, кто его использует. Захотел что-то поменять в стороннем компоненте — просто унаследуйся и переопредели.
Наследование позволяет те же задачи решать красивее, без рефакторинга исходного компонента и не создавая кучи свойств, без разделения на smart/dumb.