Комментарии 38
Очень много возникло вопросов и по форме и по содержанию, поэтому попробую предложить свой отзыв.
FSD запрещает прямые зависимости между фичами. Приходится изгаляться через shared или создавать искусственные слои
Вот тут бы хотелось сразу получить наглядный пример. Потому что с одной стороны для кроссимпортов есть ясных механизм. А с другой стороны импорты между фичами в принципе конфликтуют с идеей, что фичи это код объединенный общей целью и не связный с другими фичами.
Пример который вы предоставили сразу ставит под сомнение, что предложенный функционал написан с пониманием фсд.
Куда положить NotificationService? В shared? Но он используется только в конкретных фичах.
Тут тоже хотелось бы ясный пример. Если вы сами пишете, что он используется только в фичах, то располагать его нужно на уровнях ниже. И судя по названию, ему действительно прямая дорога в shared.
Тестирование — не самое удобное
Вот такая же фигня. Вы даете пример моков на уровне модулей! Т.е. проблема вообще с fsd никак не связана. Данный пример говорит о том, что проблема в написании кода, который полностью зависит от реализации модулей. И даже в этом случае, скорее всего у вас будут моки в файлах, а в самих тестах просто какая-то конфигурация замоканного модуля под конкретный тест.
Переиспользование логики — боль. Когда похожая логика нужна в разных фичах, приходится либо дублировать код, либо выносить в shared и терять контекст фичи.
Понять проблему вообще не удалось. И понять причем тут fsd тем более. Причем тут shared? О каком контексте фичи идет речь? А это точно две разные фичи? Зачем дублировать код?
Основная идея Clean Architecture — инверсия зависимостей. Бизнес-логика не зависит от деталей реализации (UI, API, хранилище), а наоборот.
Неплохо было бы добавить врезку об Чистой архитектуре гугл в андроид приложениях. Потому что основная идея там ровно такая же, т.е. прям один в один как и в fsd. Т.е. приложение состоит из ясного набора слоев, каждый из которых имеет ясное назначение и отношения к другим слоям. И названы эти слои и их назначение.
Моя версия Clean Architecture
Если я правильно понял у вас получилась: app->features->core, что очень похоже на fds: app->features->share. Если я правильно понял, то отношение между слоями у вас ровно такие же, но вашей архитектуре нет слайсов (или вы так думаете)
Каждая фича строится из трёх слоёв:
Вот поэтому мне и не хватает врезки по Чистой архитектуре гугл. Потому что получается у вас приложение делится на Части, Часть с названием feature делится еще на Части, при этом каждая Часть делится на Слои.
1. Domain слой — сердце приложения. Здесь живут абстракции, которые не зависят от конкретных реализаций:
У нас сердце приложения, по идее, это Часть с название core, а Domain слой - это слой в конкретной Части в Части под название feature. У вас даже название выбрано соответствующее: core - ядро.
Вот тут любопытно поискать аналогии в fsd. Все таки цель статьи уйти от fsd к clean arch...
Думаю если проводить аналогию, то абстракции, которые никак не зависят от реализации и при этом используются в разных частях приложения - это сущности из слоя shared в fsd. Они могут быть навеяны конкретной предметной областью, но не являются конкретными реализациями.
У use case есть функция executor которую принятно обычно называть execute. Это как раз функция выполняющая действия характеризующее действие пользователя
Вообще, если у вас эта штука является частью архитектуры, то должна быть какая-то абстракция, которая контроллирует форму всех use cases. По моему, такая себе история писать export class LoginUseCase {}
при этом форма LoginUseCase никак не контроллируется.
Профит: Use Case — это сценарий использования приложения. Можешь добавить кэширование, логирование, аналитику — всё прозрачно и тестируемо.
Если в рамках fsd вы так же создавали сущности, которые на вход принимают зависимости там бы тоже все было прозрачно и тестируемо и не пришлось бы мокать модули.
2. Data слой — реализации. Здесь мы реализуем абстракции из Domain слоя. Сначала Network слой с проверкой типов:
Очень похоже на слой Entity из fsd
export function useStateFlow<T>(stateFlow: StateFlow<T>): T {
const [state, setState] = useState(stateFlow.value)
useEffect(() => {
// Подписываемся на изменения состояния
const unsubscribe = stateFlow.subscribe(setState)
return unsubscribe
}, [stateFlow])
return state
}
По моему, выглядит несколько избыточным. Я бы присмотрелся к useSyncExternalStore, тогда можно было бы убрать useStateFlow, useFlow.
Скрытый текст
// view/LoginPage.tsx
export const LoginPage = () => {
const viewModel = useViewModel(LoginViewModel)
const state = useStateFlow(viewModel.state)
// Подписываемся на события UI (не состояние!)
useFlow(viewModel.uiEvent, (event) => {
switch (event.type) {
case 'navigate_to_main':
navigate('/dashboard')
break
case 'show_toast':
toast[event.payload.type](event.payload.message)
break
case 'open_email_confirmation':
openModal('email-confirmation')
break
}
})
...
}
Вот это я не очень понял. Вы начали статью:
Если ты используешь FSD или до сих пор пишешь всю логику в компонентах React — эта статья точно для тебя.
Но в вашем примере вы написали логику обработки события модели прямо в компоненте при том, что эта логика вообще не имеет ни одного замыкания на LoginPage.
И второе, а зачем вообще в таких случаях нужен useFlow?
Скрытый текст
...
useEffect(() => {
const unsubscribe = viewModel.uiEvent.subscribe((event) => {
switch (event.type) {
case 'navigate_to_main':
navigate('/dashboard')
break
case 'show_toast':
toast[event.payload.type](event.payload.message)
break
case 'open_email_confirmation':
openModal('email-confirmation')
break
}
})
return unsubscribe
}, [viewModel])
...
Это, конечно дело вкуса, но вроде так куда выразительнее.
Простое тестирование — ViewModel тестируется без UI, а UI — без логики
Тут другой вопрос, а нужно ли их тестировать без компонентов? Их существование обусловлено требованием какого-либо компонента. Если в системе нет компонента, который бы требовал какой-то метод этого объекта, то тестировать его не нужно, по идее.
Далее уже не буду комментировать ограничен во времени, а вовсе не пренебрегаю написанным.
Но в целом выглядит так, что можно взять fsd и добавить туда какой-нибдуь вид DI и будет ничем не хуже. А даже лучше, потому что не нужно будет самому придумывать терминологию, и выбирать иерархию строительных блоков.
Что-то типа в app идет линковка токенов и конкретных классов для di. Абстракции без реализации в shared или entity. Количество кроссипортов будет сведено к минимуму.
Вот тут бы хотелось сразу получить наглядный пример. Потому что с одной стороны для кроссимпортов есть ясных механизм. А с другой стороны импорты между фичами в принципе конфликтуют с идеей, что фичи это код объединенный общей целью и не связный с другими фичами.
Я буду говорить про мобильное приложение (это для того, чтобы было представление о визуале).
К примеру:
Имеем: Чат, Урок, Плеер.
Чат это полноценная фича. Чат может быть типов: Учебный, Поддержка (Консультант), Групповой.
Урок это тоже полноценная фича.
Плеер это не фича, но имеет большую кодовую базу. Имеет превьюшки и открывается поверх как в Ютубе.
Теперь у нас задача от бизнеса:
В компоненте урока должен быть групповой чат (не на странице, урок переиспользуется на разных страницах).
В учебных чатах (не на странице, не на списке чатов) должен быть список уроков из той же дисциплины, к которой принадлежит урок. Это для того, чтобы быстро навигироваться между чатами и можно было открыть урок. Так же нужно в этом списке отображать статус задания (сдал/отклонено/на проверке).
Когда видео открывается на весь экран, оставшуюся часть экрана должен занимать чат по уроку.
И вот если с 3 проблем особых нет и решается через рендер функцию, то в 1-2 появляется цикличная зависимость.
И вот тут начинаются пляски с тем, как сделать лучше, кого куда вынести, где что создать, как меньше написать бойлерплейта =)
К сожалению не удается представить проблему. Если у вас есть набор фичей чат, урок, плеер, то зачем в урок добавлять плеер? Звучит как то, что у вас должен быть разный набор виджетов.
Если вы в урок добавляете чат - это уже не фича, а две фичи в одной. И звучит так, что в слое фичей такой конфигурации делать нечего. Фичи переиспользуются на уровнях виджетов и страниц.
Проблема в терминологии =)
В данном случае под фичами я имел ввиду не фичи из fsd, а фичи как полноценные модули. Они же слайсы из fsd.
Если мы берём fsd:
У нас получается два слайса: Чат и Урок.
Фичи это аналог usecase (mutation/action).
Виджеты это полноценная рабочая единица.
И вот у нас есть два виджеты: Урок и Чат, которые принимают в качестве параметра ИдУрока и ИдЧата соответственно.
Внутри себя они реализуют логику.
В данном случае под фичами я имел ввиду не фичи из fsd, а фичи как полноценные модули. Они же слайсы из fsd.
Фичи из features - это и есть слайсы.
И вот у нас есть два виджеты: Урок и Чат, которые принимают в качестве параметра ИдУрока и ИдЧата соответственно.Внутри себя они реализуют логику.
По идее, и Урок и Чат состоят из компонентов, которые нигде больше не переиспользуются. Или же наоборот это могут быть фичи, которые переиспользуют entities. Т.е. до уровня widgets еще есть два уровня на которых можно выбрать что и где будет переиспользоваться. И опять же не забываем, что и Урок и Чат состоят из сильно связных сегментов. Иными словами, нужно прямо упереться в конкретную задачу, что бы понять где и какая проблема.
Фичи из features - это и есть слайсы.
Я и написал об этом, чтобы был мост между моим комментом и дальнейшим обсуждением. В дальнейшем используется терминология fsd.
Entities - это компоненты, которые используются для отображения, а не для взаимодействия.
Features - это компоненты действий.
Widgets - это полноценные компоненты.
Что чат, что уроки - полноценные компоненты, в которые достаточно передать только ид для дальнейшей полноценной работы, будут располагаться в widgets.
Конкретные задачи были описаны в комменте выше:
Теперь у нас задача от бизнеса:
В компоненте урока должен быть групповой чат (не на странице, урок переиспользуется на разных страницах).
В учебных чатах (не на странице, не на списке чатов) должен быть список уроков из той же дисциплины, к которой принадлежит урок. Это для того, чтобы быстро навигироваться между чатами и можно было открыть урок. Так же нужно в этом списке отображать статус задания (сдал/отклонено/на проверке).
Спасибо за подрбный фидбек. Мой первый опыт написания статьи, поэтому могу гдето в тексте теряться в повествовании :)
Сейчас попробую более подробно довести свою мысль
Разъясню на счет кроссимпортов. Самое банальное что можно предложить у нас есть некая admin панель для работы с товарами в интернет магазине. У нас есть сущности для материалов и товаров.
type Material = {
id: string
name: string
img: string
}
type Product = {
id: string
name: string
materials: Material[]
}
и тут у нас уже на уровне сущностей появляется связность
возникает вопрос как это обходить
либо мы делаем кроссимпорты на прямую
либо делаем кроссимпорты через @x
Кроссимпорты хоть и запрещены, но неизбежны. С этим методология давно борется и до сих пор в документации раздел по кроссимпортам не опубликован. При этом там же в этом разделе есть сноски на сообщения в телеграмме, где тоже описаны эти моменты.
При этом по итогу мы имеем лишь костыль с @x нотацией. Возможно кому-то покажется что это нормально, но лично мое мнение это просто костыль который придумали чтобы залатать дыру
Ну и далее уже понятно что раз на уровне сущностей появилась связность, то и на уровне фичей автоматически появляются кроссимпорты при взаимодействии этих двух сущностей.
---
Далее на счет сервиса уведомлений. Опять же это тоже можно косвенно отнести к связности между сущностями.
Допустим на том же примере с админкой в котором есть материалы и товары. И у нас появляется сервис уведомлений который ловит уведомления об изменении товаров и материалов (допустим чтобы другие админы видели изменения) и эти уведомления в себе держат материал или продукт который изменили, так как по дизайну предусмотрено отображение карточки товара прямо в списке уведомлений. И получается что это не shared слой который не привязан к бизнес сущностям, но добавляя его в слои выше получаем дополнительную связность.
---
Далее замечание по тестированию. Тут больше момент того что в FSD работа с сущностью скажем так размазана по нескольким слоям, не изолирована в одном слое. Часть работы с сущностью лежит в entity, часть в feature и так далее
И тестирование уже сводится к тому что нужно всю эту цепочку найти и правильно это все организовать при тестировании.
Тут же благодаря абстракциям все части приложения друг о друге знаю только в рамках абстракции, вся реализация для друг друга это просто "черный ящик". Что упрощает поиск логической цепочки при тестировании. При этом появляется более гибкая возможность тестировать отдельные части приложения более изолированно.
Возможно как Вы упоминали, добавив DI в тот же FSD, то множество проблем решилось. Возможно это так и есть, но опять же мы решаем не все проблемы. В fsd нет конкретных абстракций над реализациями, у нас фича это и есть use case который использует "то что дали", это я про entity слой, который часто так же подвержен изменениям.
Немного тут наверное тоже отошел от темы, но надеюсь смог объяснить.
Так же хочу заметить что добавление абстракций, инкапсулирования данных и так далее так же дает некую защиту от дурака. Нужно писать так и никак иначе. Что немного минимизирует плохой код. А чем меньше плохого кода, тем проще становится и тестирование приложения.
---
На счет врезки с документации гугла по чистой из андроид не совсем понял. Если зайти на документацию по архитектуре то ясно видно что говорится в первую очередь об однонаправленном потоке состояния. У нас есть четкое и ясное разделение ui, логики и хранилища данных
А в случае с FSD эти слои размазаны по всему приложению. Банально есть entity в котором и состояние и ui и некая логика для сущности, при этом далее в фиче реализована дополнительная функциональность. Как будто сходится, фича - это юз кейс, но при этом у нас в фиче так же может быть своя ui которая связана так же как и с фичами, так же и со слоем ниже с сущностью отделенной ей
---
core слой это скорее как shared в FSD если уж брать аналогию. Это слой полностью не зависимый от бизнес сущностей. При этом никак не зависящие от внешних каких то библиотек (в идеальном мире опять таки, часто всетаки встречаются что выкрадываются реализации которые зависят от чегото внешнего, это уже минус в карму разработчика который это сделал. если мы зависим от чегото внешнего нужно от этого абстрагироваться) (опять же это все про идеальный мир, утопии не существует, нужно понимать где можно чем то пренебреч, а где нет)
А domain слой это как раз уже сердце приложения. Тут возможно возникает путанница из за дополнительного добавления изолирования фичей через папку features. В стандартном понимании чистой архитектуры мы не имеем этой папки и у нас domain это сплошная абстракция всего и вся в нашем приложении.
Даже если уж мы идем еще дальше в андроид, то там фичи изолируются еще лучше, там есть практика изолирования фичей таким образом, что мы делаем отдельный условно java модуль в котором мы отдельно настраиваем так сказать мини сборщик для этой фичи. Далее углубляться я думаю не стоит, потому что там все на много сложнее и уйду в дебри.
Если уж идти прямо действительно по клин, то даже обращение к window должно быть абстрагировано. У нас весь доменный слой должен быть полностью платформонезависемым. Опять же как пример показывал, что можно поменять все на vue. Можно так же без проблемно мигрировать все на ReactNative просто переписав ui и добавить реализации для платформозависемых фичей.
---
Для юз кейсов не нужна дополнительная абстракция. UseCases это можно сказать интеракторы в нашей бизнес логике. Они и так не зависят от реализаций. Это действия которые мы выполняем на основе исключительно доменного слоя. Это как должны работать и взаимодействовать наши абстракции. Мы тут на уровне абстракции как раз говорим как должен работать сценарий. А делать абстракцию для абстракции это уже чушь.
Если же вы имели ввиду добавить абстракцию чтобы стандартизировать названию функции экзекьютора, то по моему мнению это уже будет лишним. Тут больше уже код стайл в команде скорее. Если интереснее наследоваться дополнительно чтобы именовать экзекьютор - дело ваше
---
Если в рамках fsd вы так же создавали сущности, которые на вход принимают зависимости там бы тоже все было прозрачно и тестируемо и не пришлось бы мокать модули.
Не до конца понял вашу мыль тут. Но опять же подмечу что в случае с fsd use cases аркестрирует реализациями а не на уровне абстракций. FSD в целом как будто это только про ФП. Clean базируется на ООП и принципах SOLID. Брать аналогии у друг друга в целом очень холеварное месево получится.
---
далее вы говорите про data слой и network в нем и что похоже на entity слой из фсд. не понял к чему это, но вы правы, это в рамках fsd должно лежать именно в entity слое
---
можно было бы адаптировать и для useSyncExternalStore
но он ждет просто эмита чтобы далее забрать состояние самому
у меня же в flow реализовано так что колбэк подписки аргументом принимает новое значение. Можно взять аналогию с делегатов в шарпе
---
Но в вашем примере вы написали логику обработки события модели прямо в компоненте при том, что эта логика вообще не имеет ни одного замыкания на LoginPage.
Логика в компонентах имеется ввиду какая либо бизнес логика приложения. Отображение это не логическая составляющая приложения, так же как навигация. Это слой UI. Тут как раз таки для этого и используется паттерн MVI. Мы с контроллера (в нашем случае ViewModel) отправляем UI триггеры на которые он должен отреагировать. Ну или не отреагировать. Вью модели в данном случае без разницы. Он уведомил о каком либо действии, а UI либо както реагирует либо нет.
---
Это, конечно дело вкуса, но вроде так куда выразительнее.
Как вы и сказали это дело вкуса. Я же вынес это отдельно чтобы каждый раз не писать subscribe, unsibscribe
---
на счет тестирования не совсем вас понял. почему нет смысла отдельно тестировать. Мы отдельно протестировали логику чтобы она корректно работала. И отдельно протестировали UI чтобы он корретктно отображал состояние и корректно реагировал на эвенты
как будто тут мне больше нечего добавить либо я так вас и не понял
---
И на счет добавления Di в FSD и дополнительная эта вся реорганизация про которую вы говорите это уже немного не в ту сторону. Это уже все так же будет выглядеть как дополнительные костыли над FSD чтобы както походить на давно проверенные паттерны проектирования ООП
При том такая модернизированная FSD возможно будет не сильно проще, если вообще не еще сложнее в понимании чем та же самая clean
Надеюсь ответить на вопросы я смог достаточно хорошо. Так же большое спасибо за такой подробный фидбэк, очень приятно было подготовить вам ответ. Всегда рад когда можнно обменяться опытом. Даже пока отвечал еще пару раз заглянул в документацию андроида и вспомнил некоторые уже забытые для меня вещи оттуда. Такие дискуссии всегда полезны, получается как некая рефлексия на всем изученным и не изученным :)
Про core папку несогласен.
Те же стандартные библиотеки и внешние библиотеки для вычислений - не вижу ничего плохого в том, чтобы использовать их в core.
Core скорее должен быть абстрагирован от бл, но требовать, чтобы он не зависел от внешних библиотек - изобретать велосипеды на каждый чих.
Ровно как и DI - не вижу ничего плохого сам DI не писать, а использовать готовый, а в core добавить для него абстракции, чтобы в бл было меньше связей на внешнее и больше на core. Не инициализация, а именно сам сервис DI.
core по хорошему должен быть платформонезависемым. конечно же я уже упоминал что утопии не бывает и на каждый чих делать абстракции абсолютно с вами согласен это избыточно
главное понимать проект. Скажем так, чувствовать момент когда нужно делать дополнительную абстракцию, а когда можно этим пренебречь
А на счет DI я помоему не говорил что нужно писать самому. Вроде даже упоминал что можно взять готовый. Если я этого не упомянул, то каюсь :)
Свой DI написать скорее было вызовом для себя.
У каждого проекта свои требования. Слышал от знакомых случай где по требованиям в целом не было возможности использовать какие либо внешние библиотеки вовсе и работать система должна исключительно под локальной сетью. Но это уже совсем другая история)
и тут у нас уже на уровне сущностей появляется связность. Возникает вопрос как это обходить?Либо мы делаем кроссимпорты на прямую, либо делаем кроссимпорты через @x
Если речь идет про fsd, то выбора вариантов нет. Линтер fsd будет ругаться на прямой импорт, а занчит только такой, который предусмотрен архитектурой, т.е. через @x.
Кроссимпорты хоть и запрещены, но неизбежны. С этим методология давно борется и до сих пор в документации раздел по кроссимпортам не опубликован.
Данная версия документации появилась буквально в ближайшем прошлом. До этого в этом же году у них была другая документация. Поэтому он не до сих пор не опубликован, а пока не заполнен.
При этом старая версия никуда не делась. И выразительно показывает как слайсы должны взаимодействовать друг с другом.
https://feature-sliced.design/docs/reference/public-api
Т.е. все нормально и с документацией и с пониманием того как решать такие вопросы в fsd
При этом там же в этом разделе есть сноски на сообщения в телеграмме, где тоже описаны эти моменты.
Я пробежался по телеге и там нет ничего такого чего нет в документации. Они решили перекроить старую документацию, но ответы на все это там уже есть.
При этом по итогу мы имеем лишь костыль с @x нотацией. Возможно кому-то покажется что это нормально, но лично мое мнение это просто костыль который придумали чтобы залатать дыру
Они объяснили почему выделили слой entities и widgets. Как раз, что бы слой features не брал на себя слишком много. А в entities кроссимпорт между слайсами выглядит вполне логично. А вот если вы продолжаете использовать кроссимпорты в других слоях, то это скорее всего будет костыль. Иными словами не нужно мешать все в одну кучу.
Например, если использовать widgets/entities они как раз хотели уйти от перегруженного слоя features, где связность между слайсами создает проблему. По сути, в вашей архитектуре как раз и можно наблюдать подобное решение, когда этот слой превращается в универсальный комбайн.
-----
Ну и далее уже понятно что раз на уровне сущностей появилась связность, то и на уровне фичей автоматически появляются кроссимпорты при взаимодействии этих двух сущностей.
Не появляется, потому что на уровне фичей можно спокойно импортировать все из нижестоящего уровня. То что на уровне сущностей было кроссимпортами, на уровне фичей обычное переиспользование.
-----
Допустим на том же примере с админкой в котором есть материалы и товары. И у нас появляется сервис уведомлений который ловит уведомления об изменении товаров и материалов
То, что вы называете сервисом уведомлений, работает где-то на уровне виджетов. Потому что это комбайн, который может переиспользовать разные фичи.
-----
И тестирование уже сводится к тому что нужно всю эту цепочку найти и правильно это все организовать при тестировании.
Вы упускаете идею fsd при которой приложение состоит из четкой иерархии строительных блоков. Вам не нужно искать всю цепочку. И важно не забывать, что в fsd не все нужно располагать по разным слоям. Например, какой-то функционал может лежать прямо в слое pages потому что больше нигде он не нужен. Но, если как вы говорите у нас что-то "размазанно" по слоям, то к каждой сущности на каждом уровне нужно относиться как к отдельному самостоятельному строительному блоку.
Тут же благодаря абстракциям все части приложения друг о друге знаю только в рамках абстракции, вся реализация для друг друга это просто "черный ящик". Что упрощает поиск логической цепочки при тестировании. При этом появляется более гибкая возможность тестировать отдельные части приложения более изолированно.
Так, ничто не мешает использовать этот подход в fsd.
В fsd нет конкретных абстракций над реализациями, у нас фича это и есть use case который использует "то что дали", это я про entity слой, который часто так же подвержен изменениям.
Так и у вас тоже нет. Вы можете использовать конкретный класс, абстрактный класс, интерфейс, токен. И с тем же успехом при fsd можно использовать из слоя entites/shared конкретный класс, абстрактный класс, интерфейс, токен. Di действительно является опцией в fsd, но ничто не мешает вам внедрять зависимости любым другим способом. По сути, отсутствие контейнера приложения делает работу не централизованной и соответственно менее удобной. Но это никак не мешает вам работать по контракту.
Так же хочу заметить что добавление абстракций, инкапсулирования данных и так далее так же дает некую защиту от дурака. Нужно писать так и никак иначе. Что немного минимизирует плохой код. А чем меньше плохого кода, тем проще становится и тестирование приложения.
Возможно мне только кажется, но если у вас нет каких-то автоматических средств, что бы указывать, что в данном месте нужно использовать di или какой-то другой шаблон, то все это остается на совести разработчика. И он сам решает должен быть тут конкретный класс или зависимость.
-----
Если зайти на ...
Я что-то такое вчера и посмотрел.
Если зайти на документацию по архитектуре то ясно видно что говорится в первую очередь об однонаправленном потоке состояния.
Давайте посмотрим на эту архитектуру:
Рекомендуемая архитектура приложения
Уровень пользовательского интерфейса (ui elements -> state holders) -> Уровень данных (repositiries -> data source)
Здесь нет никакого уровня features. А это значит что любой state holder имеет доступ к любому репозиторию. А любой репозиторий к любому источнику данных. Добавив такой слой вы нарушили архитектуру гугла. Потому что если у вас репозиторий захочет иметь доступ к нескольким источникам данных он этого сделать не сможет.
Уровень данных состоит из репозиториев , каждый из которых может содержать от нуля до многих источников данных .
С этого начинается архитектура уровня данных. Поэтому я и написал:
Неплохо было бы добавить врезку об Чистой архитектуре гугл в андроид приложениях. Потому что основная идея там ровно такая же, т.е. прям один в один как и в fsd. Т.е. приложение состоит из ясного набора слоев, каждый из которых имеет ясное назначение и отношения к другим слоям. И названы эти слои и их назначение.
-----
core слой это скорее как shared в FSD если уж брать аналогию. Это слой полностью не зависимый от бизнес сущностей.
Я это понял, но именно поэтому он и называется shared, а не core.
А domain слой это как раз уже сердце приложения.
И это понятно, но вы этот слой выкинули и размазали его по подслоям features.
А domain слой это как раз уже сердце приложения. Тут возможно возникает путанница из за дополнительного добавления изолирования фичей через папку features.
Так вот поэтому я и обратил внимание, что основная идея и в fsd и в гугловской теме похожие.
-----
Если же вы имели ввиду добавить абстракцию чтобы стандартизировать названию функции экзекьютора, то по моему мнению это уже будет лишним. Тут больше уже код стайл в команде скорее. Если интереснее наследоваться дополнительно чтобы именовать экзекьютор - дело ваше
Я имел в виду, что если у нас в приложении используется какой-то шаблон, то нужно понимать, что это за шаблон. Например, какой смысл называть все функции executor "execute", если его сигнатура всегда разная и зависит от конкретного класса? Или вы имеете в виду, что каждый раз ее можно называть по своему? Логика подсказывает, что название метода должно отображать семантику того, что оно делает или иметь общий интерфейс с любым use case, тогда все они будут execute.
-----
Не до конца понял вашу мыль тут. Но опять же подмечу что в случае с fsd use cases аркестрирует реализациями а не на уровне абстракций. FSD в целом как будто это только про ФП.
fsd никак не принуждает, что нужно использовать. Можете воспринимать написанное, как добавление di в проект и использование fsd.
можно было бы адаптировать и для useSyncExternalStore. но он ждет просто эмита чтобы далее забрать состояние самому. у меня же в flow реализовано так что колбэк подписки аргументом принимает новое значение. Можно взять аналогию с делегатов в шарпе
Для обновления компонента вы добавляете хук на состояние. useSyncExternalStore как раз и служит для того, что бы не добавлять подобное. Поэтому я и предложил рассмотреть.
-----
Логика в компонентах имеется ввиду какая либо бизнес логика приложения.
Поведение страницы логина в зависимости от состояния авторизации и есть бизнес логика приложения. Например, в вашем случае у вас есть бизнес правила при которых страница переходит на страницу дашбоард, или показывает пользователю тостик. В других приложениях будут свои описанные поведения.
Как вы и сказали это дело вкуса. Я же вынес это отдельно чтобы каждый раз не писать subscribe, unsibscribe
Сокрыв реактовую рутину создали ситуацию при которой возможна ошибка и тут же эту ошибку допустили. Добавив литерал в useFlow вы забыли, что этот литерал идет в зависимости useEffect [flow, handler], поэтому всякий раз при изменении состояния у вас подписка и отписка от этого литерала. Иными словами вам нужно использовать useCallback и только после этого прокидывать ваше действие.
-----
на счет тестирования не совсем вас понял. почему нет смысла отдельно тестировать. Мы отдельно протестировали логику чтобы она корректно работала. И отдельно протестировали UI чтобы он корретктно отображал состояние и корректно реагировал на эвенты как будто тут мне больше нечего добавить либо я так вас и не понял
Fsd подразумевает, что далеко не все будет раскидано по уровням. Поэтому одного теста компонента может хватить, что бы протестировать все функциональные требования. При этом тестировать "мелкие" строительные блоки может быть не обязательно. Все зависит от того как и что переиспользуется.
И на счет добавления Di в FSD и дополнительная эта вся реорганизация про которую вы говорите это уже немного не в ту сторону. Это уже все так же будет выглядеть как дополнительные костыли над FSD чтобы както походить на давно проверенные паттерны проектирования ООП При том такая модернизированная FSD возможно будет не сильно проще, если вообще не еще сложнее в понимании чем та же самая clean
Не понял почему. fsd - это про иерархию строительных элементов и отношения между элементами этой иерархии. А что это будут за элементы, т.е. классы для Di, слайсы для redux значения не имеет. fsd для этого модернизировать не нужно.
Ну с FSD я с момента когда у них и документации толком не было. И сейчас абсолютное большинство проектов делаются на FSD
Не подумайте что я как-то хейчу FSD
Мне он до сих пор нравится и я пишу и буду писать на нем проекты.
Методология еще молодая и еще все дополняется. И многие непонятные моменты они стандартизируют.
При этом не могу сказать точно приняли этот подход или нет, но видел что в версии 2.1 методологии должно было появиться описания page first подхода. Это как раз что вы и описали, что можно логику оставить в слое page
Опять же считаю, что это сильно ломает принципы FSD
Это еще сильнее размазывает логику по приложению.
У всего должно быть свое место.
Новый разработчик приходит видит что везде все вынесено в сущности.
Допустим хочет посмотреть как работать с какойто сущностью а в папке entities ее нет. А оказывается он маленький и разработчик засунул в pages
Так же далее при масштабировании она может разрасититься и эта сущность останется раздутой в рамках pages слоя
---
Здесь нет никакого уровня features. А это значит что любой state holder имеет доступ к любому репозиторию. А любой репозиторий к любому источнику данных. Добавив такой слой вы нарушили архитектуру гугла. Потому что если у вас репозиторий захочет иметь доступ к нескольким источникам данных он этого сделать не сможет.
Не совсем понял что вы имеете ввиду. Да и как понять репозиторий имеет доступ к источникам данных? Репозиторий это в целом реализация как источник данных. У нас для работы с сущностью может быть несколько репозриториев. Например у сущности будет репозиторий для получения данных с удаленного сервера и будет репозиторий для работы с этой сущностью в локальном хранилище. А уже use case будет оркестрировать ими. Допустим будет брать с удаленного сервера данные, кешировать их через репозиторий для работы с локальным хранилищем. И при последующем запросе на получение этих данных мы можем сразу отдать закешированные данные. Как один из кейсов.
Возможно не правильно понял вашу мысль, если что поправьте меня.
---
если мы выносим его полностью в виджеты то получается что мы опять же размажем логику по слоям.
как вы делали аналогию с чистой, entity слой тоже что и domain, не совсем согласен конечно с этим, но допустим
получается что мы в данном случае вытащим все в слой виджетов оставив доменный слой пустым
Т.е. приложение состоит из ясного набора слоев, каждый из которых имеет ясное назначение и отношения к другим слоям. И названы эти слои и их назначение.
Тут тоже не пойму. Это в целом относится ко всем архитектурам. Суть любой архитектуры это разделить области приложения таким или иным образом и определить их отношения между друг другом.
Суть архитектур в целом везде чем то похожа друг на друга. В прямое сравнение FSD и чистой, что мы сейчас делаем, не самое удачное. Каждая из них решает свой спектр задач. Опять же допустим тот же самый МТС часто любят менять координально технологии судя по их рассказам с конференций и митапов. Они как раз активно используют в своих проектах именно чистую архитектуру. Если интересно можете поискать их статьи, выступления. Если быстро смогу найти, то прикреплю.
---
на счет use cases опять же если я правильно понял о чем вы говорите, то ответ остается тот же что и в прошлый раз. создание интерфейса тут мне просто напросто кажется избыточным, хотя тоже имеет место быть
execute просто общепринятое название для выполнения действия
у каждой команды он может быть свой
тут больше момент уже договоренности внутри команды
видел и такое что создается LoginUseCase с функцией экзекьютором login
---
ну так useSyncExternalStore тоже хук, просто с другой сигнатурой можно сказать
дали инструмент из под коробки
я либо делаю сам себе, либо подгоняю свою реализацию под то что дает реакт
опять же во вью на сколько я знаю нет аналога этому хуку в реакте и там придется все равно добавлять чтото
если чтото в реакте сделано из под коробки не значит что есть везде, тоже самое и наоборот
статья это не гайд как нужно делать
это как можно сделать
и у все равно как бы мы не хотели стандартизировать все и вся невозможно, везде будет чтото отличаться
у меня ход мыслей пошел сначала с создания логики потом дошел до ui
тут же вы пошли получается от обратного
взять ui, в данном случае react и подогнать под него, что конечно же тоже имеет смысл
никто не мешает потом в том же vue тоже создать инструмент который будет подогнан под реализацию которая уже сделана
---
Поведение страницы логина в зависимости от состояния авторизации и есть бизнес логика приложения. Например, в вашем случае у вас есть бизнес правила при которых страница переходит на страницу дашбоард, или показывает пользователю тостик. В других приложениях будут свои описанные поведения.
Тут с вами соглашусь, пример привел не совсем правильный, почему то я сразу этого не заметил. NavigateToLogin, ShowToast и так далее согласен это не правильно
Тут нужно не говорить что нужно сделать ui, а что произошло
допустим LoginSuccsess, ConfirmationSuccsess, Error
А ui уже каким либо образом реагирует на эти действия
Тут конечно моя ошибка, посмотрю есть ли функионал редактирования статьи, исправлю ошибку
---
Добавив литерал в useFlow вы забыли, что этот литерал идет в зависимости useEffect [flow, handler], поэтому всякий раз при изменении состояния у вас подписка и отписка от этого литерала.
Тут же не согласен, так как view model создается один раз и живет на протяжении всего жизненного цикла компонента
А flow и handler находятся именно в view model. Получается нет пересоздания view model нет и пересоздания ее полей и методов, а значит ссылки те же
---
тут как раз таки и идет координальное отличие fsd от чистой
так как суть разделения на слои у них разный, то и методологии тестирования могут отличаться
если в fsd мы берем компонент и его тестируем вместе слогикой вместе
хотя тут тоже было бы хорошо даже в рамках того же fsd логику тоже тестировать отдельно
то в чистой тестирование происходит изолированно по слоям
слои имеется ввиду что мы ui тестируем изолированно от логики
а логику изолированно от ui
---
а на счет того что в FSD без проблем можно добавить DI инверсию зависимостей адекватную и так далее я сомневаюсь
возможно это так, но я у себя в голове не могу представить себе как это адекватно должно выглядеть
было бы больше свободного времени я бы с радостью попробовал бы чтото сделать
все равно это так же очень интересная тема и можно было бы попробовать
да и если брать тот же самый редакс как стейт менеджер, то тут тоже возникают различия в presentation слое если берем чистую
так как redux реализует скорее flux подход чем какой либо из MV
но это тоже отдельная тема для разговора
Скрытый текст
Допустим хочет посмотреть как работать с какойто сущностью а в папке entities ее нет. А оказывается он маленький и разработчик засунул в pagesТак же далее при масштабировании она может разрасититься и эта сущность останется раздутой в рамках pages слоя
По идее, все что угодно можно раздуть XD
Не совсем понял что вы имеете ввиду. Да и как понять репозиторий имеет доступ к источникам данных?
Я имел в виду архитектуру гугла:
Уровень пользовательского интерфейса (ui elements -> state holders) -> Уровень данных (repositiries -> data source)
Скрытый текст
если мы выносим его полностью в виджеты то получается что мы опять же размажем логику по слоям.
Что значит размазываем логику по слоям? Выделяя логику в отдельные слои мы делаем эту логику доступной для Всего вышестоящего слоя. Поэтому мы ничего не размазываем, а создаем независимые строительные элементы. Если у нас строительный элемент переиспользуется мы уже не можем говорить, что что-то размазываем.
Скрытый текст
Тут тоже не пойму. Это в целом относится ко всем архитектурам. Суть любой архитектуры это разделить области приложения таким или иным образом и определить их отношения между друг другом.
Так вот и смотрите, у вас app->feature->core, что находится внутри каждого слоя?
Повторю архитектуру гугла:
Уровень пользовательского интерфейса (ui elements -> state holders) -> Уровень данных (repositiries -> data source)
По сути, четыре уровня. При этом четко обозначено, что находится на каждом уровне и направление зависимостей. Если проводить аналогии с гуглом, то у вас data source из разных фичей не могут быть целью для обращений репозиториев, которые не являются частью фичи, а у гугла такого ограничения нет. Для них как вы говорите на Уровне данных вся функциональность размазана по всему уровню и к ней имеет доступ любой state holder из Уровня пользовательского интерфейса.
Скрытый текст
на счет use cases опять же если я правильно понял о чем вы говорите, то ответ остается тот же что и в прошлый раз. создание интерфейса тут мне просто напросто кажется избыточным, хотя тоже имеет место быть
Мой вопрос, у вас тут шаблон или нет? Потому что судя по всему тут шаблона нет. И функцию execute можно называть по семантике работы, которую она делает.
Скрытый текст
execute просто общепринятое название для выполнения действияу каждой команды он может быть свой
Честно говоря, впервые об этом слышу. Но я знаю шаблон Команда. Там действительно есть метод execute. По идее, как мне это представляется. Вот если взять архитектуру гугла. Единственным источником данных является репозиторий. В вашем примере - этот репозиторий отвечает за хранение информации формы регистарции. Он такой один, поэтому вы его задаете в зависимости для Di, и тогда передавать в execute LoginBody вам не нужно. И соответственно, если если любую команду конфигурировать только через Di у вас элемента любого класса команды будет иметь один интерфейс execute. Если у вас шаблона нет, то execute лучше называть по семантике выполняемой операции.
тут больше момент уже договоренности внутри команды
Если шаблон есть, то это уже не зависит от разработчика. Нужно следовать интерфейсу шаблона.
Скрытый текст
я либо делаю сам себе, либо подгоняю свою реализацию под то что дает реактопять же во вью на сколько я знаю нет аналога этому хуку в реакте и там придется все равно добавлять чтото
Да этот пример со vue можно не учитывать, потому что он крайне специфичен.
Суть фреймворков как раз в том, что они меняются крайне редко.
Тут же не согласен, так как view model создается один раз и живет на протяжении всего жизненного цикла компонента
Модель создается один раз. А вот весь компонент выполняется постоянно, когда идет обновление. Вы использовали литерал и поместили его в зависимости хука useEffect, поэтому данный useEffect будет выполняться каждый раз когда идет обновление компонента.
useFlow(viewModel.uiEvent, (event) => { // <--- эта функция при каждом рендере новая
...
}
})
Di будет выглядеть ровно так же как и у вас) fsd вообще до лампочки, какие вы там используете шаблоны в своих слоях.
Ну опять же я и говорил что используется не изначальная концепция чистой, а немного модернизированная
Если уж брать изначально как должно быть, то как у меня сделано, папки features не должно быть
Есть единый доменный слой всего приложения и все
Там вообще никаких проблем не возникает, кому что надо тот то и использует
Отдельный features вынесен для более четкого разделения логики
При этом тут если же смотреть если мы делаем такое разделение по фичам, то так же как и в FSD кроссимпорты между фичами тоже могут быть. В случае с андроид мы на уровне сборщика указываем зависимости между пакетами. Тут же только если прибегать к настройкам линтера. С разделением на фичи просто удобнее работать с разделением на несколько команд разработки.
Ну и в большинстве случаев app слой является связующим, где так же можно хранить логику
Тоесть грубо говоря если бы мы убрали слой фичей
то был бы в app domain data presentation и там все хранилось бы
везде будут свои сложности. утопии нет) ничего не идеально)
чем сильнее усложнять тем больше проблем
тут же чтобы понимать как делить фичи наверное можно взять аналогию с микрофронтов
фичи это микрофронты, а app собирает их как нужно
---
размазано в данном случае говорил о том что мы вынесем всю сущность этих уведомлений в слой виджета пропустив банально entity слой
что размажет дополнительно логику
так мы отходим от стандартной практики разработки
по хорошему мы идем снизу вверх
сделали утилиты в шаред
описали сущность в энтити
написали можно сказать юз кейсы в фичах
скомпоновали в логические блоки в виджетах
собрали в страницы
мы же пропустили тут 2 слоя запихнув все в виджет
---
ну тут core как shared из fsd
а app и features
тут как написал уже выше
можно было бы сделать один доменный слой для всего приложения
тут же мы просто разбиваем приложение на отдельные логические блоки
а app их объединяет
опять же аналогия с микрофронтами
---
по use cases шаблона тут какогото нет
мы просто в рамках проекта договариваемся на нейминг
если брать документацию андроида там функцию executor'a назвали invoke
---
на счет переподписки в useFlow абсолютно правы, почему туда закрался эндлер в депсы не пойму, скорее всего линтер ругнулся что в хуке используется, а в депсы не добавлены
там хэндлер не должен быть в депсах
да и сам flow можно оттуда убрать, но как защита от дурака пусть там лежит
а на счет этого всего у меня давно закралась идея попробовать использовать zustand для этих целей, но никак руки не доходят
У меня эволюция проекта происходила так:
Взял за основу FSD и CA, но перед этим всем я изучал и другие архитектуры на другом стэке (тот же BLoC, HA, VIPER). Получилось что-то типо:
src
├── app
├── pages
├── shared
└── [name]
└── features (бл)
└── [name]
├── datasources
├── stores
├── entities
├── usecases (старое features)
└── widgets
Т.е. в shared попадали переиспользуемые компоненты, которые дополнялись абстракциями, чтобы не зависеть от фич.
Появился бойлерплейт и бриджи, но связанность уменьшилась, абстракция увеличилась.
Папка shared начала разрастаться. Я стал использовать nx и у меня многое из shared переехало как отдельные пакеты в папку libs. Тем самым уже на уровне кода я абстрагировал полноценные блоки (Плееры, UI библиотеку, сторы - не фичи, а именно надстройки над сторами для хранения, и т.д.)
Именно для болшей изолированности у меня было принято решение раздеить каждую фичу на слои и чтобы у каждой был свой доменный слой, что как можно сильнее минимизирует какието связности между фичами и можно сказать связывает руки разработчику чтобы так не сделать
конечно если брать тот же самый андроид можно настроить зависимости на уровне gradle что разработчикам вообще не позволит сделать какой либо кросимпоррт даже случайно
но мы в вебе, так что имеем что имеем)
Именно для болшей изолированности у меня было принято решение раздеить каждую фичу на слои и чтобы у каждой был свой доменный слой
Да, Я привел похожее что у меня получилось, потом дополнил про nx как способ для очистки shared.
Ну дефолтный fsd совсем не годится уже на средних проектах, т.к. папки быстро разрастаются и нужно инвертировать фичи и слои для того, чтобы был порядок в доменах.
По поводу контроля импортов - есть плагин для eslint, но я им не пользовался. Но он легко гуглится =)
Попробуйте $mol
Там совершенно другой подход - компоненты с логикой + fnq , где имя класса равно его местоположению
Буду признателен ссылкам на рекомендуемые ресурсы где лучше почитать. С радостью почитаю в свободное время!
https://habr.com/ru/articles/820871/ Можно с этой статьи начать, там и ссылки на все остальные ресурсы
P.S в этом фреймворке даже импорты писать не нужно)
Cross-импорты — постоянная головная боль
Как будто не хватает в данном примере контекста. Нет ничего критичного в таком импорте в слоях widgets, pages, app(странно, но бывают случаи).
// Находимся например в @/pages/home
import { useAuth } from '@/features/auth'
import { PostsList } from '@/features/posts'
В проекте у себя использовал FSD, но чуть изменил её под рекурсивное отображение страниц с табами. Также многие запросы сущностей легли в enitites, делая отсылку на CA.
Проблемы в импорте разных фичей на уровне выше нет. Это абсолютно нормальная практика. Проблема возникает когда в рамках одного слоя появляется зависимость между сущностями
есть решение работать через @x. Хотя если надобность этого появляется слишком часто - то тут уже без пересмотра архитектуры не обойтись.
Об этом я уже упоминал при ответе на комментарий выше. Это больше выглядиь как костыль чтобы залотать дыру.
Опять же когда в проекта 1-2 раза встречается можнт стерпеть
Конечно как я и писал клин тянуть во все проекты не нужно. Для большинства задач достаточно и fsd
На клин нужно смотреть когда либо уже чувствуешь что команда разрослась и в проекте хаос. Либо на этапе проектирования если уже понимаешь что в недавнем будущем уже прижется сильно масштабироваться, можно сразу закладывать архитектуру.
Все крупные компании так или иначе начинали с монолитов, потом безизбежно все переписывали на микросервисы, микрофронты и так далее и тому подобное. И от части это все тоже про архитектуру
"куда положить NotificationService" - если ты правильно используешь fsd сервисов обычно вообще нет. Если нужны уведомления во всем приложении в шаред пишешь lib, в app заворачиваешь все приложение. А ещё лучше - заверни это в пакет, устанавливай как либу и юзай в app
Интересен такой момент, как работаете с доменными моделями, позволяете ли их читать, писать в них напрямую или нет, если нет, то как гарантируется соблюдение бизнес логики, когда например для изменения модели необходимо делать это через сервис, потому что он дополнительно агрегирует в себе информацию, которая нужна для изменения модели? Я имею в виду защиту от того, что кто-то решит изменять модель в обход бизнес логики. Также интересует вопрос о том, как организовано взаимодействие моделей между друг другом, если оно необходимо, когда изменения данных одной модели влияет на данные другой, при этом эти модели относятся к разным доменным сущеостям
Первый вопрос не понял, можете пожалуйста подробнее описать. Не пойму о каких изменениях модели в обход бизнес логики вы говорите.
А на счет взаимодействия между друг другом. Либо у нас связующее app, либо если они связаны, то скорее всего это изначально не имело смысла делить на разные доменные слои. Но бывает и такое что они неизбежно связаны друг с другом. Тогда мы явно указываем в di модулях что они связаны между друг с другом. Это не рекомендованно, но не запрещено. Поэтому и попробовал для себя использовать не готовую DI, а сделать свой вместе с явным указанием зависимостей.
В android разработке такие фичи можно сказать разделяются на отдельные приложения, там создаются отдельные пакеты со своей конфигурацией сборщика который наследуется от основного конфига. И там мы можем настроить зависимости между модулями.
Для более простого понимания можно сделать аналогию с микрофронтами. В большинстве случаев разделение в фичах можно сделать так же как делили бы на микрофронты.
В микрофронтах явно указываем откуда что мы тянем.
Представим что есть какие то дто которые бэк присылает на клиента, эти дто становятся основой для моделей. У моделей есть свои методы и поля. Есть слой вью модели, который связывает наше вью с моделью и изменяется модель через этот слой, предположим появилось бизнес правило, после изменение каких-то данных в модели, обновлять данные в другой доменной модели. Это бизнес правило должно соблюдаться всегда. Чтобы не дублировать его между разными вью моделями мы выносим эту логику в какой-то сервис и теперь, чтобы изменить поле модели, необходимо вызвать соответствующий метод сервиса. Как вы обеспечиваете гарантию того, что кто-то из разработчиков не нарушит эту логику и не получит доступ к модели напрямую в каком-то другом сервисе или вью модели и не изменит ее обойдя бизнес правило? Есть ли какая-то изоляция моделей, когда ты не можешь получить к ним доступ обойдя бизнес правила?
Я кажется опять не до конца вас понял. Но во первых, дто не становится основой для модели. Дто у нкс лежат исплбчительно в data слое. С dto мы работаем исключительно в рамках слоя данных. Модели с которыми мы работем в доменном слое а в последующем и в слое представления уже описаны в доменном слое - это уже как раз наши бизнес сущности. Слой данных может работать с dto как хочет, на слой выше слой данных отдает именно доменные модели. Такая практика очень часто встречается не только в чистой архитектуре. Все что связано с описанием наших бизнес сущностей находится в досенном слое, и доменному слою без разницы что и как будет делать слой данных, главное чтобы совпадали входные и выходные данные которые мы описали в "контракте" (можель а длменом слое)
А вот на счет изоляции моделей и что нельязя к ним получить доступ обойдя бизнес правила я не понял.
Если нужно при изменении данных в одном месте ревалидировать данные в другом месте, то это ты делаем в use cases. Они как раз и нужны для оркестрации данными, они и описывают пользовательские сценарии.
Но как пример мне привести из реального кейса, нужно было очищать данные со всего приложения на уровне всех слоев при выходе из аккаунта. Решили тем что создаем в core класс Clearable который даёт метод для подписки и метод для очистки. Далее все репозитории которые должны очищаться через di получают и каким либо образом реагируют на эвент очистки
Тут к сожалению приходится самому везде подписываться на событие. В андроиде я бы сделал более красиво через наследовние. Сделал бы класс Clearable от которого наследовался бы и нужно было бы реализовать метод onClear, но пришлось откзааться в js в счет отсутствия миксинов, множественного наследования
пример из проекта, мне прилетает огромная древовидное dto, где поля ассоциируются с разными бизнес сущностями, часть из которых независима друг от друга, часть агрегирована и тд. На основе этой дто происходит создание моделей, которые сохраняют структуру заданную в dto, если они связаны друг с другом. У всех этих моделей очень много полей, которые можно изменять. Как в вашем подходе должны происходить такие изменения, то есть есть модель, у нее есть поле имя, описание и тд. Мы можем изменять эти данные, и эти изменения не требуют какой-то бизнес логики, буквально примитивный сеттер. Вы для их изменения заводите свои/свой useCase или позволяете менять такие данные напрямую, минуя useCase? Появляется ли бойлерплейт, когда нужно по сути дублировать эти сеттеры в отдельном useCase?
Нет, создание use case на каждый чих не обязательно. Use cases это пользовательские сценарии которые мы создаем при необходимости
Конечно в идеальном мире хочется для всего создавать, ведь в дальнейшем может понадобиться модернизация, но для обычных сеттеров создавать отдельно use case не стоит
view model может использовать репозиторий на прямую (опять же знает исключительно абстракции, реализации закидываем только через di) - и это нормальная практика
но как только появляется момент что вы во вью модели начинаете както дополнительно работать с этими данными (изменять, кешировать, перекидывать в другие репозитории), ну в двух словах появляется как раз бизнес логика, то нужно уже выносить в пользовательские сценарии (use cases)
А гарантии того, что разработчик не начнет менять модель в обход useCase есть какие-то на уровне кода, статической типизации или ограничения доступа к получению моделей? Или только на уровне соглашений между разработчиками?
Можете пожалуйста объяснить что вы подразумеваете под менять модель в обход use case?
Никак не могу понять что вы хотите спросить
Да и в целом наверное еще что вы понимаете под моделью?
Попробую еще раз, у меня на самом деле тупорылый вопрос :)
Как вы организуете код так, чтобы не было бойлерплейта при этом изменения моделей проходило через заданные точки входа (useCases в вашем примере, если я верно понял), если у модели 100500 полей, то придется их поддерживать и в точках входа, а если позволить обходить точку входа для изменения модели, то как гарантировать, что модель меняется корректно там где ее нужно менять через точку входа.
Например я хочу поменять поле name модели user, я иду в сервис и вызываю userService.setValue(readonlyUser, "name", "newName") передаю в него райт онли модель или ее айди и новое значение, у моделей может быть много "тупых" сеттеров, которые просто меняют поле на переданное значение, если мы организуем все через единые точки входа, то придется поддерживать согласованность точки входа и модели. Если мы хотим избежать бойлерплейта и позволяем коду приложения вызывать тупые сеттеры моделей напрямую, user.name = "newName" или user.setName("newName"), то как гарантировать, что те значения, изменять которые можно только через useCase, будут изменены через них, и в код не просочится аналог user.name = "newValue", когда это поле можно изменить только так userService.setValue(readonlyUser, "name", "newName"). У меня есть несколько мыслей на этот счет, но вполне возможно что я либо оверхедом занимаюсь, либо есть решения эллегантнее, хотелось бы узнать ваш подход.
Я понимаю под моделью какую-то бизнес сущность, минимальный юнит бизнес логики, ее основа, с тем с чем работает в итоге бизнес логика, единый источник правды, также для поддержки модели могут создаваться промежуточные модели, которые связывают несколько моделей вместе или изолируют часть модели в отдельную, с которой можно работать независимо от основной
Ну вообще мы в целом не можем взять и написать user.name = "Alex"
У flow в даннос случае нет сеттера
Мы можем делать только emit новых данных
Тут подход идет иммутабельности
Новое состояние, новый объект
что мы и можем видеть в методе update для MutableStateFlow
мы передаем данные которые нужно обновить и рестом собираем новый объект с пред данными и заменяем нужные поля новыми данными
а на счет запрета использования репозитория данных в обход use case, не совсем понимаю зачем, и не совсем понимаю чем может мешать изменение данных не через use case
use case нужен если мы хотим хранить кэш, дополнительно отправлять метрики, или чтото подобное
Если мы делаем todo list который работает локально и нам просто нужно без каких либо дополнительных действий изменить checked, то для этого можно забрать репозиторий прямо во вью модель и дергать метод toggleChecked допустим
ну и если я правильно понял то вы подразумеваете под моделью сам репозиторий?
тоесть источник данных для какойто сущности
но у сущности может быть несколько источников данных
например тот же самый todo
Мы делаем репозиторий для работы с тудушками на удаленном сервере
И так же репозиторий для работы с ними в локальном хранилище
Вот тут то нам и нужен use case, например ToggleTodoCheched
который будет работать например по принципу optimistic. Изменит локально сразу же состояние, и отправит запрос на сервер. Если успешно то все хорошо, в локальном хранилище уже изменили, если не успешно то откатили
похожим образом можно сделать работу оффлайн
сохранять данные локально при отсутсвии интернета
при появлении сети сделать реализацию синхронизации
вот реальные примеры для use cases
если нам нужно просто получить accsess token из репозитория токенов, то можно взять репозиторий и вытянуть оттуда данные
если нам нужно просто закинуть новую пару ключей, то так же можно сделать на прямую через репозиторий
если мы хотим закинуть пару ключей, но при этом сохранить refresh token в локальное хранилище, правильно будет уже use case который это будет делать
все что влияет на бизнес логику мы выносим в use cases
если же вы имеете ввиду что у нас есть use case для того чтобы ставить новую пару токенов, а разработчик решил идти на прямую в репозиторий, то тут уже линтерами не спастись. Это уже на совести разработчика. Перед тем как чтото делать в любом случае нужно изучить что уже написано, не зависимо от архитектуры. Тут же удобно тем что весь доменный слой просмотреть не трудно. Доменный слой полностью описывает работу приложения. Если по доменному слою не понятно что должно происходить возможно чтото не так.
если это на столько частый кейс в проекте, то возможно имеет смысл просто отказаться от прямого использования репозиториев в вью моделях. тут уже линтер вам поможет.
В целом я понял подход, отчасти у меня он похож, но много работы с редактированием лежит на клиенте. С бэком происходит переодическая синхронизация, для обновления данных. Дто являются основой для создание моделей (в вашем примере, если я верно понял - это репозитории). Модели составляют собой коллекции. Есть ViewModel - связка компонента с моделями, ViewModel обычно связаны с кокнретным компонентом, но могут быть более абстрактными и досступными через DI в некскольких местах приложения. Аналог UseCase в моем случае служат оркестраторы, но отличие только в нейминге. ViewModel также может менять модели в обход UseCase если это тупой сеттер. Но вот меня в последнее время это стало напрягать, что если теперь поле модели не просто сеттер, а какой-то UseCase, и теперь мы не просто должны дергать toggleChecked, а должны всегда действовать в рамках определенной бизнес логики, как бить по рукам тем, кто пытается обойти эту бизнес логику.
У меня был кейс, где изменение одной модели, должно было влиять на другую модель, при этом они были не связаны отношением агрегации, а были независимы друг от друга в иерархии доменных сущностей. Я решил эту проблему через медиаторы. Но решение мне не очень нравится, особенно если потребуется задействовать какие-то сервисы в ходе процесса изменения, прокидывать сервисы в модели или создавать между моделями прямые связи вообще не хочется, по хорошему нужно вынести это в UseCase, но тогда я не смогу гарантировать, что бизнес логика всегда соблюдается, а гарантировать это хочется на уровне кода, хотя возможно я упоролся)
А почему решили использовать иммутабельный подход и самописный DI? Как я понимаю захотели ограничения на уровне DI в инъектировании сервисов или были еще причины? Я взял mobx + typedi++ (наследник typedi).
Clean Architecture во frontend: почему я ушёл от FSD