Это третий принцип. Весь список здесь.

Периодически слышу и вижу в комментариях, что структура папок не должна отображать архитектуру и наоборот. У меня другое мнение. Повторю: это мой опыт, вам он может не подойти. Проверьте, протестируйте.

Я исхожу из того, что код — это продукт, и первый пользователь — это коллега рядом. Он первым будет его смотреть, комментировать, рефакторить. Структура папок и кода — это дизайн продукта. Хороший дизайн соответствует принципу наименьшего удивления. Я делю код по смыслу согласно «решётке», поэтому организую структуру папок 2-я способами в зависимости от проекта:

  • Layer Folder Structure (LFS)

  • Scope Folder Structure (SFS)

Layer Folder Structure (LFS), когда папки верхнего уровня — это слои, а внутренние папки — это области или произвольная организация. Такая структура удобна в 2-х случаях:

  • для микросервисов на бэке. Микросервис покрывает только одну область. Дополнительное деление на более мелкие области будет лишним. Слои полезно обозначить.

  • для монолита или PWA. В этом случае:

    • границы UI компонент в слое контроллеров не совпадают границами модулей. Т. е. UI компонент не часть модуля.

    • UI компонент может использовать много разных модулей из слоя приложения и инфраструктуры одновременно.

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

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

Scope Folder Structure (SFS), когда папки верхнего уровня — это области, внутренние папки — это слои. Удобно для сервисов на бэке. Здесь модуль определяет публичные интерфейсы взаимодействия, поэтому

  • контроллеры — часть модуля

  • границы модуля чёткие и понятные во всех слоях

  • модуль отвечает принципу единственной ответственности

  • код внутри модуля связан по смыслу.

Я использую эту структуру для бэковых сервисов. Сервис — покрывает поддомен, модуль — какой-то из интерфейсов API.

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

Рано или поздно появится общий код между несколькими модулями внутри слоя. Он распространяется горизонтально в границах слоя (см. «решётку»). Универсальный способ организации — создать папку pkg или packages на самом верхнем уровне. Туда кладём общий код как модули, каждый в свою папку. Такой способ практикуется в go и js проектах. Для LFS можно поступить немного проще: сделать папку внутри слоя для общего модуля.

Это выглядит примерно так:

Layer Folder Structure (LFS)

src/
├── ui/               # слой контроллеров    
│   ├── components/
|	├── layouts/
│   ├── route/
│   ├── pages/
│   └── filters/
├── app/              # слой приложения 
│   ├── user
|   ├── search 
|	├── cart
|	├── category
│   └── ...              
├── infra/            # слой инфраструктуры  
│   ├── user
|   ├── search 
|	├── cart
|	├── category
|	├── http-client   # общий http клиент, как модуль внутри слоя 
│   └── ...              
├── dc/               # конфигурация как контейнер зависимостей
│   ├── user.js
|   ├── search.js 
|	├── category.js
│   └── ...              
└── pkg/        # общие пакеты
    ├── model/  # библиотека для создания всех моделей внутри слоя приложения 
    └── ...

Scope Folder Structure (SFS)

src/
├── modules/
|   ├── search/               
|   │   ├── controllers/
|   |	├── app/
|   │   └── infra/
|   ├── category/          
|   │   ├── controllers/
|   |	├── app/
|   │   └── infra/
|   └── ...              
├── dc/              
│   ├── search.js 
|	├── category.js
│   └── ...              
└── pkg/        
    ├── model/  # библиотека для создания всех моделей внутри слоя приложения 
    ├── http-client   # общий http клиент
    └── ...

Многие не согласятся с такой структурой, я отвечу так: представь, ты новичок в проекте. Ты что-то слышал о слоённой архитектуре, примерно понимаешь, что такое модули. Ты ещё не открывал проект, не знаешь структуру папок.

К тебе пришёл пользователь/менеджер с проблемой: у меня не работает поиск, листинг товаров и т. п.

Твой вопрос — а это где? Быстрый ответ — страница поиска, где-то в Каталоге. Ты идёшь в гитлаб и ищет что-то похожее на Каталог. Допустим, это бэк сервис. Нашёл. Дальше ищешь что-то похожее на Поиск или Листинг. Если это go, скорее всего, будешь искать в internal. Заходишь внутрь. Видишь понятные модули: search, listing... Проблема с поиском — понятно куда идти, других вариантов нет. Идёшь. Видишь слои. Заходишь в контроллеры. Здесь всё понятно — это консольная команда, воркер или grpc. Как это в реальности запускается, ты разберёшься потом, когда поймёшь, что есть Di, конфиг и т. д. Но сейчас ты уже локализовал область в общей кодовой базе, ты знаешь, где искать проблему.

Бизнес/пользователь в 100% случаях формулирует задачу относительно конкретной функции в определённой области. В этой структуре новый разработчик, вероятнее всего, будет следовать именно такому сценарию, потому что эта структура соответствует принципу наименьшего удивления.

Пожалуйста, держи детали внутри модуля или слоя. Не нужно на верхнем уровне создавать папки port, adapter, persistent, repository, tools, utils... Здесь они слишком абстрактны, поэтому бессмысленны. Каждый понимает их по-своему. Может быть, даже так, что ты сам не понимаешь смысла, который вкладываешь.

Faceted-seach — нельзя понять по-своему, это модуль фасетного поиска. Всё, что внутри — часть реализации задачи фасетного поиска. Репозитории, сервисы, правила, события, модели, dto, vo — всё это есть. Есть ещё много всего, но всегда в контексте модуля или слоя.

Когда ты выносишь детали наверх, то сразу нагружаешь деталями коллегу-разработчика, увеличиваешь его когнитивную нагрузку. Ты разбиваешь внутреннее содержание модуля или слоя по папкам верхних уровней, тем самым уничтожаешь видимые границы модулей и слоёв. Ты разрушаешь «решётку», семантическую прочность модуля.

В книге «Предметно-ориентированное проектирование. Структуризация сложных систем» Эрик Эванс написал:

То, что при делении на модули должна соблюдаться низкая внешняя зависимость (low coupling) при высокой внутренней связности (high cohesion) — это общие слова. Определения зависимости и связности rрешат уклоном в чисто технические, количественные критерии, по которым их якобы можно измерить, подсчитав количество ассоциаций и взаимодействий. Но это не просто механические характеристики подразделения кода на модули, а идейные концепции. Человек не может одновременно удерживать в уме слишком много предметов (отсюда низкая внешняя зависимость). А плохо связанные между собой фраrменты информации так же трудно понять, как неструктурированную «кашу» из идей (отсюда высокая внутренняя связность).

Поэтому, пожалуйста, держи детали внутри.

Мой канал

Комментарии (17)


  1. nin-jin
    25.05.2025 20:53

    Правильная структура папок выглядит всё же так:

    /
    ├── user/
    │   ├── user.model.ts
    |	├── card/
    │   ├── row/
    │   ├── link/
    │   └── edit/
    ├── search/
    │   ├── search.model.ts
    |	├── form/
    │   └── results/
    ├── cart/
    │   ├── cart.model.ts
    │   ├── page/
    |   └── snippet/ 
    ├── category/
    │   ├── category.model.ts
    │   ├── page/
    |   └── snippet/ 
    └── model/
    


    1. abratko Автор
      25.05.2025 20:53

      Я не знаю, что такое "Правильная структура". Я предпочитаю пользоваться термином - логичная.

      Наверно, ваш вариант, в вашем контексте - логичный. В моём - нет.


      1. nin-jin
        25.05.2025 20:53

        Лет через 10 поймёте, что ваша "логичная структура" не масштабируется. Простите за спойлер.


        1. abratko Автор
          25.05.2025 20:53

          1. Не понимаю, как структура папок может масштабироваться. Что Вы имеете ввиду?

          2. Я нигде не написал , что она масштабируется.

          3. На данный момент я уверен, что она НЕ должна масштабироваться. Она должна эволиционировать в пакеты. Максимальная эволюция, когда проект состоит только из пактов и конфига. Моя структура этому не мешает, скорее даже способствует.


          1. nin-jin
            25.05.2025 20:53

            Очень просто: корзина разрастается настолько, что для работы с ней выделяется отдельная команда, которой нужен отдельный репозиторий с отдельным релизным циклом. Ваши действия?


            1. abratko Автор
              25.05.2025 20:53

              так... допустим... Собираем слои в пакет, кладем в папку pkg на "передержку". Что бы дозрел. Выносим пакет в отдельный репозиторий. Корзина релизится как пакет с версионностью и все дела. При этом пакет может содержать все три слоя, а может только 1 слой, а может сразу содержать все слои сконфигурированные в модуль. Здесь зависит от контекста.

              Конечно, я сделаю сразу пакет, если точно уверен в его границах. Если не уверен не сделаю.
              Но я не знаю будущее, иначе я бы сейчас не писал коммент, а играл на бирже или в казино.


              1. nin-jin
                25.05.2025 20:53

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


                1. abratko Автор
                  25.05.2025 20:53

                  Все верно, согласен по всем пунктам. SFS структура более гибкая. То что написано в статье не конфликтует с вашим подходом, по смыслу это одно и тоже.


                  1. nin-jin
                    25.05.2025 20:53

                    И никак не кореллирует.


            1. abratko Автор
              25.05.2025 20:53

              корзина разрастается настолько

              В моем понимании "масштабируется" и "разрастается" - это разные термины.


            1. abratko Автор
              25.05.2025 20:53

              А... еще если Вы посмотрите на SFS структуру, то можете заметить, что это почти тоже самое что у Вас в первом комментарии.

              Поэтому даже в моём контексте Ваша структура вполне логична и соотвествует тому, что написано в статье.


  1. supercat1337
    25.05.2025 20:53

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

    Работа с api сервера - отдельный пакет. Всякие сервисы, например, шифрование, - отдельный пакет. UI kit - отдельный пакет. Даже общий компонент как меню навигации для группы страниц - тоже может стать полноценным отдельным пакетом.

    Вот такая структура позволяет сосредоточиться на реальном фронтенде: сама страничка, импортированные сконфигурированные ui-компоненты и бизнес-логика.

    Даже если вы работаете в соло, можно импортировать пакеты из локальных папок и прекрасно с этим работать.

    Чем проще, понятнее, тем быстрее напишете продукт.


    1. abratko Автор
      25.05.2025 20:53

      Всё верно. Так и происходит. После стабилизации границ модуля, его зависимостей, связей каждый модуль/компонент стремится превратиться в отдельный самостоятельный пакет в своём репозитории. Мы тоже так делаем.


  1. AlexFTF
    25.05.2025 20:53

    Многие не согласятся с такой структурой, я отвечу так: представь, ты новичок в проекте. Ты что-то слышал о слоённой архитектуре, примерно понимаешь, что такое модули. Ты ещё не открывал проект, не знаешь структуру папок.

    Почему многие не согласятся и какую альтернативу они предлагают?


    1. abratko Автор
      25.05.2025 20:53

      Потому что мало кто создает слои, как отдельные папки. Т.е. создают папку для области, а в ней сразу папки для портов, моделей и т.п. Границы слоев не понятны.

      Либо все это на самом верху, и тогда еще печальнее. Самый первый комментарий это подтверждает.


  1. SadOcean
    25.05.2025 20:53

    Эти архитектурные паттерны Я слышал под названием feaure-first и layer-first.

    В целом хочется отметить, что стоит стремиться к feature first подходу, т.к он лучше поддерживает декомпозицию и понятнее для новых людей.

    К сожалению, он сложнее чем layer-first для новичков и для собственно проектирования.

    Универсального ответа нет, в вашем домене и на вашем проекте может быть лучше другой подход


  1. jbourne
    25.05.2025 20:53

    Суть статьи и выводы, мне кажется, очень сочетаются с "Законом Мелвина Конвея".

    Всегда считал его глубоким и отвечающим на многие вопросы CS.