Комментарии 42
А вот со статьёй вы меня опередили, моя только наполовину написана, и она очень пересекается с вашей.
Интересно какой процент разработчиков используют чистую архитектуру в веб-приложениях. У меня есть проблема с подбором разработчиков, большая часть кроме редакса ничего не знают и знать не хотят. Как вы решаете эту проблему?
«У меня есть проблема с подбором разработчиков, большая часть кроме редакса ничего не знают и знать не хотят.» — честно говоря, я не сильный фанат Redux'a. Если приложение пишет больше, чем 1-2 человека — может начать путаница из-за сваливания состояния. Кто-то где-то что-то отправляет (не дай Бог из UI'я) — и искать это проблематично, все состояние находится в куче. К тому же, Redux особо-то и не отобразишь на UML диаграмме, например. Какой-нибудь Repository\Entity с шаблоном Observer лучше поддается понимаю и все состояние не сваливается в кучу. Это лично мое мнение, но я его избегаю. Аналогичный подход с Redux Saga — UseCase\Repository + View Model на async\await более красиво решают эту проблему и цепочку вызовов видно напрямую.
«Как вы решаете эту проблему?» — только учить. Специалисты стоят довольно дорого и проекты не всегда имеют для них бюджет. Поэтому объяснение + код ревью. Чистая архитектура не сложная вещь (вообще, сложные вещи в разработке с трудом приживаются и тяжело поддерживаются) — поэтому разработчики довольно быстро ее понимают и со временем начинают использовать без проблем.
Ну не знаю…
Пилю средне-большие проекты на react-redux и никаких архитектурных проблем не встречал. Как по мне это overengeenring, т.к. в основном все правила бизнес логики должны быть реализованы на бекенде. А ради валидации формы пилить такой огород классов и связей…
ИМХО на бекенд/native приложения это ложится гораздо лучше.
Изначально я перешел на такую архитектуру, потому что логика в UI крайне тяжело поддается тестированию, а UI\ViewModel\Presenter слои начинают становиться God-объектами с дублирующимся кодом.
«ИМХО на бекенд/native приложения это ложится гораздо лучше.» — вообще, эту архитектуру придумали изначально для сложных бекенд приложений :). Поэтому часто для мелких фрондент проектов она избыточна — но, опять же, зависит от проекта. Как писать большой проект без чего-то подобного, оставляя его поддерживаемым, я не представляю (а ведь кто-то и fetch из onClick делает...)
А в чём собственно сложность тестирования? Если мы например берём redux, то там всё достаточно просто. Это ж просто чистые функции.
И ещё интересно, где в такой архитектуре хранилище? В бекендах хранилищем обычно выступает БД. Вам же всяко информацию где-то надо хранить (надеюсь не предполагается этого делать в моделях). И тут приходим к тому что всё равно нужно что-то типа redux (единый источник правды), иначе будут проблемы с неконсистеными состояниями и всем тем что react сообщество успешно побороло 4+ лет назад
Мне кажется, даже размер команды, и размер приложения это ещё не повод выбирать именно чистую архитектуру. Как вариант, приложение можно разбить на несколько разных модулей (lerna/monorepo), контролируя тем самым их размер. И это будет тоже довольно просто поддерживать и расширять.
«И ещё интересно, где в такой архитектуре хранилище?» — слой Repository в 95% случаев. И тем, кому нужна авторизация — дергают авторизацию по интерфейсу.
«Как вариант, приложение можно разбить на несколько разных модулей» — обычно, в приложении есть те или иные зависимости от общей логики, которые тяжело вынести.
«Как вариант, приложение можно разбить на несколько разных модулей (lerna/monorepo), контролируя тем самым их размер.» — возможно. Скажу честно, я таким не пользовался, а мой подход с чистой архитектурой лишь один из многих. Как я сказал, я лишь описываю свой опыт и лично я не люблю Redux, потому что для меня и моей команды было проще работать, используя другой подход. Но! Redux зарекомендовал себя и ничего против его использования я не имею. Так что я не отрицаю другие подходы, хотя и использую приведенную выше архитектуру, потому что она оказался самым удобным для нас решением.
То, что вы написали — это прекрасно и часто абсолютно необходимо для очень сложных UI, и лютый overengineering для простых вещей, типа формы о двух инпутах.
Плюс, если отойти чисто от архитектуры и перейти к вашему коду — мне, например, страшно не нравятся ваши ViewModelImpl. Там кругом мутабельность и неконсистентный стейт (дёрнули validate() — в модели одно, не дёрнули — другое), разбираться в этом всём при росте кода — будет убийственно. Если у вас в модели нет single source of truth — ни к чему хорошему это никогда не ведет.
«и лютый overengineering для простых вещей, типа формы о двух инпутах.» — разумеется, пример сильно надуманный, чтобы показать использование. Но! Если предположить, что этот код будет сильно меняться, а также писаться большим количеством людей — мы можем начать покрывать тестами на разных слоях, и избежать будущих проблем. Вообще, архитектуры\тестирование приобретают большую силу, именно когда проект пишется большим количеством людей на протяжении 1+ лет. Разработчики как раз начинают забывать свой код...
В общем, it depends. Но пример и правда с overengineering'ом, но только для того, чтобы объяснить архитектуру на примере.
«Плюс, если отойти чисто от архитектуры и перейти к вашему коду — мне, например, страшно не нравятся ваши ViewModelImpl.» — разумеется, чем больше код и сложнее логика, тем сложнее его читать — но глобальных проблем с этим я раньше не замечал. God-объектов в View Model я избегаю за счет Use Case'ов и внутреннего слоя. Можете привести пример, как бы Вы это реализовали?
А что на *Impl
ответите? Я бэкендщик, но иногда приходится писать и на фронте, описанный вами подход много ближе ложится в привычные бэкенд подходы, чем то, что продвигает редакс.
Но вот этот момент и *Impl
мне кажется несколько оверинжиниринговым.
«описанный вами подход много ближе ложится в привычные бэкенд подходы» — как я написал в комментарии выше, эта архитектура изначально придумана для бэканда, потому что именно он обладает сумасшедшей сложностью. Но, помимо бэканда, ее можно применять везде, где возврастает сложность.
Опять же, как я написал выше, "основную силу архитектуры\тестирование приобретают в больших проектах с несколькими разработчиками, где нужно понимать и изменять чужой код". Если вы говорите, что это оверинжениринг для чего-то относительно маленького и даже среднего — я с Вами абсолютно согласен
Ну, с тем замечанием, что вся ViewModel — это первейший кандидат на вылет при упрощении кода.
Весь фронтенд это:
— Слой view
— Слой состояния
— Слой бизнес логики
Бизнес логика изменяет состояние, на измененное состояние реагирует view слой и рендерит то, что должно быть отрендерено при текущем состоянии. Вот и всё, это настолько просто, что я даже не знаю, ну как гвоздь в доску забить. Причем в не зависимости от того, какой проект, большой или маленький.
P.S. посмотрите в сторону MobX, уверен от этого ваша жизнь станет гораздо проще. То, как вы используете локальный стейт компонентов и то, как из вне им манипулируете используя всякие подписки и колбэки, это конечно жесть.
https://github.com/RostislavDugin/clean-architecture-react-typescript/pull/3/files вот набросал по быстрому использование mobx. Может кому будет интересен такой более прагматичный подход
action всё же лучше не забывать, потому что здесь, например, у вас семантика поменялась — с action неявный notifyListeners будет вызван после завершения метода (как и у автора), а у вас — дважды, после каждого присваивания в observable, так что утекает неконсистентный стейт.
public onSignedOut(): void {
this.isAuthorized = false;
this.authToken = '';
//this.notifyListeners();
}
А в целом да, с mobx (и mobx-state-tree) подход получается очень похожий на описанный автором. Отдельно — store с моделями, которые те же entities, зависящие от них контроллёры частей приложения, далее — зависящие от них реакт-контейнеры, которые рендерят тупые реакт-компоненты. API и storage так же можно вынести за интерфейсы и инжектить.
import { configure } from 'mobx';
// Configure MobX to auto batch all sync mutations without using action/runInAction
setTimeout(() => {
configure({
reactionScheduler: (f) => {
setTimeout(f, 1);
},
});
}, 1);
P.S. В некоторых кейсах, если это сразу синхронно объявить, то реакции могут барахлить, поэтому нужен setTimeout.
Проверка в действии — codesandbox.io/s/sad-turing-7vkpz?file=/src/index.js
Хм, интересный хак. Но например с тем же реактом есть определённый плюс в синхронной отрисовке — можно поменять стейт в методе компонента через экшены и потом при необходимости сразу ручками лезть в dom, без всяких заморочек с ожиданием апдейта компонента. Да и явное, как известно, лучше неявного :)
Но за трюк спасибо, может и пригодится как-нибудь.
Допустим, у вас есть какой-то достаточно сложный компонент (например, какой-то список), который изменяет стейт, а изменение стейта вновь изменяет этот компонент (добавляет элементы в верх списка). И вы хотите, например, запоминать позицию скролла.
addElements() {
const scrollH = this.el.current.scrollHeight // старая высота элемента со списком
this.props.store.addElementsToTop() //action в сторе, который триггерит ререндер текущего компонента
this.el.current.scrollTop += this.el.current.scrollHeight - scrollH //прокручиваем список на старую позицию
}
Если у вас честный action, сразу после его вызова (2 строчка) компонент (если он observer, конечно) синхронно перерисуется и в третьей строчке scrollHeight будет уже актуальным. С вашим же трюком на третьей строчке компонент будет ещё не перерисован (потому что observer внутри — это та же реакция).
this.el.current.scrollTop += this.el.current.scrollHeight - this.lastScrollH;
Будет работать ровно так, как и ожидается.
Не знаю в чем вы проблему увидели)
Да оно понятно, но всё равно не так удобно, как когда всё в одном месте. Опять же, в одном компоненте таких эффектов и больше одного может быть, и все они будут в didUpdate, оторванные от контекста действия, плюс ещё дополнительные поля в классе… В общем, синхронное выполнение как-то лучше выглядит. Ну и с явными экшенами появляется дополнительное разделение на действие (когда что-то изменяется) и просто параметризованные геттеры — то есть функции, которые просто что-то вычисляют.
Вот посмотрите codesandbox.io/s/sweet-kowalevski-wursj
Да вот сидел и твердил себе"не забыть проставить, когда их переименовывать буду", но решил не переименовывать, хотя и режет глаз on*
На след. выходных я обязательно в нем разберусь. Спасибо, что потратили время и показали свое видение
P.S. Но отдельный момент — мой вариант, разумеется, не идеальный. Но он работает и понятен другим разработчикам, есть консистентность в проекте. Подходов много и мой — лишь один из них. Стремиться к идеальному, конечно, нужно, но идеальное — враг хорошего.
И Ваш код с MobX, как по мне, очень даже сильно дополнит архитектуру.
Всегда удивлялся, почему программисты так любят писать код. А потом долго объяснять, как это работает и сколько классных архитектурных решений использовали. Зачем столько кода? Его можно уменьшить в десятки раз и он станет проще, понятнее и его легче будет сопровождать.
Аналогичным образом можно разрабатывать приложения, если выстроить соответствующий тех. процесс. Важно, чтобы изготовление каждого слоя в отдельности имело под собой какой-то смысл. Такой как специализация сотрудника или специфичный домен.
Честно говоря, я ни разу не встречал подобных решений на фронте. Здесь разработка делится по фичам/страницам/экранам и для каждой ведётся в одну каску, максимум две — CSS и JS. По аналогии, это больше напоминает пельмени: тесто, фарш. Поэтому ритуалы с дополнительными слоями внутри, как в тортах, кажутся несколько избыточными.
Clean Architecture это крутая книга, где автор рассказывает о принципах разработки сложных систем, показывая примеры решений на разных уровнях абстракции — от самых нижних на уровне кодинга, до самого верха на уровне управления конфигурациями и релизами. Только не нужно принимать примеры за универсальные догмы, как это произошло с шаблонами проектирования.
Архитектура в большинстве случаев определяется структурой команд и тех.процессами.
Скорее они стремятся друг к другу.
Честно говоря, я ни разу не встречал подобных решений на фронте. Здесь разработка делится по фичам/страницам/экранам и для каждой ведётся в одну каску, максимум две — CSS и JS
Если фронтов несколько (хотя бы пользовательское приложение и админка), то вполне может быть что в три и более "каски": связь с бэком в том или ином виде, расшариваемая между фронтами и два UI.
Какое же огромное количество подходов крутится вокруг разработки на React.
Я не видел нигде полного руководства о том, как стоит строить гибкую архитектуру в React. Эти знания раздроблены по разным местам и все делают этот как хотят. И это очень хорошо, что автор поднял этот вопрос. Если кто-то знает такие ресурсы скиньте пожалуйста в комментарий.
Однако данный подход мне не сильно понравился. Он явно нарушает принципии KISS и YAGNI. Создается огромное количество абстракций которые создаются не потому, что они нужны и улучшают читаемость, а потому, что так требует архитектура.
Он слишком сильно завязан на ООП. React же больше не об ООП. Концепция обработки логики с помощью redux + saga куда более близка к духу React. Та же иммутабельность, те же редьюсеры и чистые функции. Если хотите ООП используйте идею сервисов из Angular и MobX.
В статье очень много рассказывается о том как строить архитектуру для организации логики. Но что насчет UI компонентов? Об этом ничего не написано, но если не думать о UI компонентах, то они очень скоро начнут дублироваться, появится 10 реализаций одного и того же в проекте и это усложнит поддержку. UI компоненты тоже нужно распределять по уровням абстракций и переиспользовать. Я больших проектах я рекомендую Атомарный дизайн
Я переписал это приложение, сделав ближе к тому, что я считаю чистой архитектурой, но при этом оставил точно такое же поведение. Дополнительных библиотек не использовал, логику вынес в сервис. Вот, что у меня получилось:
https://github.com/VladislavMurashchenko/clean-architecture-react-typescript/
«Я больших проектах я рекомендую Атомарный дизайн» — можете продублировать ссылку? К сожалению, не открывается
«Я переписал это приложение, сделав ближе к тому, что я считаю чистой архитектурой» — на след. выходных я выделю время, чтобы разобраться, интересный подход
habr.com/ru/post/249223
bradfrost.com/blog/post/atomic-web-design
Проблема таких статей всегда в том что плохо когда удается показать сложные подходы на простых примерах. Это как собирать лунный велосипед, а используемые подходы демонстрировать на обычном. Оно будет смотреться очень нелепо, переусложненно а иногда и забавно. Оценят демонстрируемые решения только те кто уже занимался похожими разработками. В то-же время выложить целый проект в open-source для демонстрации подхода очень затруднительно.
Почувствовал себя в 2005. Лютый over engineering. Ну да бог с ним. Почему у вас:
- нет автоматической реактивности. Вы всё делаете руками. Все подписки, все привязки, все уведомления. Как в каменном веке. Любая механическая ошибка и сиди дебаж что и почему сломалось
- почти нет декларативности (кроме типов). Уже буквально любой современный подход предполагает либо указание зависимостей (react), либо детектирование зависимостей от их использования (vue, mobX, knockout). Это сильно упрощает жизнь и даёт большие возможности в оптимизациях производительности.
- зачем вам React? Если вы застряли в 2005г и пишете в духе Backbone, то возьмите легковесный строковый шаблонизатор, зачем тащить целый React туда, где вы его на 3% используете. React + React.dom отнюдь не невесомые. Это очень большие и довольно сложные кодовые базы. Они решают те задачи, которые вы на них не накладываете.
- так много бойлерплейта. Вы даже redux переплюнули в этом деле
По сути вы добились того, что у вас можно изъять React из приложения и заменить его чем-нибудь другим. Сделали вы это за счёт того, что используете React как dump components renderer. Из пушки по воробьям. Зачем? Ну да, теперь его можно выкинуть. Дак выкинули бы с самого начала тогда. Хорошо хоть не Angular. Вместо привязки к, о боже, чужим фреймворкам, у вас теперь большая привязка именно к вашему собственному фреймворку. Не то чтобы это было плохо, но записать в преимущества это будет сложно.
Из позитивных моментов:
- У вас много изолированных друг от друга слоёв. Это правда про clean code
Из негатива:
- Вы перестарались, и цена фичи выросла раза в 4. Даже в Redux меньше "капусты" и копипасты
- Высокий уровень входа для новичков (в меру высокий)
- Слишком много ручной работы, а следовательно высокая цена кода и большое пространство для формирования багов
Добавьте хотя бы какой-нибудь dependency tracking и готовый (да пусть даже самописный) observable. Не пишите notify
руками. Пожалейте ваших коллег.
Ещё рекомендую, если вам таки нужен React, именно в компонентах и писать ViewModel. Он собственно для этого и создан. В качестве разделения сложной логики (когда надо) можно исопльзовать кастомные хуки-зонтики. Их же в итоге можно и тестировать изолированно. Ну и реактивность из коробки.
P.S. Прошу не воспринимать близко к сердцу. Просто сложилось впечатление что про clean code и паттерны вы читали, а про современные подходы во фронтенде нет. В итоге на слои вы нарезали, но удобного и практичного решения в духе времени не получилось. Примерно так и писали во времена Backbone.js
«Вы перестарались, и цена фичи выросла раза в 4. Даже в Redux меньше „капусты“ и копипасты» — опять же, пример надуманный и здесь действительно много overengeneering'a.
«Высокий уровень входа для новичков (в меру высокий)» — не согласен. Вопрос изучения — максимум пару дней + поправка на общий опыт. В сравнении с тем, что пишут вообще без какого-либо подхода — я считаю, что архитектура выше большой шаг вперед. Собственно, и статью я написал потому, что не увидел других общепринятых подходов. Или только с акцентом на библиотеку, или вообще никак.
«Слишком много ручной работы, а следовательно высокая цена кода и большое пространство для формирования багов» — это тема для второй статьи. Собственно, во второй статье будет добавлен MobX и часть ручной работы отпадет.
Ключевая идея статьи в том, что серебряной пули нет — но есть такая, как описаны выше. И свои задачи она решает может не всегда идеально, но решает и в большинстве своем закрывает свои задачи.
Если у Вас будет время, можете рассказать, как бы Вы изменили архитектуру выше? Может просто созвониться и обсудить\пообщаться в Telegram. Я несомненно ищу пути улучшения подхода, который использую и статья — один из способов его улучшить в какой-то мере
Если у Вас будет время, можете рассказать, как бы Вы изменили архитектуру выше?
- Добавил бы observables (можно и MobX) и постарался бы сделать декларативным почти всё что получается с пользой для дела сделать декларативным
- Как можно больше бы всё унифицировал и уделил большое внимание тому, чтобы не писать так много копипасты
- Вынес бы VM в React компоненты (он для этого и создан). Ну или хотя бы в MobX слой. Да это создаёт большую привязку к фреймворкам\либам, зато сильно упрощает разработку (ура!).
- Продумал бы возможность частичной подписки на данные, а не сразу на всё разом
Затем попробовал бы это в деле с годик. По ходу пьесы наткнулся бы на 100500 неудобных мелочей и не только. Сел бы и подумал как правильно решить эти проблемы. Уже зная в мелочах в чём эти проблемы. И снова по кругу. Придумал идею, попробовал на мелком модуле. Если зашло — используем. Если нет — ищем дальше...
Но всегда держал бы в голове, что clean code это не догмы. Это советы, руководство к действию. В первую очередь код должен решать поставленные перед ним задачи. Если какой-то pattern или правило создаёт много проблем, то надо сесть и подумать что не так. И решить — это правило не для нас\мы делает что-то не так.
Руководствуясь "правильными принципами" легко загнать даже мелкую кодовую базу в такой кромешный кровавый ынтерпрайз, что в нём дыхнуть сложно будет без уведомления 5 инстанции, изменения 5 интерфейсов, и 25 тестов. Любой инструмент, будь то какая норма, или правило, или библиотека, должен приносить больше пользы, чем страданий.
MobX по идее и обеспечивает частичную подписку на данные.
Как observables сочетается с декларативностью. observables — это её часть или ещё нет?
Ну кодовая база же не состоит только из observable примитивов. Где-то есть какие-нибудь конфиги, схемы и пр… Сделать некую структуру\схему, которая, путём запуска её в неком интерпретаторе, избавляет от рутины и всё делает однообразным образом. В первую очередь я имел ввиду это.
Во вторую, в моём понимании, есть что-то промежуточное между декларативным и императивным подходом. Не знаю как это правильнее назвать. Ну скажем декларирование зависимостей в observables или автоматическое их выявление мне кажется более декларативным, нежели руками через if-ы. Тот же useEffect мне кажется более декларативным, нежели груда if-ов в componentWillReceiveProps. Как это по уму обозвать я не знаю.
Совет №4 тоже сомнителен, хотя в текущих реалиях ($mol пока не в них) безальтернативен
Если в приложении есть места под нагрузкой, то без точечных зависимостей будет сложно обеспечить должную производительность. А ещё когда всё rerender-ится на любой чих целиком — бывает сложно дебажить. Не знаю почему это "сомнительно".
Я сам стремлюсь к балансу простоты, масштабируемости и уменьшению бойлерплейта. Но, использую Flux-подобный подход. Возможно, вам будет интересен мой пример:
github.com/sergeysibara/mobx-react-4-tier-architecture
codesandbox.io/s/mobx-react-lite-4-tier-architecture-ue2v1
Файловая структура по большей части by features.
Код разделен на 4 основных слоя:
- view — компоненты
- store — слой глобальных данных. Для каждой feature свой стор
- actions — middleware слой для побочных эффектов.
- api — слой взаимодействия с сервером
view взаимодействуют только со сторами и actions. Компоненты могут подписываться на сторы, но не могут их изменять. Изменять стор могут только actions.
стор не знает ни о каком слое, ни о других сторах. К нему за данными могут обращаться компоненты и actions. Ну и подписаться на его изменения.
В сторе хранятся только глобальные данные и getter-ы/setter-ы для них. Плюс логика для преобразования этих данных (например, фильтрация). Никакой сторонней бизнес-логики, не относящейся к работе с этими данными, здесь быть не должно.
actions вызывают api и обрабатывают полученные данные, а также обновляют сторы. Могут как считывать, так и изменять сторы. Различные сопутствующие сайд эффекты также в этом слое.
api ничего не знает о других слоях. Единственные назначения – взаимодействие с сервером и преобразование данных в нужный формат перед отправкой и после получения.
В моем примере классы сторов, api, actions можно наследовать и переопределять или использовать свои, если базовые не подходят. В базовые классы я выношу стандартный общий функционал для REST (получить отфильтрованный и отсортированный список, получить один элемент, создать новый элемент, изменить существующий, удалить элемент).
При добавлении новой страницы или группы страниц, относящиеся к одной feature, в основном изменения будут в пределах одной папки, как в моем примере: src/pages/todos. В ней находятся унаследованные стор, api, actions и компоненты, относящиеся только к функционалу todos.
В ближайшие недели хочу разобраться во всех подходах, которые мне посоветовали\дополнили и дополнить свою архитектуру (на очереди — MobX).
Обязательно посмотрю Ваш репозиторий, не удаляйте его в ближайшее время, пожалуйста :)
Но чем больше кода и элементов в системе, тем сложнее её разрабатывать и поддерживать, ведь появляется всё больше мест для совершения ошибок.
Через 5 лет мучений на разных СТМ и подходах с реактом я пришел к Effector, и три года искал способы писать код в большой команде. Оказалось, что его модели можно класть рядом с VM-компонентом не обременяя себя проблемами огромной кодовой базы. Каждый разработчик у нас в команде думает лишь о двух вещах: как реализовать UI без привязки к логике и как реализовать логику не думая о UI. При этом никто не хочет думать об особенностях CleanArchitecture, мы лишь думаем о данных и событиях, грубо говоря о потоках данных.
Можем пообщаться в Telegram и обсудить подходы, которые мы используем в production. Мне несколько неловко писать огромные статьи на хабр.
The Clean Architecture на TypeScript и React. Часть 1: Основы