Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

Про 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).

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

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

Что простите? Дак не забывать или делать? И что такое "по необходимости", когда это оправдано делать 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, но практически ничего полезного там не нашел для себя.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Дак они у вас и сейчас находятся там (правда инфраструктурный слой выше слоя приложения, слой данных вы наверное имели ввиду?), и очень жестко (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. А модуль пользователя и не соприкасается никак со складами и товарами.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации