Принёс спор на суд. По старой привычке с Laravel выношу логику в сервисы, а коллега на ревью каждый раз просит вернуть её в модель. Пример, отмена заказа:
class CancelOrder
def self.call(order, reason:)
new(order, reason).call
end
def initialize(order, reason)
@order = order
@reason = reason
end
def call
return false unless @order.cancelable?
@order.transaction do
@order.update!(status: :canceled, cancel_reason: @reason)
@order.items.each(&:release_stock!)
end
OrderMailer.canceled(@order).deliver_later
true
end
end
Коллега говорит, что это order.cancel!(reason) и десяток строк в модели. Я отвечаю, что Order и так 600 строк. Кто прав?
Я тут скорее на стороне коллеги, и не из принципа. Отмена заказа это поведение заказа, и order.cancel! читается естественно. Если модель 600 строк, проблема не в отмене, а в том, что в ней живёт ещё десять тем сразу. Её можно разрезать на concern'ы по смыслу, и тогда отмена лежит в Order::Cancellation:
module Order::Cancellation
extend ActiveSupport::Concern
def cancel!(reason)
return false unless cancelable?
transaction do
update!(status: :canceled, cancel_reason: reason)
items.each(&:release_stock!)
end
OrderMailer.canceled(self).deliver_later
true
end
end
Сервис я беру, когда операция трогает несколько равноправных моделей или внешний мир: платёжку, склад в другом сервисе. Тогда у неё нет естественного хозяина.
в Go это была бы функция CancelOrder(order, reason), и спора бы не было
Самое рабочее правило, которое я видел: если метод меняет только саму запись и её дочерние, он в модели. Если трогает чужие агрегаты или ходит наружу, это сервис. Споры на ревью заканчиваются, когда правило записано в README команды, а не живёт в головах.
Пришёл из C# и сначала тоже всё складывал в сервисы. Со временем почти всё переехало в модели, кода стало меньше