1

我担心允许我让后端用户能够对元素进行排序。我将它用于一些不同的元素。Rails 社区似乎非常反对关注和回调,我想就如何更好地建模以下代码提供一些建议:

require 'active_support/concern'

module Rankable
  extend ActiveSupport::Concern
  included do
    validates :row_order, :presence => true
    scope :next_rank, lambda { |rank| where('row_order > ?',rank).order("row_order asc").limit(1)}
    scope :previous_rank, lambda { |rank| where('row_order < ?',rank).order("row_order desc").limit(1)}
    scope :bigger_rank, order("row_order desc").limit('1')
    before_validation :assign_rank
  end

  def invert(target)
    a = self.row_order
    b = target.row_order
    self.row_order = target.row_order
    target.row_order = a
    if self.save 
      if target.save
        true
      else
        self.row_order = a
        self.save
        false
      end
    else
      false
    end
  end

  def increase_rank
    return false unless self.next_rank.first && self.invert(self.next_rank.first)
  end

  def decrease_rank
    return false unless self.previous_rank.first && self.invert(self.previous_rank.first)
  end

  private
  def assign_default_rank
    if !self.row_order
      if self.class.bigger_rank.first
        self.row_order = self.class.bigger_rank.first.row_order + 1
      else
        self.row_order=0
      end
    end
  end
end
4

1 回答 1

2

I think a Concern is a good choice for what you are trying to accomplish (particularly with validations and scopes because ActiveRecord does those two very well). However, if you did want to move things out of the Concern, apart from validations and scopes, here is a possibility. Just looking at the code it seems like you have a concept of rank which is represented by an integer but can become it's own object:

class Rank
  def initialize(rankable)
    @rankable = rankable
    @klass = rankable.class
  end

  def number
    @rankable.row_order
  end

  def increase
    next_rank ? RankableInversionService.call(@rankable, next_rank) : false
  end

  def decrease
    previous_rank ? RankableInversionService.call(@rankable, previous_rank) : false
  end

  private

  def next_rank
    @next_rank ||= @klass.next_rank.first
  end

  def previous_rank
    @previous_rank ||= @klass.previous_rank.first
  end
end

To extract out the #invert method we could create a RankableInversionService (referenced above):

class RankableInversionService
  def self.call(rankable, other)
    new(rankable, other).call
  end

  def initialize(rankable, other)
    @rankable = rankable
    @other = other
    @original_rankable_rank = rankable.rank
    @original_other_rank = other.rank
  end

  def call
    @rankable.rank = @other.rank
    @other.rank = @rankable.rank

    if @rankable.save && @other.save
      true
    else
      @rankable.rank = @original_rankable_rank
      @other.rank = @original_other_rank

      @rankable.save
      @other.save

      false
    end
  end
end

To extract out the callback you could have a RankableUpdateService which will assign the default rank prior to saving the object:

class RankableUpdateService
  def self.call(rankable)
    new(rankable).call
  end

  def initialize(rankable)
    @rankable = rankable
    @klass = rankable.class
  end

  def call
    @rankable.rank = bigger_rank unless @rankable.ranked?
    @rankable.save
  end

  private

  def bigger_rank
    @bigger_rank ||= @klass.bigger_rank.first.try(:rank)
  end
end

Now you concern becomes:

module Rankable
  extend ActiveSupport::Concern

  included do
    # validations
    # scopes
  end

  def rank
    @rank ||= Rank.new(self)
  end

  def rank=(rank)
    self.row_order = rank.number; @rank = rank
  end

  def ranked?
    rank.number.present?
  end
end

I'm sure there are issues with this code if you use it as is, but you get the concept. Overall I think the only thing that might be good to do here is extracting out a Rank object, other than that it might be too much complexity that the concern encapsulates pretty nicely.

于 2013-09-03T16:57:27.683 回答