如何在yield块中调用枚举数的下一个

问题描述

我有一个批量更新过程,该过程会更新产品 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 && !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” ,在...之后将很难维护。像六个月 每个开发人员都会困惑为什么需要显式返回 true false 我有什么方法可以从该区块中拨打电话吗?还是不需要使用块?

我知道我可以通过应用 Template Pattern 解决此问题:制作一个包含该过程的抽象类,并继承它以覆盖每个私有方法

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行。 我不想为这么小的差异创建太多的类,所以我选择先使用块。

解决方法

next是控制流,所以不能,您不能从收益内部next

使用block_given?是使用此回调结构(没有像raisethrow这样的非线性控制流的唯一方法),而且正如您所提到的,它有点用奇怪的b / c抽象不太适合。

我认为“就地进行操作”而不是像这样注入障碍物会更直接:

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(&:increment!).each(&:update!)

或者您可能只需要像这样的update_subs!

class Product < ApplicationRecord
  def update_subs!(sub_ids)
    subs = subscriptions.where(id: ids).each { |sub| yield sub }
    subs.each(&: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(&:destroy!)
,

您可以利用catchthrow使跳过更明确:

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