
В предыдущей статье «Как организовать структуру приложения» обсуждались различные подходы к организации кода, включая монолитную архитектуру, многослойную архитектуру и принципы чистой архитектуры. В ней я акцентировал внимание на важности понятной структуры кода, которая облегчает понимание, внедрение новых функций и снижает вероятность ошибок. Особое внимание уделялось критериям понятности, таким как чёткое назначение файлов и папок, разделение логики по модулям или функциональным зонам, использование самодокументируемых названий и соблюдение стандартов кодирования. Также были рассмотрены проблемы, возникающие при неструктурированной организации кода, такие как сложность навигации, отсутствие модульности и нарушение принципов SOLID и GRASP.
В этой статье мы продолжим эту тему, сосредоточившись на сравнении двух подходов к организации кода: Package by Feature и Package by Layer. Мы подробно рассмотрим, как каждый из этих методов влияет на структуру проекта, поддерживаемость и масштабируемость кода.
🏗 Примеры из реальной жизни
Пример №1. Строительство города
Представь, что ты строишь город. В этом городе есть дома, школы, магазины, парки.Ты можешь организовать город по-разному:
Package by Layer – это когда все одинаковые здания стоят в одном месте: Все магазины в одном районе, Все дома в другом, Все школы в третьем. Хотели бы жить в таком городе?
Package by Feature – это когда у тебя есть районы, где всё нужное находится рядом: В одном районе есть и дома, и магазины, и школы – всё для жителей этого района.
Пример №2. Организация одежды в гардеробе
Представь, что ты складываешь одежду в шкафу. Ты можешь разложить ее по-разному:
Package by Layer – все футболки на одной полке, все брюки на другой, а все носки в отдельном ящике.
Package by Feature - А теперь представь, что ты складываешь вещи для разных занятий. Все вещи для плавания - в одном месте (купальник, полотенце, очки), все вещи для школы - в другом (учебники, тетради, ручки).
Пример №3. Организация кухни ресторана
Представь, что у нас есть ресторан и несколько поваров и нам пришел заказ приготовить пиццу, салат и десерт. У тебя есть много ингредиентов и инструментов: мука, сыр, помидоры, огурцы, яйца, миксер, ножи, доски для нарезки и так далее. Тебе нужно организовать всё так, чтобы готовить было удобно.
Package by Layer - Этот способ похож на то, как ты можешь разложить всё по типам, независимо от того, для какого блюда это нужно. Например:
Коробка "Все овощи"
Коробка "Все инструменты"
Коробка "Специи"
Если ты готовишь пиццу, тебе нужно взять коробку с овощами, коробку с инструментами, коробку с сыпучими продуктами и коробку с посудой. Это занимает много времени и сил. Ты можешь запутаться, потому что всё разбросано по разным коробкам.
Package by Feature - ты можешь разложить всё, что нужно для каждого блюда, в отдельные коробки или зоны на кухне. Например:
Коробка "Пицца"
Коробка Салаты
Коробка Дисерты
Ты не путаешься, потому что всё для каждого блюда лежит в одном месте. Повара не мешаю друг другу.
Пример №4. Собираем школьный рюкзак
Представь что нам нужно собрать школьный рюкзак.
Package by Layer - это как сложить все учебники вместе, все тетради вместе, все ручки вместе. Удобно, когда нужно найти все учебники, но неудобно собираться по предметам.
Package by Feature - это как сложить отдельно все для математики (учебник, тетрадь, линейка), отдельно все для рисования (альбом, краски, кисточки). Удобно собираться на конкретный урок, но сложнее найти все учебники сразу.
🏛 Как парадигмы программирования влияют на организацию кода?
Прежде чем углубляться в организацию кода, важно понимать, что разные парадигмы программирования могут влиять на выбор между Package by Layer и Package by Feature.
🔗 Обектно ориентированое програмирования (ООП) и организация кода
ООП — это парадигма, в которой программа строится вокруг объектов, которые представляют собой экземпляры классов. Основные принципы ООП включают:
Инкапсуляция: Сокрытие внутреннего состояния объекта и предоставление доступа к нему только через методы.
Наследование: Возможность создания новых классов на основе существующих, что позволяет повторно использовать код.
Полиморфизм: Возможность объектов разных классов обрабатываться как объекты одного класса.
Абстракция: Упрощение сложных систем путем моделирования классов, которые отражают только существенные характеристики.
Package by Feature
Package by Feature естественным образом поддерживает принципы ООП, такие как инкапсуляция и сокрытие реализации. Классы, связанные с одной функциональностью, группируются вместе, что упрощает понимание и поддержку кода.
Позволяет инкапсулировать всю логику, связанную с конкретной функциональностью, внутри одного пакета, что соответствует принципам инкапсуляции и абстракции.
Такой подход способствует созданию высокосвязных модулей с низкой связанностью между ними, что соответствует принципам SOLID.
Легче соблюдать принцип единственной ответственности (SRP), так как каждый пакет отвечает за конкретную функциональность.
Лучше сочетается с DDD (Domain-Driven Design), так как позволяет группировать объекты, связанные с одной бизнес-фичей, в одном месте.
Package by Layer
Package by Layer в ООП часто встречается в классических монолитах, где слои (контроллеры, сервисы, репозитории) разделены, и каждый слой отвечает за свою часть логики.
При package by layer объекты и классы, отвечающие за разные аспекты одной функции, также оказываются разделенными по разным слоям. Это может затруднить применение принципов ООП, таких как полиморфизм, и привести к созданию более сложных и запутанных связей между объектами.
🔗 Функциональное программирование (ФП) и организация кода
ФП — это парадигма, в которой программа рассматривается как набор функций, которые принимают входные данные и возвращают результаты. Основывается на идее чистых функций, неизменяемости данных и композиции. Основные принципы ФП включают:
Чистые функции: Функции, которые не имеют побочных эффектов и возвращают одинаковый результат для одних и тех же входных данных.
Неизменяемость: Данные не изменяются после создания, вместо этого создаются новые данные.
Функции высшего порядка: Функции, которые могут принимать другие функции в качестве аргументов или возвращать их.
Рекурсия: Часто используется вместо циклов для обработки данных.
Функциональное программирование основывается на идее чистых функций, неизменяемости данных и композиции. В ФП акцент делается на том, что делает программа, а не на том, как она это делает. Это ведёт к иному подходу к структуре кода:
Package by Feature
Хорошо сочетается с ФП, так как функциональность группируется вокруг конкретных задач или доменных областей. Это позволяет создавать модули, которые инкапсулируют логику, связанную с определенной функцией.
Функции, которые работают с одними и теми же данными или решают одну задачу, находятся в одном месте, что упрощает композицию функций и их повторное использование.
Такой подход способствует созданию чистых функций, так как каждая функциональность изолирована и не зависит от глобального состояния.
Package by Layer
Противоречит принципам ФП, так как он разделяет код по техническим критериям, а не по функциональности. Это может затруднить композицию функций и привести к разбросу логики по разным слоям. Функциональный код может стать менее читаемым, если функции, связанные с одной задачей, находятся в разных пакетах.
👴 Что говорят эксперты?
1️⃣ Эрик Эванс в "Domain-Driven Design"?
Эрик Эванс вводит четырехслойную архитектуру (Layered Architecture), которая помогает разделить бизнес-логику, инфраструктуру, пользовательский интерфейс и доступ к данным. Однако он не настаивает на конкретной структуре директорий, а лишь подчеркивает важность разделения зон ответственности.
"При создании сложных программ критически важно разделить код на слои с разными зонами ответственности."
— Эванс, DDD, Глава 4 "Изоляция доменной логики"
📌 Четыре слоя по Эвансу:
Доменный слой (Domain) должен быть изолирован от инфраструктуры. (Entities, Value Objects, Aggregates)
Сценарии использования (Application) управляют бизнес-процессами, но не содержат бизнес-логики. (Use Cases)
Инфраструктурный слой (Infrastructure) реализует хранилища данных, API, интеграции.
Интерфейсный слой (UserInterface) отвечает за взаимодействие с пользователем
Эванс не говорит, что именно так должна быть организована структура файлов. Он дает концепцию разделения ответственности, но не конкретное правило именования папок. Также Эванс подчеркивает важность Bounded Context и разделения кода по контекстам, а не только по слоям.
"Когда большая система делится на несколько Ограниченных Контекстов (Bounded Contexts), каждый из них имеет свою собственную доменную модель и логику. Код должен быть организован так, чтобы четко отражать эти границы."
Это намекает на Package by Feature, когда код разделен по фичам/контекстам (Bonus/, Customer/, Order/), а не просто слоям (Domain/, Application/, Infrastructure/).
2️⃣ Вон Вернон в "Implementing Domain-Driven Design"
Вон Вернон также поддерживает разделение слоев, но он более гибко подходит к организации кода. В книге "Implementing DDD" он даже упоминает Package by Feature как возможный вариант!
Bounded Context и организацию кода:
Ограниченный контекст (Bounded Context) — это центральный паттерн в Domain-Driven Design (DDD). Он определяет границы, в рамках которых конкретная модель применима и остается согласованной. Организация кода вокруг ограниченных контекстов, а не только вокруг технических слоев, помогает сохранять ясность и соответствие бизнес-домену.
"Ограниченный контекст (Bounded Context) — это логическая граница. Применение архитектурных слоев внутри него — это выбор, а не требование. Главное — сохранить чистоту и выразительность доменной модели в рамках этой границы."
- Вернон, IDDD, Глава 2: Границы контекста
Организация кода и возможные подходы:
«Организация кода строго по слоям часто приводит к искусственному разделению ответственности. Вместо этого рассмотрите возможность группировки кода по доменным концепциям в первую очередь, а затем решите, нужны ли слои внутри каждого контекста.»— Вон Вернон, Реализация Domain
- Вернон, IDDD, Глава 4: Стратегическое проектирование с использованием ограниченных контекстов*
Что это говорит о структуре кода?
"DDD не диктует жесткую структуру пакетов. Важно, чтобы код отражал границы контекста и модель домена."
— Вернон, IDDD, Глава 2 "Границы контекста"
Он признает, что Package by Feature можно использовать внутри DDD и советует группировать код по Bounded Context, а внутри можно использовать слои. Также предлагает гибридный вариант между Package by Layer и Package by Feature:

3️⃣ Карлос Буэносвинос, Кристиан Соронеллас и Кейван Акбари в книге «DDD in PHP»
В книге PHP in DDD также затрагивается вопрос организации кода, включая модели, слои и Bounded Context. Авторы книги ориентируется на принципы Эванса и Вернона, но с адаптацией под PHP.
Поддержка Bounded Context и Package by Feature
"Хорошая архитектура PHP организует код вокруг доменных концепций, а не технических слоев. Каждый контекст должен иметь свою собственную доменную логику, независимую от инфраструктуры."
— PHP в DDD, Глава 6: Организация кода с использованием ограниченных контекстов.
Поддерживает Bounded Context и Package by Feature, а не только слои. Код внутри Domain Layer должен скрывать детали реализации и работать через Use Cases.
Слои используются гибко
"Слоеная архитектура — полезный инструмент, но она не должна диктовать, как структурировать ваше PHP-приложение. Главное — отделить бизнес-логику от инфраструктуры."
— PHP в DDD, Глава 10: Роль уровней приложений и инфраструктуры
4️⃣ Дядюшка Боб в чистом коде
Взгляните на Принципы дизайна упаковки дядюшки Боба. Он объясняет причины и мотивы, лежащие в основе этих принципов, которые я подробно рассмотрел ниже. Следующие три принципа упаковки касаются связности упаковки, они говорят нам, что положить внутрь упаковки:

Как это связано с пакетированием по функциям и пакетированием по слоям?

Принципы дяди Боба показывают, что Package by Feature — более гибкий и масштабируемый подход, так как:
✅ Избегает циклических зависимостей.
✅ Каждая фича меняется независимо.
✅ Группирует код по смыслу, а не по техническим слоям.
Однако, если вы используете Package by Layer, вам нужно очень тщательно управлять зависимостями, чтобы не нарушить принципы! 🚀
Сравнение подходов к организации кода в DDD

Ключевые моменты:
🔹 Ограниченный контекст как основа (как подчеркивают все три автора).
🔹 Package by Context + Package by Layer внутри каждого контекста.
🔹 Гибкое использование слоев (отделяем домен от инфраструктуры, но избегаем излишней сложности).
🔹 Доменная модель играет ключевую роль (Сущность, Агрегат, Объекты-Значения).
🔹 DDD не диктует структуру папок, но требует четкого разделения ответственности.
🔹 Главный принцип: DDD → сначала контексты, затем слои, а не наоборот! 🚀
🎯 Use Cases в Чистой архитектуре и Package by Feature
Use Cases — это один из ключевых элементов Чистой архитектуры, который отвечает за обработку бизнес-логики.
Package by Feature — это способ организации кода, при котором все файлы, связанные с одной функциональностью (фичей), хранятся в одной папке.
Эти два подхода отлично дополняют друг друга: Use Cases позволяют выделить бизнес-логику, а Package by Feature делает кодовую базу более модульной и понятной.

Use Cases и Package by Feature не противоречат друг другу. Напротив, они отлично работают вместе, поскольку обе методики направлены на улучшение структуры кода.
Use Cases помогают изолировать бизнес-логику и делают код тестируемым.
Package by Feature позволяет хранить весь код, связанный с одной функцией, в одном месте, делая его модульным.
Комбинируя эти подходы, вы получаете лучшее из обоих миров. В результате достигается:
Гибкий код, сгруппированный по фичам, который можно легко перемещать и тестировать независимо.
Поддерживаемый и масштабируемый код, который проще сопровождать и расширять.
Удобная для разработчиков кодовая база, в которой легче ориентироваться и работать.
Такое сочетание ведет к чистой, модульной и эффективной архитектуре. 🚀
💁 Когда что использовать?
В PHP-проектах оба подхода — Package by Feature и Package by Layer — применяются в зависимости от архитектурных решений и масштабов проекта. Давай разберем их с примерами.
Package by Layer подходит, когда:
У вас небольшое приложение
Много новых разработчиков в команде
Нужна простая, понятная структура
Функциональность тесно связана между собой
Package by Feature подходит, когда:
У вас большое приложение
Разные команды работают над разными функциями
Функциональность слабо связана между собой
Важна независимость модулей
Планируете переход на микросервисы
Проект большой с четкими доменными границами
Команда знакома с DDD
Используете собственную архитектуру или модульные фреймворки
🎯 Как организовать код?
Давайте рассмотрим на примерах.
Package by Layer (Пакетизация по структурным слоям)
Этот подход разделяет код по уровням ответственности:
🔹 Controller (контроллеры) – обработка HTTP-запросов
🔹 Service (сервисы) – бизнес-логика
🔹 Repository (репозитории) – работа с БД
🔹 Entity (сущности) – модели данных

Package by Layer (Пакетизация по архитектурным слоям)

Минус этих подходов в том, что логика одной фичи разбросана по разным слоям. При добавлении новой фичи приходится вносить изменения в несколько слоев.
Package by Feature (Пакетизация по фичам)
Здесь код группируется по фичам (сценариям работы), а не по слоям.

DDD и организация кода
На своем опыте я сталкивался с несколькими подходами. Все подходы что я покажу технически работоспособны и потдерживают тактические патерны. На практике часто используется гибридный подход, который сочетает лучшие стороны Package by Feature и Package by Layer.
Подход №1. Доменно-ориентированные слои
Этот подход предполагает выдиления по контекстам на верхнем уровне. Если контекст большой, в нем могут быть компоненты, а внутри компонентов — разделение на слои. Я бы советовал избегать глубокой вложенности компонентов. Если такая ситуация возникает, стоит пересмотреть границы и структуру компонентов.

С точки зрения DDD и Layer Architecture, более предпочтительным является такой подход и вот почему:
Ограниченные контексты (Bounded Contexts)
Он лучше отражает концепцию ограниченных контекстов в DDD
Каждая функциональная область (order, bonus) представляет собой отдельный ограниченный контекст
Это обеспечивает лучшую изоляцию бизнес-логики и уменьшает связанность между разными доменами
Модульность и масштабируемость
При первом подходе легче добавлять новые функциональные модули
Каждый модуль содержит все необходимые слои и может развиваться независимо
Проще работать нескольким командам над разными модулями
Поддержка и навигация
Легче находить весь связанный код конкретной функциональности
Проще понять границы каждого модуля
Меньше риск случайного смешивания кода разных доменов
Соблюдение принципов DDD
Лучше отражает стратегический дизайн DDD
Четче видны границы агрегатов и доменных сервисов
Проще контролировать зависимости между модулями
Подход №2. Слои с контекстами внутри домена
Этот подход предполагает, что доменные модели разделены по контекстам, но остальные слои могут быть общими для всего приложения.

Плюсы:
Меньше дублирования кода в infrastructure или presentation, если эти слои действительно общие.
Может быть проще для небольшого проекта или монолита с единой точкой входа.
Минусы:
Общие слои application и infrastructure могут привести к нежелательным зависимостям между bounded context’ами. Например, изменение в инфраструктуре для Bonus может повлиять на Analytical.
Общий presentation слой может затруднить разделение UI по контекстам, если это потребуется в будущем.
Нарушение принципа автономности bounded contexts, так как общие слои увеличивают связность системы.
Если слои application и infrastructure не имеют подкаталогов для каждого контекста (а это не указано), их код может стать смешанным, что усложнит поддержку.
По мере роста проекта становится труднее навигировать по коду. Появляеться выше риск нарушения границ между доменами
Подход №3. Слои с поддоменами внутри домена
Этот подход предполагает, что доменные модели разделены по поддоменам, но остальные слои могут быть общими для всего приложения.

В DDD поддомены (subdomains) — это логические части основного домена (domain), которые могут быть выделены для упрощения моделирования. Однако в рамках одного bounded context поддомены обычно не требуют строгого физического разделения на уровне кода, если между ними нет явных границ контекста (context boundaries). Это решения остаеться на разработчике.
Плюсы:
Компактность: Для небольшого микросервиса с одним bounded context такая структура проста и понятна. Все слои находятся на одном уровне, что соответствует классическому подходу DDD (слоистая архитектура: presentation → application → domain → infrastructure).
Поддомены в domain: Разделение на order и bonus внутри domain удобно для логической группировки сущностей и бизнес-логики. Это особенно полезно, если поддомены имеют разные бизнес-правила или агрегаты, но всё ещё работают в рамках единого контекста.
Подходит для микросервиса: Если микросервис маленький, нет необходимости усложнять структуру излишней вложенностью или разделением на модули. Один bounded context = один микросервис — это типичный подход в DDD при переходе к микросервисной архитектуре.
Минусы:
Смешение поддоменов: Если order и bonus имеют пересекающиеся сущности или зависимости, их физическое разделение внутри domain может быть избыточным. В небольшом проекте можно было бы обойтись без подкаталогов, просто разделяя код логически (например, через имена классов или пакеты внутри единого domain).
Ограниченная масштабируемость: Если в будущем order или bonus вырастут в отдельные bounded contexts (например, потребуется разделить их на разные микросервисы), текущая структура потребует реорганизации. В таком случае первый вариант из твоего предыдущего вопроса (с отдельными модулями для каждого контекста) был бы более гибким.
Неоднозначность слоя presentation: Если presentation — это просто точка входа (например, REST API), то всё в порядке. Но если он начинает содержать бизнес-логику (что иногда случается в небольших проектах), это нарушает принцип разделения слоев.
Советы по организации DDD-проекта
В DDD ключевое значение имеет автономность bounded contexts. Первый вариант, где каждый контекст имеет свои собственные слои, лучше поддерживает этот принцип.
Начните с выделения ключевых доменов.
Определите границы контекстов.
Создайте базовую структуру папок.
Поместите общий код в отдельный модуль shared/common.
Используйте чистую архитектуру внутри каждого домена.
Определите четкие правила взаимодействия между доменами.
Примеры
https://github.com/CodelyTV/php-ddd-example
https://github.com/salletti/symfony-ddd-example
Выводы
Оба подхода к организации кода имеют свои преимущества и недостатки. Выбор между package by feature и package by layer зависит от конкретных требований проекта и предпочтений команды разработчиков.
В целом, package by feature кажется более подходящим для современных приложений, так как он лучше сочетается с принципами ФП и ООП, способствует созданию более модульного и понятного кода, а также облегчает разработку и поддержку приложения. Особенно хорошо этот подход подходит для проектов, использующих DDD.
Однако, важно отметить, что package by layer также может быть эффективным в определенных ситуациях, особенно в проектах с четко выраженной слоистой архитектурой и строгими требованиями к разделению ответственности между слоями.
Вне зависимости от выбранного подхода, важно придерживаться единого стиля кодирования и обеспечивать хорошую документированность кода, чтобы облегчить его понимание и поддержку.