#ruby-on-rails #ruby #design-patterns
#ruby-on-rails #ruby #шаблоны проектирования
Вопрос:
У меня есть процесс массового обновления, который обновляет подписку на продукт (has_many) и вызывается в нескольких местах, поэтому я преобразовываю его в сервис.
В каждом месте, где вызывается этот процесс, все еще есть своя специальная предварительная обработка: например, добавление счетчика и т.д. И некоторые подписки: обновление может быть пропущено, если оно будет уничтожено. Итак, я отправляю ему блок:
# subscriptions_params is an array containing permitted parameters from controller
module SavingService
def self.call!(product, subscriptions_params)
subscriptions_params.each do |params|
subscription = product.subscriptions.find(params[:id])
next if block_given amp;amp; !yield(subscription, params)
subscription.update!(params)
end
product.update_something!
end
end
# It can work well
SavingService.call!(product, subscriptions_params)
# I can add some special process in the block
SavingService.call!(product, subscriptions_params) do |subscription, params|
if params[:checked]
subscription.counter = 1
true
else
subscription.destroy!
false
end
end
Однако мне нужно явно вернуть true или false, чтобы выполнить «next», это будет трудно поддерживать после… около 6 месяцев.
Каждый разработчик будет сбит с толку тем, почему ему нужно явно возвращать true, false.
Есть ли какой-либо способ, которым я могу вызвать next из блока? или не нужно использовать блок?
Я знаю, что могу решить эту проблему, применив шаблон Template: создайте абстрактный класс, содержащий процесс, и унаследуйте его, чтобы перезаписать каждый частный метод:
class SavingService
def call!
pre_process
process
post_process
end
private
def pre_process; end
def process; end
def post_process; end
end
Но разные части каждого места, вызывающего процесс, очень малы, всего 1-3 строки.
Я не хочу создавать так много классов для таких крошечных различий, поэтому я решил сначала использовать block.
Ответ №1:
next
это поток управления, так что нет, вы не можете next
изнутри yield.
Использование block_given?
— единственный способ сделать это с этой структурой обратного вызова (без нелинейного потока управления, такого как raise
или throw
), и, как вы упомянули, это работает немного странно, потому что абстракция c не совсем подходит.
Я думаю, было бы проще использовать «to things in place» вместо того, чтобы вводить блок, подобный этому:
to_increment, to_destroy = subscriptions_params.partition { |p| p[:checked] }
product.subscriptions.where(id: to_increment.map { _1[:id] })
.each { |sub| sub.counter = 1 }
.then { |subs| Subscription.update_all(subs) } # something like this, I forget exact syntax
product.subscriptions.where(id: to_destroy.map { _1[:id] }).destroy_all!
Причина этого в том, что для реального извлечения не так много разделяемой логики или «работы» — это просто выполнение некоторых действий несколько раз.
Возможно, то, что вы ищете, — это встроить эти действия в Subscription
как методы? вот так:
class Subscription < ApplicationRecord
def increment!
self.counter = 1
end
end
product.subscriptions.where(id: to_increment).each(amp;:increment!).each(amp;:update!)
Или, возможно, все, что вам нужно, это update_subs!
подобное:
class Product < ApplicationRecord
def update_subs!(sub_ids)
subs = subscriptions.where(id: ids).each { |sub| yield sub }
subs.each(amp;:update!)
end
end
# one line each, can't get much more straightforward than this
product.update_subs!(to_increment) { |sub| sub.counter = 1 }
product.subscriptions.where(id: to_destroy).each(amp;:destroy!)
Комментарии:
1. спасибо за ваш ответ! Я выбрал catch and throw one, потому что это именно то, что я хочу. но ваш ответ меня очень вдохновляет! Особенно для этой фразы: Причина этого в том, что для реального извлечения не так много общей логики или «работы» — это просто выполнение некоторых действий несколько раз.
Ответ №2:
Вы могли бы использовать catch
и throw
, чтобы сделать пропуск более явным:
module SavingService
def self.call!(product, subscriptions_params)
subscriptions_params.each do |params|
catch(:skip) do
subscription = product.subscriptions.find(params[:id])
yield(subscription, params) if block_given?
subscription.update!(params)
end
end
product.update_something!
end
end
SavingService.call!(product, subscriptions_params) do |subscription, params|
if params[:checked]
subscription.counter = 1
else
subscription.destroy!
throw(:skip)
end
end
Комментарии:
1. никогда не видел этого раньше!
2. @kevinluo201 вам это редко нужно. Rails использует
throw :abort
для остановки выполнения цепочек обратного вызова.