Pull to refresh
68.24
Холдинг Т1
Многопрофильный ИТ-холдинг

Проблемы структуры проектов при использовании Feature-Sliced Design

Level of difficultyMedium
Reading time7 min
Views1.3K

Меня зовут Сергей Сибара, я фронтенд-разработчик в компании ИТ-холдинг Т1. В этой статье я решил рассказать о нескольких проблемах, к которым приводит методология Feature-Sliced Design, — отсутствию правил взаимодействия между сегментами и большой раздробленности связанной функциональности, — а также о возможных решениях. Статья будет полезна тем, кто уже знаком с методологией FSD, и тем, кто отказался от неё. А поскольку я разрабатываю на React, то и статью писал с небольшим уклоном в эту технологию.

Терминология 

Чтобы у нас с вами не было путаницы, я добавил небольшой глоссарий по используемым терминам:

  • Модуль — любой файл с кодом.

  • Домен — группа модулей, предназначенных для реализации части предметной области приложения. Пример: домен страниц просмотра каталога товаров, домен страницы корзины. 

    Домен может состоять из поддоменов. Инфраструктурные модули не относятся к доменам, например общие компоненты формы, утилиты работы со строками. 

    Этот термин по смыслу близок к термину «домен» из Domain-Driven Design. Но я лишь очень поверхностно знаком с DDD, поэтому не возьмусь утверждать, что моё толкование этого термина правильное.

  • Подсистема — группа модулей, сфокусированная на реализации какой-либо связанной части приложения. К подсистемам относятся любые домены, а также любые группы инфраструктурных и бизнес-модулей. Может состоять из вложенных подсистем. Другими словами, этот термин охватывает инфраструктурную и бизнес-функциональность. 

  • Паттерн «фасад». Позволяет скрыть внутреннюю реализацию и сложность подсистемы путём сведения внешних обращений к одному объекту, который уже внутри себя ссылается на внутренние объекты, функции и модули. То есть вместо того, чтобы внешние модули напрямую обращались к внутренним элементам подсистемы, они обращаются к ним через одну точку взаимодействия с подсистемой, называемую «фасадный объект». В простых случаях он один, но у одной подсистемы может быть несколько фасадных объектов для обращения к ней из разных подсистем ради избегания ненужных связей между одним фасадным объектом и другими подсистемами. В FSD есть понятие public API, описывающее похожий механизм, но с использованием не объекта, а файла index.js. Так как они очень похожи, в рамках статьи я иногда использую термин «фасад».

Принципы, на которых я акцентирую внимание и к которым стремится FSD:

  1. High Cohesion из GRASP. Принцип гласит, что в одной и той же подсистеме, модуле, классе или функции желательно писать только код, предназначенный для решения одной задачи либо связанных или похожих задач.

  2. Low Coupling из GRASP. Этот принцип связан с предыдущим и гласит, что подсистемы, модули, классы, функции и прочие сущности, решающие разные задачи, желательно делать менее связанными друг с другом.

  3. Группировать файлы по папкам стоит стремиться в первую очередь по назначению (по задачам, для решения которых они предназначены). Группирование файлов по типу (хуки, компоненты, утилиты, типы, константы) уже не первое десятилетие считается плохо масштабируемым. Этот принцип пересекается с предыдущими. Если группировать модули по подсистемам, то размещение в подсистеме не связанных с ней модулей будет нарушением вышеперечисленных принципов.

Первая проблема: отсутствие правил взаимодействия между сегментами

В документации написано, что сегменты могут быть произвольными, но нет достаточно понятного определения, что такое сегмент. Также нет чётких указаний, от чего зависит набор сегментов. Часто создают сегменты вроде hooks, types, consts. В документации упоминается, что такое группирование ошибочно, потому что это возврат к группированию файлов по типу, а не по назначению. 

Сегменты в FSD — это практически аналоги паттернов MVC, MVP, MVVM, Flux и т. д., разделяющих приложение на слои. Исключение — сегмент lib. Обычно используют стандартные сегменты: api, model, ui, lib, config. Независимо от выбранного паттерна, для любой бизнес-логики используют сегмент model. Получается, что его используют как универсальный для любого проекта. К тому же, если бизнес-логики много, то в этот сегмент складируют много несвязанных модулей: хранилище, схемы валидации и прочее. В таких случаях с сегментом становится неудобно работать. Но внутри него можно создать дополнительные папки и договориться в команде о связях между ними.

Я считаю, что раз паттернов разделения на слои много, то и в FSD набор сегментов должен зависеть от выбранного паттерна, но не обязательно именовать их так же. Если в паттерне больше трёх слоёв, то объединять несколько в один сегмент model, на мой взгляд, не очень правильное решение.

FSD не накладывает никаких ограничений на взаимодействие между сегментами. Я считаю это серьёзной недоработкой. Многие десятилетия индустрия разработки программного обеспечения стремится к чёткой связи между слоями, к фиксированному потоку данных. Поэтому я считаю, что строгая связь между сегментами нужна, и её должен диктовать паттерн, выбранный разработчиками для своего проекта. В небольших слайсах проблемы незаметны, они проявляют себя при увеличение количества модулей в слайсе.

Вторая проблема: связанная функциональность слишком раздроблена на слои

Слайсы FSD из одного или нескольких разных слоёв (pages, widgets, features, entities) для работы с одной бизнес-сущностью (или группой связанных бизнес-сущностей в одних и те же слайсах) я отношу к одному домену. То есть, на мой взгляд, в FSD (особенно до версии 2.1, в которой стали рекомендовать использовать подход pages-first) домен обычно разделён на несколько FSD-слоёв в виде слайсов.

На иллюстрации показано то, о чём я говорю: в каждом слое есть слайсы двух доменов — domainA и domainB. Конечно, даже по FSD слайсы разных слоёв не обязаны называться одинаково и относиться к одному домену. Каждый слайс следующего слоя может работать с большим количеством сущностей, чем слайс в нижележащем слое. Зачастую такие слайсы делают вложенными: их группируют в один слайс по одной общей сущности. Выносить часть функциональности домена в отдельный слой — это нормально. Плохо, когда это делают не для переиспользования сейчас или в дальнейшем. В таком случае вынесение на слой ниже приносит больше вреда, чем пользы. В итоге может получиться как на иллюстрации. Слайс уровнем ниже нужен не для работы с определённым доменом или страницей, а в первую очередь для вынесения какой-либо функциональности для использования в нескольких слайсах на уровнях выше, и в конечном итоге, в разных доменах.

Разделение домена на множество слоёв — это одна из самых больших проблем FSD, особенно до версии 2.1. В результате усложняется разработка из-за того, что связанная функциональность дробится на несколько слоёв, которые  находятся далеко друг от друга. Нормально, что на этом уровне вложенности файловой иерархии лежат несвязанные подсистемы в виде слайсов. Но плохо, когда связанная и тем более не переиспользуемая функциональность размещена в нескольких папках-слоях. Стоит стремиться переносить связанную функциональность на более низкий (глубокий) уровень внутри одной папки либо в соседние папки, но не на тот же уровень в соседних слоях. То есть, если бы папки entities, features и widgets, относящиеся к одному домену или странице, были бы внутри pages/SomePage, то это было бы куда меньшим нарушением High Cohesion и Low Coupling. Я не имею в виду, что внутри страниц нужно заводить папки entities, features и widgets. Я говорю о том, что стоит стремиться уменьшать разбиение страниц на слои. 

Одной из причин большего числа слоёв была попытка избежать кросс-импортов в бизнес-функциональности. Но, во-первых, в большинстве проектов вряд ли возможно полностью избежать кросс-импортов. Да и потребовалось бы вводить ещё больше слоёв. Во-вторых, исправлять кросс-импорты разделением на большое количество слоёв — очень сомнительное решение. На мой взгляд, нормально разделять один большой слайс на несколько на том же уровне, уменьшая количество связей между ними. А если какая-то его часть часто переиспользуется в других слайсах, то уже выносить её на один слой ниже. Если же модули слайса используются только в нём или в одном-двух других слайсах, то размещение этих модулей в разных слоях проекта зачастую может принести больше вреда, чем пользы. Естественно, что некоторые домены в какой-то степени связаны между собой. И связи эти лучше минимизировать. Делается это в первую очередь не с помощью разделения на несколько слоёв, а с помощью, например, таких паттернов как «фасад» (public API в FSD). Здесь я говорю не о том, что плохо выносить модули, нужные в нескольких слайсах слоя pages, а о том, что часто плохо продолжать выносить модули дальше, из слоя widgets в слой features, а оттуда в слой entities. Зачастую кросс-импорты являются меньшей болью, чем вынесение части функциональности в другой слой. Также иногда лучше дублировать небольшую часть функциональности.

Группирование по составным частям страницы находится уровнем выше, чем сама страница

На мой взгляд, было ошибкой вводить слои widgets, features и, возможно, entities. Такое разделение выглядит более приемлемым внутри папок-страниц, но не уровне слоев проекта. Разделение кода на основе того, сколько ответственности ему требуется и от скольких других модулей он зависит, слабо пересекается с группированием по связанной функциональности. Также в FSD до версии 2.1 это нарушает изоляцию модулей страницы: модули, ненужные нигде, кроме определённой страницы, доступны любым модулям в слоях выше них.  

Вместо слоёв widgets и features в большинстве проектов достаточно было бы сделать слой переиспользуемой бизнес-логики с редкими кросс-импортами через public API. До появления FSD я неоднократно использовал подобный слой в проектах среднего размера, и проблем не возникало. Слоя вроде entities у меня не было. Насчёт него я не уверен. Может, в некоторых больших проектах он будет полезен.

Feature-first вместо Pages-first

В версии FSD 2.1 пришли к более правильному решению по сравнению с тем, что было: стали размещать всё связанное только с определённой страницей в слайсе этой же страницы в слое pages. Но можно пойти ещё дальше: к группированию всех модулей, включая страницы, по функциональности (по доменам).

Евгений Паромов рассказал об интересном подходе feature-first. Также об этом подходе можно почитать в статье «Структура Flutter-приложения: feature-first или layer-first». Приведу цитату оттуда:

«На этом этапе у вас может возникнуть соблазн создать по одной папке для каждого экрана вашего приложения. Но это ошибка, потому что функции (фичи) — это не экраны.»

Вместо группирования по страницам предлагается группировать по бизнес-функциональности (по фиче). В такой фиче может быть несколько страниц для работы с какой-либо сущностью или несколькими сущностями. Например, страница просмотра списка и страница создания или редактирования какой-либо сущности из базы данных. Или какой-нибудь небольшой раздел на сайте. 

Эпилог

У меня в планах выложить статью с описанием ошибок, которые я сам делал и встречаю в проектах, использующих эту методологию. Хочу показать, что FSD покрывает далеко не все ситуации, которые возникают в разработке, и иногда эффективнее нарушать правила методологии. Возможно, со временем выложу пример, как я бы организовал файловую структуру проекта. 

Ссылки на полезные и использованные при написании статьи материалы:

  1. Cohesion и Coupling: отличия

  2. Релиз FSD 2.1! Обсуждаем с кор-тимой FSD плюсы и минусы

  3. Структура Flutter-приложения: feature-first или layer-first

  4. Архитектура на паттернах FSD | Пробуем evolution-design linter — интересный разрабатываемый линтер для настройки в проекте своих архитектурных правил.

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments2

Articles

Information

Website
t1.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия
Representative
ИТ-холдинг Т1