Skip to content

Latest commit

 

History

History
377 lines (301 loc) · 23.4 KB

app-splitting.md

File metadata and controls

377 lines (301 loc) · 23.4 KB

Разбиение приложения

О способах разбиения проекта по модулям и позиции методологии


TL;DR:

  • Методология рекомендует разбивать проект следующим образом:

    1. Сначала по слайсам - согласно скоупу влияния модуля

      app, processes, pages, features, entities, shared

    2. Затем по доменам - согласно конкретной функциональности БЛ

      Нейминг очень зависит от конкретного проекта

    3. И наконец по типам - согласно назначению модуля в коде и реализации

      ui, model, api, lib

  • Структура приложения должна помогать внедрять новых людей, предсказуемо вносить изменения и развивать проект, в условиях постоянно меняющихся требований

  • Разбиение согласно методологии - в конце статьи


💤 Предистория

Каждый из нас застал на своем пути проекты примерно такой структуры

└── src/
    ├── api/
    ├── components/
    ├── containers/
    ├── helpers/
    ├── pages/
    ├── store/
    └── index.tsx/
Ведь на первый взгляд - такой подход кажется простым
  • Все лежит на верхнем уровне

    Нет глубокой вложенности и длинным импортам!

  • В каждом модуле конкретно понятно что лежит, по его прямому назначению

    В store - все экшены/редьюсеры/эффекты приложения, в хелперах - все хелперы и т.д.

И более того - каждый из нас, поначалу, создавал проекты с такой структурой!

Особенно если мы говорим про React

(*) Запомним эту конструкцию, и вернемся к ней позднее

💥 Жизненный цикл проекта

Проекты не живут в статике. Они постоянно развиваются и дополняются требованиями, новым функционалом.

При этом мы не знаем новые требования наперед - и это тоже вызывает проблемы, при структурировании модулей

Потому хочется, чтобы архитектура и структура приложения - позволяла масштабировать кодовую базу и комманду - с наименьшими негативными последствиями для проекта

Вот в чем беда

  • Обычно изначально проект разрабатывается под изначальный слепок требований с определенным контекстом

    Если мы попытаемся добавить новые/изменить прежние - поначалу еще как-то удается их вписать в существующую архитектуру

    Но скорей всего - если архитектура приложения не заточена под изменения в функционале - кодобаза превратится в спагетти-код, который будут понимать только те, кто стояли у истоков

  • У нас, разработчиков, нет "шара предвидения". Тем не менее мы постоянно пытаемся что-то предсказать

    • какой модуль будет переиспользован в дальнейшем (а может вынести все в shared?),
    • где добавить прослойку для уменьшения прямой связности модулей
    • и т.д.

Последствия

  • И из-за этого, обычно, проект - это большой набор костылей, подпирающих динамически меняющийся функционал

    Будь то A/B тесты, или же просто новый пласт требований - который "плохо кладется" под имеющуюся архитектуру

  • Ситуация становится еще "веселее", когда старый код уже настолько покрылся мхом - что начинают сыпаться баги из-за регрессий и высокой связности

    А поскольку о модуляризации и изолированности изначально чаще всего никто не хотел/не успел подумать - нет возможности взять и отрефакторить начисто только один модуль

    В некоторых случаях это может даже загубить проект, если не соблюдаются практики код-ревью, рефакторинга и покрытия тестами

  • По итогу, чаще всего, получаем то, чего хотели избежать в проекте

    • Оверсложность
    • Непредсказуемость
    • Конфликты при добавлении изменений
    • Cложность в освоении новыми людьми
    • и т.д.

🤔 Как быть?

Я думаю мало кто из нас может похвастаться талантом "провидца", и разделять модули в приложении под будущие изменения корректно

А потому, нам - разработчикам - стоит принять следующее:

  1. Контекст разработки и доменной области каждого конкретного проекта - равен нулю в начале, и увеличивается только с развитием проекта

    Т.е. мы в подавляющем большинстве случаев не знаем куда положить определенную фичу, какой модуль может быть переиспользован в будущем для приложения и т.д.

  2. Проект постоянно будет меняться, а потому - лучше изначально готовить архитектуру к возможным изменениям

    При этом важен баланс: возможно, не стоит добавлять прослойку для работу с API "на будущее"

    Однако логика должна быть распределена так - чтобы мы могли завтра ее относительно легко дополнить/переиспользовать или же вообще удалить и написать именно эту часть по-новой

💠 Подходы к структуризации

Вернемся к структуризации.

Очень хочется, чтобы структура и архитектура проекта позволяла бы снизить негативное влияние перечисленных выше проблем

Но увы - на данный момент в мире фронтенда нет каких-то устоявшихся подходов, которые бы встречались из проекта в проект

Да, у нас есть принципы Separation of Concerns, есть DDD, есть SOLID и т.д.

И даже их каждый интерпретирует по-своему - и это еще одна проблема

Однако если отбросить детали - можно условно выделить два базисных подхода к структуризации

1. Flat Structure

FIRST by-types, THEN by-domains

Помните изначальную структуру (*)? Обычно ее называют "плоской"

  • Сначала - идет разделение по типам
  • Затем - по доменам
Пример структуры
└── src/
    ├── api/            # API приложения для конкретных доменов
    |   ├── users/
    |   └── posts/
    ├── ui/             # UIKit
    ├── containers/     # Контейнеры (упаси бог) для конкретных доменов
    |   ├── users/
    |   └── posts/
    ├── helpers/        # Хелперы для конкретных доменов
    |   ├── users/
    |   └── posts/
    ├── pages/          # Страницы для конкретных доменов
    |   ├── users/
    |   └── posts/
    ├── store/          # Store-логика доменов
    |   ├── users/
    |   └── posts/
    └── index.tsx/

Заметьте, что чаще всего одни и те же сущности/фичи - повторяются на внутреннем уровне верхних модулей

Плюсы/Минусы
  • 😊 Низкая вложенность, облегчает импорты
  • 😊 Легко реализовать для простых проектов
  • 😡 Сложно "прочитать" проект, при добавлении новых людей в команду

    Так называемые discoverability и navigation-in-project

  • 😡 Идет упор на "технологичное применение" модуля, а не на "функциональное"

    Из-за чего сложно "прочитать" проект и поддерживать связи меж модулями в желаемом состоянии

  • 😡 Логика размыта по всему проекту

    Из-за чего при внесении изменений в проект - она разбивается сразу на все директории на верхнем уровне

    Помимо того, что это может привести к конфликтам - это гораздо сложней вычитывать и дебажить

  • 😡 Сложно избавиться от сильной связности модулей

    Что ведет к непредсказуемому взаимодействию модулей друг на друга

  • 😡 Сложно поддерживать модули, при росте кодовой базы

    Особенно увидеть и гарантировать - где используются, например контейнеры для постов

2. Domain Based Structure

FIRST by-domains, THEN by-types

А теперь попробуем наоборот

  • Сначала - идет разделение по доменам
  • Затем - по типам
Пример структуры
└── src/
    ├── ui/             # Общий UIKit, без привязки к БЛ
    ├── helpers/        # Общий набор хелперов, без привязки к БЛ
    ├── posts/          # Функционал постов (со всеми необходимыми ресурсами)
    |   ├── (api/)
    |   ├── ui/
    |   ├── store/
    |   ├── helpers/
    |   ├── (pages/)
    |   └── posts/
    ├── users/          # Функционал пользователей (со всеми необходимыми ресурсами)
    |   ├── (api/)
    |   ├── ui/
    |   ├── store/
    |   ├── helpers/
    |   ├── (pages/)
    |   └── posts/
    └── index.tsx/

Заметьте, что да - теперь у нас все повторяется внутри сущностей, однако теперь каждая "фича" содержит в себе все что нужно

И исправляя что-то в функционале users, вы затрагиваете только эту директорию и не больше!

Плюсы/Минусы
  • 😊 Легко добавлять/изменять функциональность

    Просто добавить/подправить отдельную папку - без влияния на другие

  • 😊 Каждая фича содержит в себе только то что нужно

    Нет ничего лишнего, сразу видно все используемое в фиче

    В том числе - это упрощает модель (*store) самой фичи

  • 😊 Модули изолированы друг от друга

    Что значительно помогает в разработке и исправлении ошибок

  • 😊 Логика распределена по фичам(доменам), а не по всему проекту

    Что упрощает изучение проекта и навигацию по нему

    И разработку - в том числе

  • 😊 Легко расширять команду

    Можно выделить команду/конкретных людей на отдельные фичи(домены) - и они смогут вести разработку независимо друг от друга, с наименьшим числом конфликтов

    А также - гораздо легче изучать сам проект (например не весь сразу - а только конкретную функциональность)

  • 😊 Проект становится масштабируемым, поддерживаемым и читабельным
  • 😡 Требуется много усилий, чтобы оставлять фичи изолированными

    Это в целом, достаточно давний вопрос

    Но обычно - дело решается добавлением новых абстракций (processes/entities)

    Хоть и требуется доп. код, для поддержания изолированности - бенефиты от этого все равно перевешивают

  • 😡 Структура чаще всего - становится слишком вложенной
  • 😡 Иногда сложно выделить - куда положить логику кода для pages, для виджета, для конкретной сущности и т.д.

☄️ Позиция методологии

Да, как ни странно - мы склоняемся больше к domain-based подходу 😄

Однако понятно, что у него есть свои проблемы

А потому, методология вводит дополнительную группу slices - для группировки доменов определенного скоупа влияния

app > processes > pages > features > entities > shared

Разделение

  1. Группа абстракций slices: на уровне распределенных скоупов логики приложения

    app, *processes, pages, features, *entities, shared

  2. Группа абстракций domains: на уровне конкретной функциональности БЛ

    Все, что связно с бизнес-логикой вашего приложения для данной абракции фичи:

    • (processes) auth, payment, ...
    • (features) auth-by-phone, pay-by-card, inline-post, ...
    • (entities) user, post, task, ...
    • ...
  3. Группа абстракций types: на уровне конкретного назначения модуля в коде

    ui, model, lib, *api

По структуре можно отследить...
  • Сначала идет разделение по слайсам

    это код для приложения/процесса/страницы/фичи/... ?

  • Затем подбирается конкретный домен

    в зависимости от БЛ приложения

  • И наконец - выбирается тип модуля

    в зависимости от технического назначения - UI/BL/Utils/...

Иными словами...

Разделение по доменам(фичам) - замечательно

Но когда все домены лежат в куче в руте - среди этого сложно ориентироваться

Еще сложнее, управлять грамотно скоупом "знания" и "опасности изменений"

А так, мы явно задаем правила из структуры:

  1. Модуль "знает" только про себя и нижележащие модули, но не выше лежащие

    По уровню знания/ответственности

    • app > *processes > ... > entities > shared
  2. Чем ниже расположен модуль - тем опаснее вносить в него изменения

    Т.к. скорее всего он заиспользован во многих вышележащих местах

    По уровню опасности изменений

    • shared > entities > ... > *processes > app

Таким образом - каждая группа абстракций служит для своих целей, и при этом позволяет на уровне конвенций распределять код

Структура

└── src/
    ├── app/                    # Slice: Приложение
    |                           #
    ├── (processes/)            # Slice: Процесс (опционален)
    |   ├── {some-process}/     #   Domain: (н-р процесс CartPayment)
    |   |   ├── model/          #       Type: Бизнес-логика
    |   |   └── lib/            #       Type: Инфраструктурная-логика (хелперы)
    |   ...                     #
    |                           #
    ├── pages/                  # Slice: Страница
    |   ├── {some-page}/        #   Domain: (н-р страница ProfilePage)
    |   |   ├── ui/             #       Type: UI-логика
    |   |   ├── model/          #       Type: Бизнес-логика
    |   |   └── lib/            #       Type: Инфраструктурная-логика (хелперы)
    |   ...                     #
    |                           #
    ├── features/               # Slice: Фича
    |   ├── {some-feature}/     #   Domain: (н-р фича AuthByPhone)
    |   |   ├── ui/             #       Type: UI-логика
    |   |   ├── model/          #       Type: Бизнес-логика
    |   |   └── lib/            #       Type: Инфраструктурная-логика (хелперы)
    |   ...                     #
    |                           #
    ├── entities/               # Slice: Бизнес-сущность
    |   ├── {some-entity}/      #   Domain: (н-р сущность User)
    |   |   ├── ui/             #       Type: UI-логика
    |   |   ├── model/          #       Type: Бизнес-логика
    |   |   └── lib/            #       Type: Инфраструктурная-логика (хелперы)
    |   ...                     #
    |                           #
    ├── shared/                 # Slice: Переиспользуемые ресурсы
    |   ├── api/                #       Type: Логика запросов к API
    |   ├── ui/                 #       Type: UI-логика
    |   └── lib/                #       Type: Инфраструктурная-логика (хелперы)
    |   ...                     #
    |                           #
    └── index.tsx/              #

Ограничение на абстракции

Учитывая вышесказанное - разумно ограничить использование каждой группы в пути файла до максимум одного раза

# Dirname:
{slice}/{domain}/{type}/...

# Для shared нет доменов - т.к. там располагаем переиспользуемую логику
# Без привязки к БЛ
- {shared}/{-}/{ui}/button
- {entities}/{viewer}/{lib}/use-auth
- {features}/{auth-by-phone}/{ui}/...
- {pages}/{profile}/{model}
- ...

Так пропадают все вопросы типа

  • А могут ли быть вложенные фичи?
  • А может ли быть entity внутри page?
  • и т.д.

📑 См. также