Как вызвать следующий enumerator в блоке yield

#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 для остановки выполнения цепочек обратного вызова.