Как организовать структуру приложения

от автора


🔔 Введение

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

В этой статье мы разберём, что значит «понятная структура кода», и почему её наличие столь важно для успешной разработки.

Критерии понятности

  • Каждый файл и папка имеют своё чёткое назначение. Организация файловой системы проекта должна быть интуитивно понятной.

  • Логика разделена по модулям, слоям или функциональным зонам. Разделение логики способствует упрощению понимания системы и снижению связанности компонентов.

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

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

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

  • Архитектура учитывает возможное расширение проекта. Проект должен быть построен с учётом его возможного масштабирования и изменения в будущем.


🔖 Монолитная

Какие есть проблемы?

Слишком общий уровень абстракции

Структура построена на основе категорий, которые представляют собой технические аспекты кода (например, Service, Repository, Validator). Такой подход не учитывает предметную область приложения. Это затрудняет понимание системы, особенно для новых разработчиков, которые не знают, какие модули относятся к какой функциональности.

Отсутствие модульности

Это приводит к тому, что изменение в одной части кода может потребовать модификации файлов в разных директориях, повышая вероятность ошибок. При добавлении нового функционал, в 95% соучаев вам придется распределить код по нескольким директориям. Это увеличивает когнитивную нагрузку, так как логика одного модуля разбросана по всему проекту. Также это увеличивает число конфликтов которые возникнут при слиянии веток, так как разные разработчики будут изминять один и тот же код. 

Сложность навигации

Директории превращаются в «свалку», где накапливаются файлы разных функционалов, которые тяжело найти и классифицировать.

Нарушение DDD

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

Проблемы с масштабируемостью

При увеличении объема кода предложенная структура начинает «раздуваться». Папки становятся переполненными файлами, их имена начинают дублироваться. Также увеличиваеться размер самих файлов что приводит уже к другим запахам кода.

Нарушения SOLID

S — Директории вроде Service, Helper, Constants нарушают SRP, потому что они группируют файлы не по функциональности, а по техническому назначению. Это приводит к тому, что в одной папке могут находиться классы, отвечающие за совершенно разные аспекты системы. 

O — Данная структура затрудняет расширение функциональности без модификации существующего кода. Например, добавление новой бизнес-логики требует модификации существующих сервисов или валидаторов, что нарушает принцип открытости/закрытости

L —  Не обнаружено. 

I — Общие папки вроде Helper или Validator способствуют созданию утилитарных классов, которые предоставляют слишком широкий интерфейс. Это вынуждает пользователей использовать методы, которые им не нужны. 

D — В текущей структуре зависимости часто определяются на уровне инфраструктуры (Repository, ElasticSearch), а не на уровне абстракций. Это нарушает DIP, поскольку модули начинают зависеть от деталей реализации, а не от абстракций.

Нарушения GRASP

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

Low Coupling & High Cohesion — Код одного модуля разрывается на множество папок, что снижает когезию. Вместо того чтобы объединить взаимосвязанные классы, они разбросаны по проекту. Текущая структура может провоцировать излишнюю связанность между модулями.

Нарушения KISS

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


🔖 MVC

Какие есть проблемы?

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

Многослойная

Преимущества структуры

У нас появляеться четкое разделение ответственности. Изменения в одном слое не будет влиять на другие слои (например, замена базы данны). Новые слои или модули добавляются без значительных изменений в остальных. Каждый слой отвечает за строго определенные задачи: 

  • Presentation — обработка пользовательских запросов.

  • Application — координация действий, вызов бизнес-логики.

  • Domain — ядро, содержащее бизнес-логику.

  • Infrastructure — технические детали.

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

Какие есть проблемы?

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

Нарушения SOLID

S — код, относящийся к одной функциональности, размазан по слоям. Это усложняет понимание системы. Например, модуль заказов (Order) вынужден включать отдельные компоненты в каждом слое, вместо того чтобы быть локализованным.

O —  Не обнаружено.

L — Не обнаружено.

I — Не обнаружено.

D — При не правильной организации кода, может нарушать этот принцып. Когда интерфейсы определены вместе с реализацией в Infrastructure а не в Domain.

Нарушения GRASP

Low Coupling & High Cohesion — Код, все еще связанный с одной функциональностью, размазан по нескольким директориям (слоям). Также при не правильной организации кода, может нарушать этот принцып когда например, Application напрямую зависит от Domain, а Domain от Infrastructure.Чтобы не допустить этого нужно использовать интерфейсы для коммуникации между слоями и избегать прямых зависимостей.

Дублирование кода

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

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

  • Изменения в одном месте не должны влиять на другое.

  • Контексты использования существенно различаются

Дублирование логики

вот что действительно опасно! Не нужно путать эти два понятия — случайное совпадение кода и истинное дублирование бизнес-логики.


🔖 Чистая Архитектура

Use Cases (Используемые сценарии) — приложения, которые обрабатывают бизнес-логику, взаимодействуют с сущностями и выполняют конкретные операции в приложении.

Преимущества структуры

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

Удобная навигация по проекту. Логичное и последовательное распределение файлов по категориям (например, Entities, UseCases, Presenters) делает навигацию по проекту интуитивно понятной. Разработчики могут быстро найти нужный компонент, не тратя время на поиск.

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

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

Какие есть проблемы?

Имеет похожие проблемы с неструктурированным способом организации.


🔖 Гексагональная

Inbound Ports (входные порты): Определяют интерфейсы, через которые внешние компоненты или системы взаимодействуют с вашим приложением (например, API, веб-контроллеры, команды).

Outbound Ports (исходящие порты): Определяют интерфейсы для взаимодействия приложения с внешними сервисами, такими как базы данных, внешние API, очереди сообщений, и т. д.

Adapters:  Реализует интеграцию с внешними системами

Какие есть проблемы?

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

Нарушения SOLID

S — Может быть нарушен SRP, если адаптеры начинают выполнять дополнительные функции помимо реализации порта, например, обработку бизнес-логики.

O —  Не обнаружено.

L —  Не обнаружено.

I —  Не обнаружено.

D —  Не обнаружено.

Нарушения GRASP

Не обнаружено.

Может быть нарушен Information Expert, если ответственность за логику, которая лучше подходит для другой части системы, ложится на слой приложения или интерфейса.

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


🔖 DDD (Проектирование, ориентированное на предметную область)

Преимущества структуры

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

Инкапсуляция. Логика каждого модуля отделена, что помогает минимизировать влияние изменений в одном модуле на другие. Код четко разделен на уровни. Слои внутри модуля остаются изолированными от других модулей.

Масштабируемость. Добавление нового функционала или модуля (например, Partner) не требует изменений в существующих модулях.

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

DDD. Предложенная структура хорошо соответствует подходу. Есть разделение модулей на домены и также есть четкое выделение слоев.

Какие есть проблемы?

Shared code

Может появиться Shared код. Добавление модуля Shared для общих компонентов (например, валидаторов, исключений, утилит) может привести к антипаттерну, если использовать его неправильно. Чтобы избежать этого нужно добавлять в Shared только строго общие и высокоизолированные компоненты и следить за тем, чтобы Shared не превратился в «мусорную корзину».

Нарушения SOLID

  • Не обнаружено. 

Может быть нарушен DIP, если не использовать DI для подстановки реализации.  

Может быть нарушен ISP при роздувании интерфейсов.

Нарушения GRASP

  • Не обнаружено

Может быть нарушен High Cohesion, если слои будут содержать не связаный код. Код который используеться вместе должен лежать вместе. 

Может быть нарушен Low Coupling, если не использовать события или шину сообщений для асинхронного взаимодействия.


✨ Советы

Совет №1. Иногла слои могут быть избыточными. Например. если у нас есть не большей домен и мы с точностю можем сказать что он не будет расширяться.

Совет №2. Иногда слои могут включать несколько поддоменов. В этом случае структура будет расширена.

Совет №3. Перенос конфигурации, миграций и фикстур в границы модулей. Это делает модуль полностью самодостаточным: все, что нужно для разработки, тестирования, и развертывания, находится в одном месте. 

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

  • Все тесты, относящиеся к модулю, хранятся в его папке Tests, что улучшает изоляцию. 

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

Пример реализации на Symfony:

class Kernel extends \Symfony\Component\HttpKernel\Kernel {     use MicroKernelTrait;      protected function configureRoutes(RoutingConfigurator $routes): void     {         $configDir = $this->getConfigDir();         $routes->import($configDir.'/routes.yaml');          $this->loadDomainConfigurations($routes);     }      protected function loadDomainConfigurations(RoutingConfigurator $routes): void     {         $projectDir = $this->getProjectDir();         $finder = new Finder();         $finder->directories()             ->in($projectDir.'/src')             ->depth(0)             ->notName('Kernel.php');          foreach ($finder as $domainDir) {             $domainPath = $domainDir->getRealPath();             $domainName = strtolower($domainDir->getBasename());              // Load API routes             $apiPath = $domainPath.'/Presentation/Api';             if (is_dir($apiPath)) {                 $routes->import($apiPath, 'attribute')                     ->prefix('/'.$domainName);             } else {                 // Search in subdirectories for API routes                 $subFinder = new Finder();                 $subFinder->directories()                     ->in($domainPath)                     ->depth(0);                  foreach ($subFinder as $subFolder) {                     $apiPath = $domainPath.'/'.$subFolder->getBasename().'/Presentation/Api';                     if (is_dir($apiPath)) {                         $routes->import($apiPath, 'attribute')                             ->prefix('/'.$domainName);                     }                 }             }              // Load Web routes             $webPath = $domainPath.'/Presentation/Web';             if (is_dir($webPath)) {                 $routes->import($webPath, 'attribute')                     ->prefix('/'.$domainName);             } else {                 // Search in subdirectories for Web routes                 $subFinder = new Finder();                 $subFinder->directories()                     ->in($domainPath)                     ->depth(0);                  foreach ($subFinder as $subFolder) {                     $webPath = $domainPath.'/'.$subFolder->getBasename().'/Presentation/Web';                     if (is_dir($webPath)) {                         $routes->import($webPath, 'attribute')                             ->prefix('/'.$domainName);                     }                 }             }              // Load YAML routes if they exist             $routesPath = $domainPath.'/Resources/Config/routes.yaml';             if (file_exists($routesPath)) {                 $routes->import($routesPath);             } else {                 // Search in subdirectories for routes.yaml                 $subFinder = new Finder();                 $subFinder->directories()                     ->in($domainPath)                     ->depth(0);                  foreach ($subFinder as $subFolder) {                     $routesPath = $domainPath.'/'.$subFolder->getBasename().'/Resources/Config/routes.yaml';                     if (file_exists($routesPath)) {                         $routes->import($routesPath);                     }                 }             }         }     }      protected function buildContainer(): ContainerBuilder     {         $container = parent::buildContainer();          // Load domain services         $projectDir = $this->getProjectDir();         $finder = new Finder();         $finder->directories()             ->in($projectDir.'/src')             ->depth(0)             ->notName('Kernel.php');          // Load services         $loader = new YamlFileLoader($container, new FileLocator());         foreach ($finder as $domainDir) {             $domainPath = $domainDir->getRealPath();             $servicesPath = $domainPath.'/Resources/Config/services.yaml';              if (file_exists($servicesPath)) {                 $loader->load($servicesPath);             } else {                 // Search in all subdirectories                 $subFinder = new Finder();                 $subFinder->directories()                     ->in($domainPath)                     ->depth(0);                  foreach ($subFinder as $subFolder) {                     $servicesPath = $domainPath.'/'.$subFolder->getBasename().'/Resources/Config/services.yaml';                      if (file_exists($servicesPath)) {                         $loader->load($servicesPath);                     }                 }             }         }          return $container;     } }

Вот и все 🎉, спасибо за прочтение, не забудьте подписаться на меня 😊 и похлопать 👏 статье.


ссылка на оригинал статьи https://habr.com/ru/articles/873880/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *