Pull to refresh
1
0
Send message

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

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

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

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

А вот нет таких инвариантов

У вас нет, у других есть. Например не может быть баланс меньше кредитного лимита или 0, ну не может и всё. А если может, значит нет такого инварианта действительно

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

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

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

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

Почему не в сервис изменения баланса? Как вообще юзер может сам менять свой баланс? Это совершенно неправильная модель.


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

Ну как это не забудет, в сущности 1000 методов (потому что есть 1000 сценариев, а менять поля вне сущности мы не хотим),

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

Опять же DDD и агрегаты. Они в целом не могут существовать на анемиках. Если только не двойной маппинг, где есть отдельно сущности ОРМ и отдельно бизнес сущности.
Получается Фаулер не прав, DDD и Эванс не прав, Окрамиус не прав, Инкапсуляция не права, tell don't ask не право. Не верю )


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

"Отправить email после создания заказа" - мало похоже на бизнес логику, хоть и является требованием. Да это часть процесса, но это скорее инфраструктура. Зачастую ошибка тех, кто против этого подхода, что они думают, что ВСЯ кодовая база отправляется в сущности: отправка емейлов, очереди, регистрация, авторизация. Это не так. В сущности отправляется логика, которая может нарушить целостность системы и истинные инварианты, чтобы этого сделать было нельзя.

Является ли критической проблемой, что емейл будет не отослан? Наверное нет.

А является ли критической проблемой, что баланс юзера установился неправильно, из-за того, что человеком малопосвященным всем ньюансам системы, забыл перед этим вызвать какой то калькулятор или чекер, и просто засетил рандомный баланс? Видимо да. И решением этого будет инкапсуляция логики изменения баланса в сущность юзера $user->addMoney($money, $moneyChecker, $banking). И никто уже не забудет в флоу добавления баланса, проверить там что то и какие то операции вызвать.

Продублирую (давал в другом ответе уже) старый доклад на эту тему https://ocramius.github.io/doctrine-best-practices/#/32

Active Record про маппинг на таблицу и работу с таблицей через модель и это совсем про другое, нежели бизнес логика. При занесении логики в сущность как раз улучшается инкапсулция и тестируемость. Никто не сможет занести в сущность невалдиные для бизнес процессов данные. Вот старый доклад на эту тему https://ocramius.github.io/doctrine-best-practices/#/32

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

$data = $actionHandler->action($dto)
$user->setData($data); // Взяли да забыли вызвать хэндлер и просто составили невалидный $data

vs

$user->action($actionDto)
Ну или если есть зависимости или сложная для одной сущности логика, то можно какой то сервис и передать.
$user->action($actionDto, $actionServiceHelper)

На нейминг не смотреть)

Давайте проведем мысленный эксперимент:

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

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

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

А потом мы подумали, а почему авторизация это микросервис? Это дополнительные сетевые накладки. Мы втягиваем его в свой монолит отдельным модулем, а меняем только протокол общения с HTTP на локальный, просто подменив адаптеры/anticoruption layer. Но авторизация осталась таким же гексагоном, внутри нашего же проекта, но отдельным модулем.

И вот мы получили два гексагона (основной проект и авторизация) в одном проекте.

Далее мы дробим основной проект на подмодули - более мелкие гексагоны по такому же принципу как авторизация.

В чем я ошибаюсь?

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

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

Берутся и используются классы другого модуля напрямую. Кто может этому помешать? Модуль - это логическая единица.

Допустим мы разделили проект на модуль Orders (тут работает команда,которая работает с заказами), и модуль Users (Тут работает команда отвечающая за регистрацию и профили). Без каких либо привязок к языку. Просто папочки и классы.

И вот команде Orders понадобился email пользователя. И тут есть 2 способа (берем самые простые, естественно есть другие с дубликатом емейла):
1) Дергается репозиторий usersRepository.get(userId).getEmail. Команда Users об этом ничего не знает, потом они каким то образом меняют getEmail на getMail - все крашится.
2) Дергается публичный контракт, где UserDto. usersModule.query.get(userId). Команда отвечающая за юзеров сама его предоставила и знает что он публичный, отвечает за его поддержку. Но она отвечает только за поддержку контрактов, а не любого публичного метода любого класса.

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

Все это про инкапсуляцию, только на уровне модулей и связей подссистем.

Наше всё приложение - это не 1 гексагон. Наше приложение - это куча гексагончиков.

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

Основной поинт и смысл всего этого - это low coupling. Чтобы границы были четко определены, тем самым можно легко следить за изменениями и расширениями. Иначе кто то просто что-то заюзал из внутренней реализации, и как за этим можно следить? Да никак. А за четко определенными контрактами - можно.

Upd. Какая например разница, кто вызвал создание заказа cli, http, или другой модуль? Почему другой модуль не такая же абстракция как cli или http ?

Это можно сделать как отдельно в своих модулях, так и в модуле заказа.

Основное, что нельзя просто взять класс/функционал из другого модуля и работать с ней где угодно. Работать только по публичным контрактам модуля. В этом смысл гексагоналки.

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

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

Например у нас есть кейс оформить заказ и доставку.
И в луковой вы вполне спокойной делаете подряд в одном методе
order = ...
orderRepository.add(order)
delivery = ...
deliveryRepository.add(delivery)

Вроде все по слоям, слой БД, слой логики.

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

order = ...
orderRepository.add(order)
deliveryModule.create(deliveryCreateDto).

Поидее много разных архитектур говорят об одном и том же.
Например берем тот же DDD.
Слоистая архитектура - это разделение на Infrastructure, Domain, Application
А гексагональная - это разделение на контексты.

Слова вроде разные, а смысл в реализации почти один.

Все же это с разных углов. Когда мы говорим о многослойности, то говорим о том, что есть слой контроллеров, слой логики, слой БД. Еще какие ровные слои бутерброда.
Когда говорим о гексагональной архитектуре, то основное - это блоки, у которых есть порты входа и выхода. Интерфейсы (контракты), через которые идет общение с этими блоками и никак иначе.

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

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

Но никто не мешет использовать их обе, и это идеально.

Думаю можно еще так разделить:
Гексагональная архитектура больше про coupling и cohesion.
Слоистая, луковая - про инверсию зависимостей.

Подскажите, пожалуйста, а как у вас реализуется фабричный метод?
$filter = ArticleFilterFactory->active();

Или например:
$filter = ActiveFilter::create();

Не совсем уверен в своих мыслях, но мне кажется это работает так, другого объяснения я не вижу:
Результат:
$tmp_2 = POST_INC $a

не $tmp_2=2 $a=1, а, наоборот, $tmp_2=1 $a=2. Т.е. $tmp_2 — это результат $a++, а не состояние $a

Ведь:
$a = 1;
$tmp_2 = $a++;
var_dump($a, $tmp_2); // int(2), int(1) 


$c = $a + $a + $a++ = ($a + $a) + ($a++) =  (1+1) + (1) 
$c = $a + $a++ = $a + ($a++) = 2+(1)

Видимо оп коды это еще не совсем справа команда, слева результат без сайд эффектов.
Т.е. опкоды — это скорее результат разложения кода на более простые операции и последовательности операций, а не линейный «машинный код»

Не исключаю, что это не верный путь. Но, например, я заполняю базу данными и заполняю так, что для тестируемого запроса — результат сущность с ID =5. Конечно есть вариант проверять по другому полю. Но вроде как id — точно будет идентифицировать сущность.
Есть куда более практичный способ. Пакет dama/doctrine-test-bundle предоставляет удобный апи для оборачивания тестов в бд транзакцию. Которую после его выполнения откатывается. Просто, быстро, удобно. Чаще всего даже отдельная бд для тестов не нужна.

Но есть проблема в этом способе — транзвакции не откатывают счетчик автоинкремента. И если есть привязка теста к IDs — возникнет проблема.
Я бы даже осмелился предположить, что данная опция вообще не нужна при remote_connect_back = 1
Продление вроде тоже со скидкой.
Да, данную функцию нужно явно вызывать. Но в вашем случае надо явно прописывать: property className varible. Собственно по количеству символов ваш вариант не короче.
$var = $this->createMock(Class::class);
Против
@property Namespace\SubSpace\Class var 
$this->var;

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

Да, вы используете класс propertyBag, в котором у вас хранится тот же массив. Не нравится массив? Используйте коллекцию. Да и тайпхинтинга у вас тоже нет. Ведь все создается динамически.

Еще раз повторюсь, ничего не имею против, классная статья, интересная идея и архитектура. Я для себя даже кое что подчерпнул. Но на мой взгляд — излишнее усложнение того, что можно сделать проще. Просто профита не вижу, от написанной сложности.
Очень интересная статья и опыт. Спасибо. Но вот просмотрев основной код, можно выделить 2 основные задачи, которые ими решаются:
1) Короткое создание моков + удаление их в tearDown.
2) Парсинг докблока.
Могу ошибаться, но разве это не сложный велосипед? Для быстрого создания моков есть codeception/stub, а для парсинга докблока — phpDocumentor/ReflectionDocBlock (Но возможно не совсем подходит, конкретно под ваш кейс)

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

Беру теоретичсекий код, который должен работать, и все это заменить:

protected $mocks = [ ];
protected createMock($className, $constructorParams = [] ){
    //тут код создания мока
   $this->mocks[] = $mock;
    return $mock;
}

protected function tearDown()
    {
        $this->mocks = null; 
        parent::tearDown();
    }


Разве нет?
Еще добавлю, паттерн декоратор работает с композицией, а не с наследованием. При паттерне декоратор, можно в теории делать сколько угодно слоев декорирования, в этом его смысл. Например вы захотите сделать еще один декоратор, вы будете что декорировать наследованием? LogCreateUser или CreateUser?
Декораторы выглядят так:

SendEmailCreateUser(new StatCreateUser(new LogCreateUser(new CreateUser())))
1

Information

Rating
Does not participate
Registered
Activity