Комментарии 14
У такого сервиса будет хотя бы явно-определенный интерфейс с типизацией.
Зона ответственности класса так же весьма размыта. Судя по коду из базового трейта Интерактора автор уже как минимум предлагает прикручивать к реализации валидацию входящего запроса.
<?php
namespace Lib\Interactor;
trait Interactor
{
abstract function call();
public function __invoke()
{
$arguments = func_get_args();
$payload = [];
if (method_exists($this, 'validate') && !$this->validate(...$arguments))
{
return new InteractorResult($payload, false);
}
static::call(...$arguments);
foreach ((static::$expose ?? []) as $expose) {
$payload[$expose] = $this->{$expose};
}
return new InteractorResult($payload, true);
}
}
Думаю, чтобы пустить такое решение в продакшен должны быть действительно веские аргументы. Я не представляю как понять что он делает не запустив его, или не покопавшись в документации по магическим методам.
Пока что не совсем понятны преимущества такого подхода даже перед самым простым решением в лоб — запилить какой-то класс сервис, который будет заниматься созданием Книги, и дерганьем других сервисов, типа Email-sender`а если его нужно отправить Email при создании книги.
У такого сервиса будет хотя бы явно-определенный интерфейс с типизацией.
Проблема «сервисов» в том, что само их определение достаточно размытое и каждый может вкладывать в этот термин свой смысл. Интерактор (или же «операция») — по факту и есть максимально узко специализированный «класс-сервис», отвечающий за конкретную операцию бизнес-процесса. Интерфейс интерактора в данном случае — один метод call. Типизация (type hinting) к сожалению потерялась в угоду другим плюшкам. Это особенность конкретной реализации интерактора. Сам же трэйт тут нужен по двум причинам — что бы примеры кода в тексте были максимально приближены к исходному тексту, и для уменьшения количества шаблонного кода плюс единообразный интерфейс всех интеракторов.
Зона ответственности класса так же весьма размыта. Судя по коду из базового трейта Интерактора автор уже как минимум предлагает прикручивать к реализации валидацию входящего запроса.
Нет, валидация именно входящего запроса не предполагается. Валидация входных параметров может быть частью операции и реализует «return early» паттерн. Например, на вход может подаваться результат другого интерактора.
Думаю, чтобы пустить такое решение в продакшен должны быть действительно веские аргументы. Я не представляю как понять что он делает не запустив его, или не покопавшись в документации по магическим методам.
Это всего лишь реакция «триггер» на наличие магических методов и чего то нового. Если мы имеем некий AddBookService, это точно так же ничего не говорит нам о том, что он делает и как его запустить не покопавшись в документации (или исходном коде). Когда в тоже самое время реализация интеракторов рассмотренная в тексте, сводит интерфейс класса к одному единственному публичному методу (call\__invoke). Некая «магия» конечно присутствует, но без этого не обходится ни один современный фрэймворк, так как эта магия призвана упрощать нам жизнь.
Ну и тащить что то в продакшен тут не предлагается, цель текста — рассмотреть и обсудить паттерн, показать простейший пример разработки через тестирование.
Нет, валидация именно входящего запроса не предполагается. Валидация входных параметров может быть частью операции и реализует «return early» паттерн.
Я не силён в этих ваших паттернах, но не нарушает ли такое поведение указанный же выше принцип единственной ответственности?
Краткий пример (псевдокод):
if (valid($params)) {
doSomething()
} else {
...
}
//return early
if (!valid($params)) {
return
}
doSomething()
Здесь же она скорей нужна для использования интеракторов в цепочке:
result = doSomething(params) //вернул negative result
result = doSomething2(result) // проверили, что на вход пришёл negative result и вернули его же
Проблема «сервисов» в том, что само их определение достаточно размытое и каждый может вкладывать в этот термин свой смысл. Интерактор (или же «операция») — по факту и есть максимально узко специализированный «класс-сервис», отвечающий за конкретную операцию бизнес-процесса.
Я не вижу никаких проблем в обычных классах-сервисах которые аггрегируют внутри себя вызовы других сервисов, определяя какой-то сценарий. С Интеракторами у нас получается тоже самое, но с исковерканным не понятно для чего интерфейсом.
Ну то есть да, если бы я такое увидел у меня точно рука не поднялась добавлять в эту странную штуку какие-то свои методы, помимо call(), но такие вещи должны отсекаться на уровне code review, а от добавления в Интеракторы какой-нибудь не подходщей для них логики неопытным программистом всё-равно никто нас не застрахует.
Для пущего спокойствия можно инжектить обычные сервисы через единый интерфейс с каким-нибудь одним публичным методом типа call();
Поздравляю, вы придумали command bus, только без bus.
https://laravel.com/docs/5.0/bus
Хотя, там нет возвращаемого значения, а значит это скорее процедура, чем функция.
Ну или вот Actions из yii
https://www.yiiframework.com/doc/api/2.0/yii-base-action
Прям почти тоже самое.
Вот только что это меняет?
А собственно ничего. Фактически, в вашей реализации, это сервис с одним методом.
Какая мне разница, тестировать один сервис с десятью методами, или десять сервисов с одним методом?
Удобнее ли это? Кому-то да, особенно функциональщикам — они любят накомпорзировать всякого, а потом гонять скомпоженное туда-сюда. А кому-то и нет. Потому-как (здравствуйте), в Java (откуда, судя по всему, это поделие и привалило к нам), в отличии от php, имеются дженерики, и можно обозначить возвращаемое значение например как InteractorResult<User>
, тем самым явно определив содержимое результата. В php же для сохранения явной типизации, придется плодить наследников типа class InteractorUserResult extends InteracotrResult
… дичь, в общем.
Идея нормальная, но надо оправить ее обратно в Java. Пусть приходит, когда в php дженериков завезут.
Вот только что это меняет?В целом это всё схожие паттерны.
Всё остальное в статье. Пришло это не из java, а из ruby (а туда надо полагать из функциональщины). Один сервис с десятью методами запросто превращается в God Object.
Вот с типизацией действительно есть некоторая проблема, дженерики тут немного мимо, InteractorResult — уже сам по себе контейнер, вместо него мог быть какой нибудь именованный tuple.
Паттерн Интерактор (Interactor, Operation)