如何在导出为 CSV 的 Ruby 方法中重构嵌套循环?

How to refactor nested loops in a Ruby method that exports to CSV?

我必须将一些信息导出到 CSV。我写了这段代码,但我不太喜欢它。我不知道如何重构它并摆脱嵌套循环。

我的关系如下:Order 有很多 Moves,Move 有很多 Stops。

我必须将所有这些导出到 CSV,所以我将有多个行用于同一个订单。

这是我的(低质量)代码:

def to_csv
  CSV.generate(headers: true) do |csv|
    csv << h.t(self.first.exported_attributes.values.flatten) # headers
    self.each do |order|
      order.moves.map do |move|
        move.stops.map do |stop|
          order_data = order.exported_attributes[:order].map do |attributes|
            order.public_send(attributes)
          end
          move_data = order.exported_attributes[:move].map do |attributes|
            move.decorate.public_send(attributes)
          end
          stop_data = order.exported_attributes[:stop].map do |attributes|
            stop.decorate.public_send(attributes)
          end
          csv << order_data + move_data + stop_data
        end
      end
    end
  end
end

我昨天做了这个:

  def to_csv
    CSV.generate(headers: true) do |csv|
      csv << h.t(self.first.exported_attributes.values.flatten) # headers
      self.each do |order|
        order.moves.each do |move|
          move.stops.each do |stop|
            csv << order.exported_attributes[:order].map { |attr| order.public_send(attr) } +
              order.exported_attributes[:move].map { |attr| move.decorate.send(attr) } +
              order.exported_attributes[:stop].map { |attr| stop.decorate.send(attr) }
          end
        end
      end
    end
  end

我闻到的最大气味不是嵌套循环,而是从每个模型中获取值的方式几乎重复。

让我们在 OrderMoveStop:

上将重复提取到具有相同名称 exported_values 的类似方法中
class Order
  def exported_values
    exported_attributes[:order].map { |attrs| { public_send(attrs) }
  end
end

class Move
  def exported_values
    order.exported_attributes[:stop].map { |attrs| { decorate.public_send(attrs) }
  end
end

class Stop
  def exported_values
    move.order.exported_attributes[:move].map { |attrs| { decorate.public_send(attrs) }
  end
end

并在 to_csv:

中使用它们
def to_csv
  CSV.generate(headers: true) do |csv|
    csv << h.t(first.exported_attributes.values.flatten) # headers
    each do |order|
      order_values = order.exported_values
      order.moves.each do |move|
        order_and_move_values = order_values + move.exported_values
        move.stops.each do |stop|
          csv << order_and_move_values + stop.exported_values
        end
      end
    end
  end
end

以上还有一些额外的小改进:

  • 在最外层的循环中获取并连接导出的值以提高效率。
  • 循环以 each 而不是 map 移动和停止,因为循环是针对副作用而不是 return 值完成的。
  • 删除 self..
  • 的不必要使用

现在 to_csv 还不错。但是它还有一点点feature envy(就是调用了太多其他对象的方法),所以让我们提取更多的方法到模型上:

def to_csv
  CSV.generate(headers: true) do |csv|
    csv << h.t(first.exported_attributes.values.flatten) # headers
    each { |order| order.append_to_csv(csv) }
  end
end

class Order
  def append_to_csv(csv)
    values = exported_values
    moves.each { |move| move.append_to_csv(csv, values) }
  end
end

class Move
  def append_to_csv(csv, prefix)
    values = exported_values
    stops.each { |stop| stop.append_to_csv(csv, prefix + values) }
  end
end

class Stop
  def append_to_csv(csv, prefix)
    csv << prefix + exported_values
  end
end

没有更多的嵌套循环。提取的方法有点重复,但我认为如果提取重复的话,它们就不清楚了。

接下来我们可以尝试将 exported_values 个方法重构为一个方法。

  • 也许 Order#exported_attributes 可以分解为每个 class 上的一个方法,它不接受任何参数,return 只接受 class 的方法导出的属性。

  • 这些方法之间的另一个区别是 Order 不需要 .decorator 但其他 class 需要。如果它有一个装饰器,只需使用它而不是实际的顺序;如果没有,就给它一个假的:

    class Order
      def decorator
        self
      end
    end
    

然后您可以在模块中定义一个 exported_values 方法并将其包含在所有三个 class 中:

def exported_values
  exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end

还有一个可能的改进:如果每个模型的导出值在实例的生命周期内保持不变是可以的,您可以像这样缓存它们

def exported_values
  @exported_values ||= exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end

并在 append_to_csv 方法中内联 values 局部变量 从这些方法中的父对象获取 "prefixes" 而不是传递它们作为参数。

可能所有新方法都应该提取到装饰器而不是模型;我不确定您的装饰器是用于 CSV 生成还是仅用于其他目的。