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

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

Какая-то сумбурщина полная чес слово

Так и статья не конструктивна) А комментарий зато лаконичный, и спасает от траты времени.

Это почему статья не конструктивна?

Если вы не понимаете написанного, видимо, вам оно не нужно.

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

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

Когда речь заходит за архитектуру любого ПО лучше прибегать к средствам визуального моделирования UML, как минимум диаграмма классов, круто когда есть диаграмма последовательностей. Это не делает красивее статью, это делает ее понятнее, если посмотреть на статьи по паттернам там везде прилагают схемы.

Пока остановился на таком формате, со схемами получится всё гораздо объемнее.

Конечно, это скорее рекомендательный характер, в любом случае спасибо)

Имхо. Начиная с примеров из середины статьи — архитектура становится всё менее чистой.

Складывая всё в App... раскидывая по папочкам типа "сервисы в Service", "энтити в Entity" и тому подобное, вы начинаете увеличивать вес "чистых" спаггети. Когда сущностей в проекте увеличится до значительных размеров — сидеть с такой "красотой" станет невыносимо грустно.

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

...
"autoload": {
    "psr-4": {
      "App\\": "app/",
      "YourCompaty\\SomeContextA\\": "domain/SomeContextA/",
      "YourCompaty\\SomeContextB\\": "domain/SomeContextB/"
    }
  },
...

, где в директории domain/ContextName вы уже можете хранить ту же структуру с "сервисы в Service", "энтити в Entity", которая не будет пересекаться с соседними контекстами. Что позже, при необходимости, поможет вам в вычленении "контекста" в отдельными микросервис.

Плюс можно добавить Modulite и/или Deeptrac и все будет еще лучше и чище.

Честно сказать, из текста статьи вообще не понятно ничего. Ни одного примера нету, где мы нарушили SOLID и как мы переписали код, таким образом чтобы соблюдать SOLID. Мало примеров кода и картинок. Советы типа делать UserOrderCheckoutService, приводят к тому что появляется куча однотипных названий сервисов, и потом никто не знает какой из них использовать в той или иной ситуации.

Например что делать, если наши юзеры разбиваются еще на группы, у этих групп есть разные настройки и разные права. Получаться разработчики захотят создать UserSallerCheckoutService, UserOptovikWhithoutRuleReservCheckoutService и т.д.

Примеры есть как соблюдать SOLID есть.

Если вы делаете сервисы и не знаете как их потом использовать, тут даже не знаю как помочь, может быть стоит давать понятные имена, разъяснять из назначения. Но уменьшение количества классов эту проблему не решает. Такая же проблема будет и с функциями.

Я упоминал, что можно разделять код на классы с различными названиями.

Ни одного примера нету, где мы нарушили SOLID и как мы переписали код, таким образом чтобы соблюдать SOLID

Это все конечно интересно. Но меня больше интересует вопрос - а зачем вообще соблюдать SOLID? На это есть какойото адекватный практический ответ, а не "потому что так правильно" ?

Множество литературы об этом есть. Думаю, вам никто не будет здесь её пересказывать.

"потому что так в книжке написано", ага. Не удивляйтесь что с вас смеются в коментах

Эти вещи актуальны для всех видов проектов, особенно чаще начинаешь вспоминать про эти принципы когда пытаешься написать юнит тесты, там ошибки проектирования всплывают моментально и становиться больно смотреть на код. Например начинаешь понимать что лучше использовать DI потому что так легче мокать какие то объекты, самое банальное это когда пишется какая то интеграция с внешним апи, и используется Guzzle, и вот в тестах становиться вопрос особенно остро, как засунуть в свой сервис объект Guzzle, так что бы были заранее замоканы ответы в нем, да еще во всех возможных вариантах, вот тут и начинает большую роль играть SOLID. Можно и без тестов да, но если у вас есть какая то интеграция с платежными системами то лучше что бы тесты были.

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

замоканы ответы в нем

Интересный вопрос, что вы вообще тестируете, если часть логики у вас моки, а не реальный код

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

Чем отличается domain/ContextName от App\Service\ContextName?

Тем что у вас в одном случае явное ограничение скоупа, а во втором просто какие-то файлы в общей директории/нэймспэйсе Service

use Domain\Order\{
	Service,
	Repository,
	Entity,
	Exception
}

Которые могут жить в чем-то типа:

domain\
      \src\
          \Service
          \Repository
          \Entity
          \Exception
      \tests\
            \Service
            \Repository
            \Entity
            \Exception

Вы просто открыли директорию с контекстом бизнесовой логики, и далеко ходить никуда не надо. Всё кучно, всё понятно. Очень легко и не принужденно приходит понимание что "модель из контекста А" не протечет в "контексте Б" просто потому что она лежала рядом +/- директория.

Очень грубый пример:

Обед\
    \морковь\
    	\суп\
    	\гуляш\
    \кортофель\
    	\суп\
    	\гуляш\
    \соль\
    	\суп\
    	\гуляш\
    \перец\
    	\суп\
    	\гуляш\
    \вода\
    	\суп\
    	\гуляш\

и/или

Обед\
	\суп\
		\морковь\
		\кортофель\
		\соль\
		\перец\
		\вода\
    \гуляш\
    	\морковь\
    	\кортофель\
    	\соль\
    	\перец\
    	\вода\

Я сейчас играюсь с ларавелькой, где такой подход вывел в "локальные" композер пакеты, которые регистрируются автоматически. То есть это не внешняя (пока не внешняя) зависимость которая ставится по сети, а директория с оформлена в виде пакет, в которой не только бизнесовые сущности ограничены, но и внешние зависимости тянутся под контекст задачи. Получилось красиво. При этом вытащил абстракции в свой отдельный пакет, от которого наследую финальные реализации

Что значит явное ограничение скоупа? Код, лежащий в Domain\Order идентичен коду из App\Service\Order. Это дело вкуса, но второй вариант более стандартный, если можно так выразиться.

Логика проста, причина такого подхода это изолироваться от немспейса самого фреймворка, и это стремление отгородить именно "бизнес логику", по сути можно и в App все складывать, а можно просто создать директорию и дописать одну строчку в композере и будет отдельный неймспейс под бизнес логику. Одна из причин почему я так когда то сделал, я хотел избежать протечек фреймворка в бизнес логику, просто что бы не один одна строчка с App/... не попала в логику, правда вот до доменов все никак не доберусь но идея в примере классная, один из плюсов такого подхода, если надо будет как то отделить код от проекта то как минимум меньше времени уйдет, не будет зависимостей у других доменов, так и у самого домена.

scope — the extent of the area or subject matter that something deals with or to which it is relevant.

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

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

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

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

Так и надо в отдельный неймспейс, а в идеале завернуть в пакет. Вон на C# вообще делают отдельную компилируемую библиотеку динамическую.

То, что второй вариант (класть все в App/Service) более стандартный (наверное в силу того, что так в симфони гайдах написано) не делает его более правильным.

Я не раз встречался с проектами, которые начинали свою структуру именно по такому шаблону... Как итог, с этим становится невыносимо грустно работать: в директории Entity дохреналиард файлов с сущностями и вложенных директорий с ними же, аналогичная история с Service, DTO, Repository и др. В итоге все файлы компонентов перемешаны, нет чёткого понимания где определённый класс может быть заюзан (буквально через use), а где нет. А уж в плане навигации по коду проекта так совсем не весело скролить по несколько экранов вверх-вниз (ведь раскрывая директорию выпадает куча файлов). Все эти лишние сложности исчезают, когда логически взаимосвязанный код лежит рядом, в одном корневой неймспейсе (домене, модуле, пакете).

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

DDD в помощь, но без фанатизма, во всем важна золотая середина.

Положить можно куда угодно, я для примера взял App\Service. Да, в основном код можно класть рядом, туда же. Но, например, удобно видеть все точки входа в приложение, открыв App\Controller. С EventListener систуация неоднозначная, думаю, можно и рядом с сервисом хранить. Если же entity хранить рядом с сервисом, придётся прописывать их в конфиге для каждого модуля, что не очень удобно, да и maker создает классы в App\Entity. Можно, конечно, с этим что-то придумать, но это уже отдельная тема. Да и вопрос, стоит ли? Если следовать правилу, что первый уровень в Entity, Repository и т.д. это название модуля, то проблемы быть не должно. То есть Entity\Module1, Repository\Module1 и т.д.

Можно рассматривать первый уровень в App, как некоторые адаптеры к фреймворка к логике приложения.

Еще, как вариант, делить приложение на бандлы, как делалось в старых версиях Symfony. В общем-то неплохой вариант, но добавляются некоторые издержки на поддержку бандлов. Но, конечно, не такие, как на поддержку микросервисов. Бандл, если нужно, можно всегда доработать до микросервиса. Да и микросервис то к чему? Можно же запустить два инстанса приложения на разных серверах. Что даст микросервис, сэконмит несколько мегабайт оперативной памяти? Делать отдельные сервисы есть смысл в довольно редких случаях.

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

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

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

Директория Services является вырожденной (Так же как и Entity), т.к. не соответствует никакой задаче. Это плохой пример и явно не соответствует понятию "чистая архитектура", но допустимо в рамках небольшого ПО.


Можно взять себе за правило, что любой кейс, который содержит кейворды Service, Util, Manager, Support, etc почти всегда (как и в вашем случае) является некорректным с точки зрения нейминга и построения структуры ПО.

В книге "Чистая архитектура", как раз и приводится один из примеров, где в Service хранится бизнес-логика. Директория не всегда должна соответствовать какой-то конкретной задаче, не выдумывайте.

Да мало ли что там приводится? Это тривиальная логика: Нейминг "Service" является мусорным и не несёт никакого смысла. Зачем оно нужно? Удалить его и ничего не изменится вообще.


Репозиторий — тоже является сервисом предоставления данных из внешнего источника. Энтити (если она анемичная и тонкая, как у вас в примерах) — тоже является сервисом репрезентации записи в БД. И т.д. Почему всё тогда не складывать в директорию "Service", если это тоже сервисы?


И да, директория (в т.ч. неймспейс) должен соответствовать не задаче, а области ответственности. Что изменится, если вместо примитивной структуры "для новичков":


App\
    Entity\
        Order.php
    Service\
        OrderCheckoutService.php

Будет более адекватная и очевидная?


App\
    Domain\
        Order\
            Order.php
            CheckoutProcess.php

Если считаете что это "не удобно", то:


1) Куда будете складывать ошибки, возникшие в процессе оформления заказа? Про второй случай всё очевидно, в заказах (с вариациями) Order\***\NotEnoughMoneyException. А в первом?


2) А где будет валидатор, который проверяет состояние заказа?


3) А как написать архитектурные тесты (например через https://github.com/carlosas/phpat), которые будут проверять, что "заказ" не имеет доступа и не обращается ни к кому, кроме соседних "сервисов" в том же неймспейсе?

Можно и в Service все складывать, большой разницы нет на самом деле. Но тогда действительно эта директория будет лишней. Я уже объяснял в комментарии выше https://habr.com/ru/articles/736574/#comment_25576836

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

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

Это какая то мешанина из классов, обсуждение куда положить OrderFactory вместо того чтобы объя“снить для чего он и зачем ему интерфейс.

Выбор между заказом зависящим от доставки или доставкой зависящей от заказа (в пределе у них собственные жизненные циклы связанные весьма условно)

Выше же написано: "Имеет смыл делать логику независимой от Entity и Repository". И дальше рассказывается как это сделать.

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

Какой смысл в фабрике под интерфейсом? если это место где происходит бизнес-логика(из статьи создаётся впечатление что бизнес логика где то еще), то не понятно зачем там интерфейс, т.к. или фабрика будет одна, или их будет несколько принципиально разных.

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

Хороший пример к чему на практике приводит следование чистой архитектуре, solid, gang of four прочему вредному мусору

С этой плашки отдельно кекнул:

Охлол, и это я еще теги не читал:

гексагональная архитектура, слоистая архитектура, предметно-ориентированное

Можете аргументировать почему это "вредный мусор"?

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

Чем больше кодовая база и сложнее проектируемая система, тем труднее её поддерживать, для этого и существуют архитектуры, SOLID'ы, GRASP'ы и другой на ваш взгляд "мусор".

Следование которым порой создаёт больше проблем, чем не следование.

Всё нужно делать с осознанием, принципы - это не правила, которым следует безукоризненно следовать.

Мусор или нет - зависит от ситуации.

Но текст ведь действительно сложный... для восприятия и понимания (зачем он).

Высшая математика тоже сложная для понимания, зачем она... (ирония). Все относительно, если для вас текст слишком сложный, вы можете его не читать, никто не заставляет.

Входные данные можно принимать через параметры функции, но часто удобнее передавать структуру с данными, DTO.

Которая всё ещё будет как параметр функции. Смысл понятен, но донесён не совсем удачно, имхо.

Могут понадобиться App\Service\Order\Checkout\InputDto, возможно, и InputDtoInterface.

Возможно? А зачем?

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

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

Зачем, кстати, выделяете все ключевые слова жирным, чтоб в поиске выпадать чаще?

 Можно вместо интерфейсов использовать и промежуточную структуру, DTO, но такой способ будет более объёмным в реализации и иметь некоторые другие минусы.

Можете раскрыть мысль? Как я вместо интерфейса могу DTO использовать? Какие некоторые другие минусы?

Таким образом, принцип единой ответственности из SOLID можно легко реализовать не привязываясь к структуре БД

Очень общая фраза, которая никак не следует из предыдущих рассуждений и непонятно при чём тут SRP.

Логика не привязана к конкретной сущности, её можно применить к любым другим данным — принцип инверсии зависимости

О какой конкретно логике речь?

Класс с реализацией можно легко подменить через сервис-контейнер — принцип открытости-закрытости

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

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

Пустые слова, ибо нет конкретных аргументов

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

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

Тогда класс App\Service\Delivery\DeliveryCalculator становится конкретной реализацией адаптера. К нему может добавиться ещё некоторая логика, тогда его уже можно назвать декоратором. А если он будет обращаться к нескольким классам, то это уже посредник

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

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

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

Можете раскрыть мысль? Как я вместо интерфейса могу DTO использовать? Какие некоторые другие минусы?

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

Очень общая фраза, которая никак не следует из предыдущих рассуждений и непонятно при чём тут SRP.

В сущности, в таблице БД могут быть разнородные данные. И логика их обработки разносится по разным классам - сервисам. В этом и применение SRP.

О какой конкретно логике речь?

О логике обработки данных сущности, о логике приложения, какой же еще?

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

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

Пустые слова, ибо нет конкретных аргументов

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

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

Да, может ходить вверх и вниз. Вниз ходит через инверсию зависимости, чтобы не зависеть от нижних уровней.

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

В программировании это уже устоявшееся слово, имеет отношение к проектированию.

Статья о том, как с помощью названия путей на диске без единой строчки кода объяснить "чистую" архитектуру. Автор просто увеличивает энтропию вселенной используя слова Энтити, Домен, Сервис...

Если вы откроете другую литературу по схожей теме, в том числе книгу "Чистая архитектура", то очень удивитесь, что там практически нет кода.

очень удивитесь, что там практически нет кода.

Как и стоящих мыслей

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

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

Публикации

Истории