Упакуйте свой код правильно

от автора


В предыдущей статье «Как организовать структуру приложения» обсуждались различные подходы к организации кода, включая монолитную архитектуру, многослойную архитектуру и принципы чистой архитектуры. В ней я  акцентировал внимание на важности понятной структуры кода, которая облегчает понимание, внедрение новых функций и снижает вероятность ошибок. Особое внимание уделялось критериям понятности, таким как чёткое назначение файлов и папок, разделение логики по модулям или функциональным зонам, использование самодокументируемых названий и соблюдение стандартов кодирования. Также были рассмотрены проблемы, возникающие при неструктурированной организации кода, такие как сложность навигации, отсутствие модульности и нарушение принципов 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.

🔗 Обектно ориентированое програмирования (ООП) и организация кода

ООП — это парадигма, в которой программа строится вокруг объектов, которые представляют собой экземпляры классов. Основные принципы ООП включают:

  1. Инкапсуляция: Сокрытие внутреннего состояния объекта и предоставление доступа к нему только через методы.

  2. Наследование: Возможность создания новых классов на основе существующих, что позволяет повторно использовать код.

  3. Полиморфизм: Возможность объектов разных классов обрабатываться как объекты одного класса.

  4. Абстракция: Упрощение сложных систем путем моделирования классов, которые отражают только существенные характеристики.

    Package by Feature

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

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

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

    • Легче соблюдать принцип единственной ответственности (SRP), так как каждый пакет отвечает за конкретную функциональность.

    • Лучше сочетается с DDD (Domain-Driven Design), так как позволяет группировать объекты, связанные с одной бизнес-фичей, в одном месте.

    Package by Layer

    • Package by Layer в ООП часто встречается в классических монолитах, где слои (контроллеры, сервисы, репозитории) разделены, и каждый слой отвечает за свою часть логики.

    • При package by layer объекты и классы, отвечающие за разные аспекты одной функции, также оказываются разделенными по разным слоям. Это может затруднить применение принципов ООП, таких как полиморфизм, и привести к созданию более сложных и запутанных связей между объектами.

🔗 Функциональное программирование (ФП) и организация кода

ФП — это парадигма, в которой программа рассматривается как набор функций, которые принимают входные данные и возвращают результаты. Основывается на идее чистых функций, неизменяемости данных и композиции. Основные принципы ФП включают:

  1. Чистые функции: Функции, которые не имеют побочных эффектов и возвращают одинаковый результат для одних и тех же входных данных.

  2. Неизменяемость: Данные не изменяются после создания, вместо этого создаются новые данные.

  3. Функции высшего порядка: Функции, которые могут принимать другие функции в качестве аргументов или возвращать их.

  4. Рекурсия: Часто используется вместо циклов для обработки данных.

Функциональное программирование основывается на идее чистых функций, неизменяемости данных и композиции. В ФП акцент делается на том, что делает программа, а не на том, как она это делает. Это ведёт к иному подходу к структуре кода:

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. Слои с доменными компонентами

Этот подход имеет следующие недостатки:

  • Смешивание разных доменных моделей в одном каталоге

  • Сложнее контролировать зависимости между доменами

  • По мере роста проекта становится труднее навигировать по коду

  • Выше риск нарушения границ между доменами

Советы по организации DDD-проекта:

  1. Начните с выделения ключевых доменов.

  2. Определите границы контекстов.

  3. Создайте базовую структуру папок.

  4. Поместите общий код в отдельный модуль shared/common.

  5. Используйте чистую архитектуру внутри каждого домена.

  6. Определите четкие правила взаимодействия между доменами.


Выводы

Оба подхода к организации кода имеют свои преимущества и недостатки. Выбор между package by feature и package by layer зависит от конкретных требований проекта и предпочтений команды разработчиков.

В целом, package by feature кажется более подходящим для современных приложений, так как он лучше сочетается с принципами ФП и ООП, способствует созданию более модульного и понятного кода, а также облегчает разработку и поддержку приложения. Особенно хорошо этот подход подходит для проектов, использующих DDD.

Однако, важно отметить, что package by layer также может быть эффективным в определенных ситуациях, особенно в проектах с четко выраженной слоистой архитектурой и строгими требованиями к разделению ответственности между слоями.

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

Источники


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


Комментарии

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

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