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

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

Так, по мере необходимости, можно будет выделять компоненты. Обозначим некоторые особенности компонента. Компонет может зависеть от другого компонента, но этот другой компонент не может зависить от него, и циклической зависимости через другие компоненты не должно быть. Иначе это все будет один цельный компонент, разделенный на модули. Компонент мы можем переиспользовать или заменить на подобный компонент с минимальными усилиями. Внутри можно делать его, как удобно разработчикам, главное, чтобы было понятно как им пользоваться. Можно разделить его на слои. Можно не делить или делить не полностью. Можно выделить какой-то слой в отдельный компонент. Необязательно давать слоям конкретные имена слоев, это могут быть просто разные классы, папки. Не забываем, слои верхнего уровня не зависят от слоев нижнего уровня. Если так происходит, применяем инверсию зависимости или делаем адаптер. Нижний уровень находится наиболее близко к I/O — это репозитории, стороннее API, шаблоны Twig. Далее идут контроллеры и наши сервисы с логикой. Делаем по-простому, усложняя по мере необходимости. Либо сразу делаем сложно, если знаем, что это может понадобиться. Про SOLID не забываем, но тоже применяем по необходимости.

Для удобства можно оставить общие папки с Entity, Repository, Controller, как в Symfony сделано по умолчанию, чтобы не прописывать каждый раз к ним пути в конфиге, либо нужно автоматизировать этот процесс. Yaml конфиги, шаблоны, переводы так же остаются по умолчанию. Остальной код, код модулей можно поместить по традиции в папку Service. Внутри папок можно дублировать названия модулей. В дальнейшем так проще будет выделить переиспользуемый компонент, бандл, если понадобится.

Entity отображают таблицы, можно работать с ними, как с простыми структурами, DTO. Не стоит путать с концепцией Entity из слоистой, гексагональной архитектуры, DDD. Это просто данные, минимум логики. Так мы не привязываем структуру наших классов с логикой к структуре таблиц БД, можем пользоваться DI контейнером, и при необходимости через интерфейсы или DTO можно будет довольно легко отделить логику от конкретной сущности. Если код сущности сильно разрастается, можно делить его на трейты PHP или делать связи один к одному.

Всю логику пишем в сервисах, называем сервис по его назначению. Например, если логика относится к Product, кладем ее в ProductService. Если сервис будет расти, мы сможем выделить из него, например, ProductStoresService. Соответственно, лучше сразу писать логику и учитывать, что она, возможно, будет отделена. И лучше, конечно, сразу правильно ее разделить, чтобы в дальнейшем меньше времени потратить на рефакторинг.

Если ProductStoresService лежит в папке Product, то мы можем сократить название до StoresService. А можно переименовать, к примеру, в InventoryControl (складской учет). Можно выделить его отдельно. В общем, вариантов много. Название ProductStoresService универсальное, говорит нам, что алгоритм сервиса обрабатывает данные складов по отношению к товару. Так же может быть создан StoreService, в котором содержится логика для одного склада, независимая от товара, если она имеется, и так далее.

А если будет происходить взаимодействие складов не только с товарами, а с чем-то еще, например, с пользователями, заведующими складами, то может появиться другой StoresService, в другой папке — UserStores. А UserStores будет зависеть от User и Product. Но если мы не хотим, чтобы UserStore зависел от целого Product, придется выделять из Product отдельно Store. Но тогда Product начнет зависеть от Store, а товар вполне себе может существовать без склада. То есть нам не нужна зависимость Product от Store. И чтобы убрать ее, нужно сделать отдельно еще ProductStores, который их связывает вместе. А товар без склада у нас не имеет смысла, нам больше нечего хранить на складе, кроме товаров. Поэтому мы можем включить Store внутрь ProductStores. И так, у нас остаются: Product, ProductStores, User, UserStores. UserStore зависит от User и ProductStores, а ProductStores — от Product. Теперь UserStores зависит от Product не напрямую, а косвенно через ProductStores. Конечно, на практике вряд ли нам может понадобиться UserStores без Product, поэтому ProductStores можно включить в Product. А User может существовать и без Store. И если мы включим UserStores в User, то User будет тянуть за собой Product, что довольно странно. В итоге оставляем: Product, User, UserStore.

Repository можно оставлять пустыми и использовать их в наших репозиториях, которые мы будем создавать в модулях, расположенных в Service. Эти репозитории — простые сервисы, которые используют Doctrine Repository. Репозитории нужны, чтобы абстрагироваться от хранилища. Так же для каждого модуля можно создавать PHP конфиги в виде сервисов и не использовать YAML.

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

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


  1. Audiophile
    25.09.2021 21:46
    +4

    Выглядит фрагментарно. Графики бы добавить, диаграм.


    1. maxkain Автор
      26.09.2021 02:34

      Довольно сложно делать здесь графики, есть то, что есть )


  1. HEKET313
    25.09.2021 22:10
    +11

    Статья больше похожа на поток мыслей по архитектуре приложений.

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

    2. Не совсем понятно для кого эта статья: если это туториал для новичков - то для новичков лучше не писать "лучше делать по SOLID, но применять его с умом". Новичок не знает, что значит "с умом", у него опыта не достаточно понять, когда SOLID необходим, а когда нет. Для новичка должно быть догмой - пишем по SOLID и точка. Если же это для опытных разработчиков статья, то ценной информации как-то маловато. Каждый синьор уже закалён в холиварах по поводу нейминга и применения паттернов и прекрасно разбирается в этих вопросах.

    3. И вопрос ещё, а какая цель статьи в итоге? Из содержания не понятно: мы рассматриваем какой-то частный случай построения архитектуры приложения по конкретной методологии? Или хотели донести, как избежать каких-то проблем с расширением приложения в будущем? Ни один из вопросов не покрывается полностью.

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


    1. maxkain Автор
      26.09.2021 02:14
      +1

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

      Для новичка должно быть догмой - пишем по SOLID и точка

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


  1. Bonio
    25.09.2021 23:40
    +1

    Такой подход называется package by feature, действительно удобный способ организации кода, когда кодовая база начинает превышать некоторый объем.


    Для удобства можно оставить общие папки с Entity, Repository, Controller, как в Symfony сделано по умолчанию

    Тогда теряется смысл идеи, их тоже лучше хранить внути папки с модулем, чтобы вообще весь код, относящийся к данной конкретной фиче лежал в одной папке, а не размазывался разными файлами по проекту. В корневой папке модуля в таком случае нужно создать конфиг в формате php для кофигурации контейнера и routes (можно и yaml, но будет неудобно рефакторить) и настроить их импорт в Kernel.php.


    1. maxkain Автор
      26.09.2021 02:28

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


      1. toratoda
        26.09.2021 08:05
        +2

        Из пояснений не ясно какого размера у вас модули, есть какой либо критерий по объёму когда один модуль следует разбить на два?

        Я использую схожую идею разделения на модули, но в проектах на laravel. Для себя выделил следующие правила:

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

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


        1. maxkain Автор
          26.09.2021 13:00

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

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

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


  1. rpsv
    27.09.2021 07:43
    +2

    Делаем по-простому, усложняя по мере необходимости. Либо сразу делаем сложно, если знаем, что это может понадобиться.

    В целом плохая идея делать "сложно", чем проще система тем лучше. А делать что-то на будущее бред, поэтому что это будущее может не наступить (и скорее всего не наступит). Поэтому нужно делать ровно то что надо, но оставлять возможно для расширения.

    Про SOLID не забываем, но тоже применяем по необходимости.

    Что простите? Дак не забывать или делать? И что такое "по необходимости", когда это оправдано делать god-object "семирукицсемисис" который делает все что угодно? Или наследовать объекты с разным поведением только лишь для переиспользования кода?

    Entity отображают таблицы, ... Это просто данные, минимум логики.

    А в чем плюс анемичных моделей в сравнении с моделями домена?

    Так мы не привязываем структуру наших классов с логикой к структуре таблиц БД

    Именно так и вы привязываете структуру ваших классов к структуре БД, Entity же отражают структуру таблиц из БД из предложения выше.

    Например, если логика относится к Product, кладем ее в ProductService. Если сервис будет расти, мы сможем выделить из него, например, ProductStoresService

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

    А если будет происходить взаимодействие складов не только с товарами, а с чем-то еще, например, с пользователями, заведующими складами, то может появиться другой StoresService, в другой папке — UserStores

    Эээ а зачем? Stores же в любом случае работает с Product и он является его частью. Или у вас склады могут существовать без товаров? Если у вас склад это независимая ни от чего сущность (т.е. может существовать сам собой без User и Product), то выделяйте его в отдельный модуль, иначе (даже по мере роста функционала), где изначально находился сервис там его и оставляется и расширяйте.

    Если код небольшой, можно писать его прямо в контроллере. Но можно и в отдельном сервисе или нескольких сервисах

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

    P.S. статье на хватает хотя бы листингов папок, чтобы наглядно видеть о чем речь.

    P.S.S. есть подозрение, что вы либо не докрутили, либо целенаправленно не используете старые добрые слои: domain - data - service - infrastructure , и у вас получается кашка из-за этого. Ну и в целом у вас получается этакий DDD (только нет Domain Model (а зря) и разделения на Entity/Aggregate (хотя в целом можно и без него пережить) миксом с CQS (где UseCases это Command).


    1. maxkain Автор
      28.09.2021 00:02

      В целом плохая идея делать "сложно", чем проще система тем лучше. А делать что-то на будущее бред, поэтому что это будущее может не наступить (и скорее всего не наступит). Поэтому нужно делать ровно то что надо, но оставлять возможно для расширения.

      Собственно об этом и речь. Усложняем, если есть в этом необходимость.

      Что простите? Дак не забывать или делать? И что такое "по необходимости", когда это оправдано делать god-object "семирукицсемисис" который делает все что угодно? Или наследовать объекты с разным поведением только лишь для переиспользования кода?

      Об этом речи не было. Про SOLID я ответил в другом комментарии: "Лучше понимать, что ты делаешь и для чего. Это не универсальные правила, которые всегда подходят, и начинающего могут сбить с толка". Да и вообще, универсальных правил, где учитываются все возможные случаи, не существует, наверное.

      А в чем плюс анемичных моделей в сравнении с моделями домена?

      Название "анемичная модель" считаю некорректым, так как Entity - это скорее DTO, которое вполне описывает БД, и не соответствует значению слова "анемичность". А преимущества, были упомянуты: "Так мы не привязываем структуру наших классов с логикой к структуре таблиц БД, можем пользоваться DI контейнером, и при необходимости через интерфейсы или DTO можно будет довольно легко отделить логику от конкретной сущности."

      Именно так и вы привязываете структуру ваших классов к структуре БД, Entity же отражают структуру таблиц из БД из предложения выше.

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

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

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

      Эээ а зачем? Stores же в любом случае работает с Product и он является его частью. Или у вас склады могут существовать без товаров? Если у вас склад это независимая ни от чего сущность (т.е. может существовать сам собой без User и Product), то выделяйте его в отдельный модуль, иначе (даже по мере роста функционала), где изначально находился сервис там его и оставляется и расширяйте.

      Собственно, это и объясняется в том абзаце, только более подробно.

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

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

      P.S.S. есть подозрение, что вы либо не докрутили, либо целенаправленно не используете старые добрые слои: domain - data - service - infrastructure, и у вас получается кашка из-за этого. Ну и в целом у вас получается этакий DDD (только нет Domain Model (а зря) и разделения на Entity/Aggregate (хотя в целом можно и без него пережить) миксом с CQS (где UseCases это Command)

      Названия слоев довольно условны, их может быть больше или меньше, а может и не быть, если модуль небольшой, например. На слои можно делить все приложение и отдельные модули. В основном, информацию по теме архитектуры я получал из книги Роберта Мартина "Чистая архитектура" (оттуда SOLID, UseCase, компоненты, слои), статей в интернете и собственных размышлений, опыта. Позже читал и книги про DDD, но практически ничего полезного там не нашел для себя.


  1. rpsv
    28.09.2021 19:16

    Название "анемичная модель" считаю некорректым, так как Entity - это скорее DTO, которое вполне описывает БД, и не соответствует значению слова "анемичность"

    Выдержка из статьи: "Это просто данные, минимум логики". Собственно это и есть анемичная модель: сущности где куча гетеров и сеттеров без какой-либо логики.

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

    Дак они у вас и сейчас находятся там (правда инфраструктурный слой выше слоя приложения, слой данных вы наверное имели ввиду?), и очень жестко (1 в 1) завязаны на структуре БД и очень вряд ли отражают объекты с точки зрения предметной области.

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

    Мы все еще говорим про НЕ анемичную модель, ага.

    Собственно, это и объясняется в том абзаце, только более подробно.

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

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

    Если уж дальше разруливать эту ситуацию, то по хорошему должны быть контексты (в рамках статьи наверное модули):

    1. товар, склад и заведующий склада (который никакого отношения не имеет к пользователю, имеет только ид, который в рамках приложения совпадает с ид пользователя)

    2. пользователь который вообще никак не сопрекасается со складом и товарам

    Подробнее посмотрите про ограниченный контекст.

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

    Не нужно строить иллюзий, "хороший код, там где не нужно" === "говнокод" :)

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

    Вот прям обидно стало за ребят, которые все эти слои "кровью и потом" и своим опытом выводили. А оказывается они условны.

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


    1. maxkain Автор
      29.09.2021 23:25

      Дак они у вас и сейчас находятся там (правда инфраструктурный слой выше слоя приложения, слой данных вы наверное имели ввиду?), и очень жестко (1 в 1) завязаны на структуре БД и очень вряд ли отражают объекты с точки зрения предметной области.

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

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

      Есть Product, содержит в себе Store, есть User, и есть модуль UserStore, отвечающий за взаимодействие User c Store. Так же можно его назвать UserProductStore, если могут быть еще какие-то Store не связанные c Product.

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

      Получается, что склад без товара не может существовать. Лучше даже назвать модуль не UserStore, а как-нибудь StoreUser или StoreDirector. Название модуля отражает его функционал и необязательно должно говорить с какими ентити он работает.

      Если уж дальше разруливать эту ситуацию, то по хорошему должны быть контексты (в рамках статьи наверное модули):

      1. товар, склад и заведующий склада (который никакого отношения не имеет к пользователю, имеет только ид, который в рамках приложения совпадает с ид пользователя)

      2. пользователь который вообще никак не сопрекасается со складом и товарам

      1. Контексты находятся в модулях и отделены от ентити через интерфейсы, например. И эти модули могут работать независимо от струтуры БД, ентити. То есть, данные о заведующем можно поместить в User ентити, а можно сделать отдельную. Главное, чтобы она реализовывала StoreDirectorInterface из модуля StoreDirector. И все, модуль ничего не знает о конкретной ентити, работает с ней через интерфейс. Так можно проектировать любую структуру таблиц, независимо от наших модулей. Модуль User так же может ничего не знать об ентити User, он может работать с ней через UserInterface. Но конечно, вы можете отнести ентити User к модулю User, но тогда, модуль StoreDirector уже должен взаимоействовать с целым модулем User, а не только c ентити.

      2. А модуль пользователя и не соприкасается никак со складами и товарами.