Всем привет! 13 ноября, в официальном Telegram канале Feature Sliced Design состоялся релиз новой версии архитектурной методологии. Он принёс в себе несколько важных, фундаментальных изменений, о которых мы сегодня и поговорим.
Если вы не знакомы с архитектурной методологией Feature Sliced Design, можете познакомиться с ней здесь.
Предисловие
Если Вы не подписаны на Telegram канал FSD, вероятно Вы даже не знаете, что минорная версия методологии обновилась, так как нигде на официальном сайте (кроме страницы "Migration Guide") нельзя прочесть новость об этом (прошло уже более месяца).
Полный Changelog обновления можно посмотреть только в соответствующем файле на Github.
P.S: Весь нижеописанный текст является субъективным мнением автора и подлежит конструктивной критике в любом виде.
Про "Pages first" подход
Главным изменением в новой версии методологии стал "pages first" подход.
Он предлагает ограничивать декомпозицию слайсов на слое "pages" (страницы), если "entities", "features" и "wigdets" внутри данной страницы не требуется пере использовать где-то ещё. Приведём пример.
До версии 2.1, разбиение слайсов для простого ToDo List приложения могло выглядеть следующим образом:
? app
-
? pages
? ToDo
-
? features
-
? todo
-
? ui
?CreateTask
?RemoveTask
-
-
-
? entities
-
? todo
-
? ui
?TaskForm
?TaskList
-
-
Теперь же, с приходом "pages first", предлагается сохранить логику, которая не будет пере использоваться, внутри слоя "pages" (структура проекта примерная, так как официального примера в документации к 2.1. версии не существует):
? app
-
? pages
-
? ToDo
-
? logic
?CreateTask
?RemoveTask
-
? ui
?TaskForm
?TaskList
-
-
Казалось бы, стало лучше, ведь "entities" и "features" из слоёв на глобальном уровне были перемещены внутрь страницы, в которой используются и область их применения стала сужена только данной страницей. Однако, такие изменения приводят к существенным проблемам.
Почему "pages first" подход плох?
На мой взгляд, отдавая приоритет к декомпозиции на уровне одного слоя, разработчик потенциально столкнётся со следующим набором проблем:
Становится сложнее выстраивать структуру большого объёма кода в рамках одного слоя. То есть, среднестатистический разработчик уровня "junior" или "pre-middle", вероятно выстроит архитектуру внутри слоя "pages" хуже, чем с чётким разделением слоёв, задающих свою структуру.
Появляется "дополнительный шаг" при поиске нужного участка кода.
Ранее, храня всю логику внутри глобальных "entities", "features" и "wigdets" слоёв, было очевидно, что для поиска условного UI компонента "BlogPost" мне необходимо перейти по следующему пути "entities/blog/ui/BlogPost", теперь же, с равной вероятностью этот компонент может лежать как по пути "entities/blog/ui/BlogPost", так и по пути "pages/Blog/ui/BlogPost".Вся четкая иерархия слоёв, выстроенная до 2.1 версии, фактически "множится на 0", так как польза от её использования появляется только при декомпозиции пере используемой логики, что вероятно, не является частым кейсом при разработке.
Вообще, данный подход существовал и ранее, просто не был официально задокументирован, однако официальный линтер Steiger всегда предлагал перенести код, который не импортируется куда-либо более одного раза на слоях "entities", "features" и "wigdets" на более вышележащие слои.
На что можно заменить "pages first"?
Разумным вариантом я считаю такой формат декомпозиции слоёв:
На слое "entities" хранятся слайсы с UI, мапперами, хэлперами и т.д. не несущими внутри себя сложной логики и действующими по принципу "черной коробки", отображая, либо преобразуя уже имеющийся набор данных. (Например верстка компонента шапки, сайдбара и т.д.)
На слое "features" хранятся слайсы с вариантами использования "entities" или "shared" слайсов либо собственного, самодостаточного кода без их использования. (Например кнопка "Logout").
На слое "widgets" хранятся слайсы с крупными логическими блоками, а также вариантами использования "entities", "features" или "shared" слайсов либо собственного, самодостаточного кода без их использования. (Например форма заказа товаров).
На слое "pages" хранится только "скелет", компонующий в себе слайсы из нижележащих слоёв и содержащий логику для их взаимодействия. Собственного аналога логики, сходной с той, что уже находится в слайсах на слоях "entities", "features" и "widgets", слайс на слое "pages" иметь НЕ может.
Конечно же вышеописанный вариант чётким разделением логики на глобальном уровне слоёв имеет свои недостатки, такой например как излишне "широкая" декомпозиция, когда для разработки простой фичи приходится порождать внушительный объём кода на нескольких слоях, вместо того, чтобы объединить всю логику в одном файле. Однако же, субъективно, он выглядит более логичным, чем "pages first".
Про легальный механизм кросс-импортов
Проблема, с которой сталкивается наверное каждый разработчик, который хотя бы раз использовал FSD, это кросс-импорты.
Кросс-импорт - ситуация, при которой в рамках одного слоя появилась необходимость импорта данных одного слайса в другой.
Ранее FSD предлагала экспериментальный подход к разрешению кросс-импортов, однако теперь механизм, благодаря которому есть возможность в явном виде их описать вышел в стабильную фазу. Рассмотрим же его подробнее.
В Changelog к 2.1 версии не написано ни слова об этом механизме кросс-импортов, однако в рамках дискуссии с FSD Core Team Member на Youtube канале, было сказано, что данный механизм является частью обновления.
Для разрешения механизма кросс-импортов появилась "@x"-нотация, позволяющая описать публичный API, через который возможно выполнить кросс-импорт. Возьмём пример из документации FSD:
-
? entities
-
? A
-
? @x
? B.ts — специальный публичный API только для кода внутри entities/B/
? index.ts — обычный публичный API
-
-
Затем код внутри entities/B/ может импортировать из entities/A/@x/B:
import type { EntityA } from "entities/A/@x/B";
Такой механизм является более явным, за счёт создания явной промежуточной абстракции, описывающей конкретные экспортируемые модули, однако на мой взгляд, здесь также имеются существенные проблемы.
Чем плох "легальный" кросс-импорт?
На самом деле, создав дополнительную абстракцию, ситуация значительно не улучшилась. Кросс-импорт так и остался кросс-импортом, со следующим набором проблем:
Увеличивается связность слайсов по отношению друг к другу. Их становится сложнее переносить между слоями (если появилась необходимость). "@x"-нотация никак не решает эту проблему.
Не исключаются циклические зависимости между импортируемыми модулями. (Когда модулю "А" нужен кросс-импорт из модуля "B", а модулю "B" нужен кросс-импорт из модуля "А").
Лучшим решением для вашего проекта, субъективно, будут является полный отказ от кросс-импортов, однако же, если это невозможно, предложенный командой FSD вариант является приемлемым, если вы понимаете вышеописанные недостатки.
Послесловие
Новая версия методологии FSD предоставила несколько интересных нововведений, но необходимо понимать, чем может быть чревато их использование. Я постарался внести немного ясности для тех моментов, которые вызвали вопросы лично у меня.
Если у Вас есть другие моменты, которые не были описаны в данной статье и требуют внимания - милости прошу в комментарии, буду рад обсудить =)
Комментарии (19)
HungryGrizzzly
17.12.2024 09:46Наконец-то, спустя года,
переизобрелидошли до так называемого "page first" подхода, который является интуитивным и классическим) Если честно, на крупных проектах FSD - ооочень плохая идея. Он не является интуитивно понятным и логичным, каждый его еще и переделывает под себя. В итоге получаем просто монстра, с которым не понятно, как работать. А для того, чтобы убедиться, что этот подход не очень, достаточно зайти на сайте FSD в раздел с примерами и увидеть, что даже в маленьких проектах все не то, чем кажется, да и сами проекты нарушают теже самые правила FSD.alexanderpankratov03 Автор
17.12.2024 09:46Для стандартизации правил FSD существует линтер Steiger. Если разработчики его не используют, вопрос скорее к ним, нежели к команде разработки архитектуры.
По поводу использования FSD в крупных проектах, могу предложить как вариант монорепу с N-ным количеством небольших FSD приложений и разрулить потом всё через, например, ModuleFederationPlugin в сборщике.clerik_r
17.12.2024 09:46По поводу использования FSD в крупных проектах, могу предложить как вариант монорепу с N-ным количеством небольших FSD приложений и разрулить потом всё через, например, ModuleFederationPlugin в сборщике.
Oh my dear god... куда катиться этот мир. Неужели мы обречены
markelov69
Ключевое слово вероятно. А в реальности, скорее наоборот, внутри слоя "pages" он и любой другой выстроит архитектуру лучше. А вообще в целом FSD подход это шлак. Но в этой новой версии, они хотя бы сделали шаг ан встречи для адекватных людей.
Чаво чаво? Вместо 3 ненужных папок сделали одну, а значит сократили кол-во лишних шагов. А вы говорите дополнительный шаг добавился.
Четкая иерархия? Смешно.
Да никто никогда не знает сразу куда запихнуть тот или иной код, а может в entities? А может в features? А может в wigdets? И по факту каждый раз перед разработчиком встает головоломка, в итоге все превращается в убогое месиво размазанное по этим 3м папкам и ты никогда не знаешь сразу куда пойти чтобы найти то, что тебе нужно.
А 2.1 это наоборот шажок в сторону классической архитектуры нормального человека, где никаких проблем нет и не может быть, вне зависимости от того, маленький у тебя проект или гигантский. Ибо там всё максимально очевидно, просто и наглядно.
Так же как и FSD в целом выкинуть в урну и вернуться к классическому подходу. А не выдумывать проблемы самому себе и пытаться их решить жалкими способами.
Начнем с того, что кросс-импорт вообще не плох, а очень удобен. А если вы не умете с ним работать, то это сугубо ваши проблемы.
Это все равно что отказаться от классов в пользу функций. Или от функций в пользу классов. И т.д. и т.п. все подходы хороши и нужно брать лучшее из каждого и пользоваться, а заковывать себя в кандалы, отказываясь от чего-то и плакать.
alexanderpankratov03 Автор
В Вашем комментарии много заявлений и мало аргументации.
1. Говоря о том, что любой разработчик выстроит архитектуру внутри pages грамотнее, чем с чётким разбиением по слоям, хотелось бы видеть небольшой пример того, как бы Вы сделали это сами. Не уверен, что результат был бы оптимален.
2. В абзаце про "дополнительный шаг" я написал, почему считаю, что он появляется, так как появляется неочевидность в вопросе местонахождения того или иного модуля. Вы уверены, что дочитали абзац до конца?
3. Согласен с тем, что бывают проблемы с тем, в какой слой (entities, features или widgets) положить модуль, однако не вижу проблемы в том, чтобы единожды договориться об этом внутри команды и следовать договорённости.
4. Видно Ваше неприятие в сторону FSD и симпатию к некой "классической" архитектуре. Это очень размытое понятие, хотелось бы конкретики, что для Вас хорошая архитектура.
5. Очевидных плюсов использования кросс-импортов лично я не вижу, зато вижу минусы, о которых и написал в статье. Хотелось бы узнать, каковы же плюсы использования кросс-импортов на ваш взгляд, ведь даже разработчики FSD в документации рекомендуют максимально их избегать.
clerik_r
А если они скажут что есть и пить это плохо, да и спать тоже, что тогда?
Вы понимаете что апеллировать к "авторитетам" это прям скажем странно. Ибо во первых они авторитетами даже близко не являются, во вторых это не точные науки, где 2 + 2 = 4 и это можно легко проверить и однозначно в этом убедиться.
А плюсы же очевидны и просты, если модулю A нужно что-то из модуля B, ты просто берешь это и всё. Даже если A <--> B имеют зависимость друг от друга, то все решается просто, зависимости не должны вызываться синхронно в момент инициализации, достаточно сделать setTimeout и все. А если в момент инициализации синхронно никто никого не дергает, а дергает в процессе, то тем более никаких проблем.
Ну она же на то и классическая, что вполне себе стандартная на уровне интуитивных вещей. Базовый вид:
Дальше уже в зависимости от проекта, от уровня разраба и т.п. Заметьте, тут даже никакие пояснения не нужны, никакие договаривалки и изучение. Всё предельно понятно на интуитивном уровне.
Да ладно? Прямо заранее все все все вариации и кейсы обсудили и договорились да? И ещё где-то записали. И память прям у всех идеальная. Прям реалистичность зашкаливает. А теперь вернемся в реальность, разработка это не черное и белое, там 100500 вариацией и плетеней, зависимостей, сайд эффектов и т.п. Об этом нельзя договориться, поэтому на каждый чих ты думаешь а куда же это положить ту или иную фигню, ведь мы же такие умные и выбрали FSD (Freaky Sliced Design), а теперь страдаем.
Ну и опять же к чему все эти сложности на ровном месте? Когда на самом деле все предельно просто.
alexanderpankratov03 Автор
1. Конечно же бывают споры о том, куда положить модуль и после того, как команда это обсудила. Я имею в виду, что после обсуждения, БОЛЬШИНСТВО вопросов о том куда положить модуль исчезнут сами собой. Большинство не равно все, так как мы не живём в идеальном мире.
2. Касательно предложенного вами варианта структуры проекта. Если речь идёт о небольшом приложении, такая структура вполне жизнеспособна и выглядит даже лучше, чем FSD, однако если мы возьмём банальную ситуацию в которой вам нужен для нескольких страниц одинаковый, достаточно большой кусок логики (например какая-нибудь форма заказа), окажется, что либо придётся копипастить код между страницами, либо кросс-импортить компоненты между страницами, либо раздувать components большим количеством компонент.
3. Решение зависимостей через SetTimeout выглядит слегка костылём, когда других вариантов не нашлось.
4. Не совсем понимаю, о каких сложностях идёт речь. FSD концепции довольно просты для понимания. Если методология отступает от концепций очевидного наименования логики, она сразу становится нежизнеспособной?
clerik_r
Не понял вообще проблемы, ну есть у вас общая логика, вынесете ее в виде отдельного компонента в
src/components/MainForm
и все, на всех страница где нужна эта форма берите ее. Это же интуитивно понятно.Костыли костылям рознь. Есть безобидные, а есть жесткие. Либо максимальное удобство, и безобидный setTImeout либо изворачивайся через специальные модули агрегаторы и т.п.. По мне setTImeout выигрывает по всем фронтам ибо не несет когнитивной нагрузки, не усложнят код и т.п.
entities, features, widgets вообще прям все "просто" и "понятно" и голову ломать не надо каждый раз, ага. И прям "легко" по коду проекта искать то, что тебе нужно. Вместо того что страницу положить в ее законное место, мы будем разность ее по частям по разным папкам. Ну бред же.
В данном случае да. Она изначально была нежизнеспособная. Причем чтобы это понять, не надо было даже ее на реальном проекте применять, достаточно было просто провести мысленный эксперимент.
Какая разница небольшое приложение или огромное? Она одинаково интуитивно понятна всегда. В этом и смысл делать все так, чтобы было интуитивно понятно.
alexanderpankratov03 Автор
Я понимаю и принимаю Ваш ход мыслей. Вопрос о идеальной архитектуре скорее риторический, и продолжая диалог мы вряд ли изменим мнения друг друга.
clerik_r
Верно, просто я не вставляю палки себе в колеса и всё) И не понимаю тех кто вставляет) Разумеется вкусы у всех специфичны, кому то подавай велосипед с круглыми колесами, кому-то с квадратными. Те, кто предпочитают квадратные, не понимают как можно ездить на круглых и наоборот.
markelov69
Разумеется он был бы оптимален, всё просто. Я предлагаю идти ногами вперед, и это интуитивно понятно для всех людей, для этого не надо быть гением. А вы предлагаете ходить задом наперед, ещё и ноги под неестественными углами ставить. Как думаете кто пройдет дальше, быстрее и легче? Думаю ответ очевиден.
Дочитал. Но вы сами написали ерунду, фейковый "аргумент". С чего вдруг в "pages first" страница будет лежать в "entities/blog/ui/BlogPost"? Это же в высшей степени противоестественно. У нее единственный вариант, лежать в папке pages. Поэтому в случае в pages first никаких доп шагов быть не может, в поиске страниц по крайней мере.
Но всё равно FSD всё ещё непригодна к использования, да и никогда не будет пригодна. Задумка изначально противоестественная.
Всё уже придумано давно на интуитивном уровне(это я про классическую архитектуру) и варианта лучше просто объективно нет.
alexanderpankratov03 Автор
То есть Вы заявляете, что результат был бы оптимален и не решаетесь предоставить пример?) Сильная аргументация)