Metrics/AbcSize 太高了:如何降低此方法中的 ABC?

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

我最近开始使用 Rubocop 来“标准化”我的代码,它帮助我优化了很多代码,还帮助我学习了很多 Ruby“技巧”。我知道我应该根据自己的判断并在必要时禁用 Cops,但我发现自己完全受制于以下代码:

def index
  if params[:filters].present?
    if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
      # if owned is true, then we don't need to filter by admin
      params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
      # if admin is true, then must not filter by owned if false
      params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
      companies_list =
        case params[:filters][:admin]&.to_b
        when true
          current_user.admin_companies
        when false
          current_user.non_admin_companies
        end
      if params[:filters][:owned].present?
        companies_list ||= current_user.companies
        if params[:filters][:owned].to_b
          companies_list = companies_list.where(owner: current_user)
        else
          companies_list = companies_list.where.not(owner: current_user)
        end
      end
    else
      # Filters for deleted companies
      companies_list = {}
    end
  end
  companies_list ||= current_user.companies
  response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
  json_response(response)
end

除其他外,我遇到的错误如下:

C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]

我理解它背后的数学原理,但我不知道如何简化这段代码以获得相同的结果。

有人可以给我一些指导吗?

提前致谢。

首先,这个代码是否经过全面测试,包括所有无数条件?它是如此复杂,除非测试套件是严格的,否则重构肯定会是灾难性的。因此,如果您还没有综合测试套件,请编写一个。如果已经有测试套件,请确保它测试所有条件。

其次,应用“胖模型瘦控制器”范式。所以把所有的复杂性都转移到一个模型中,我们称之为 CompanyFilter

def index
  companies_list = CompanyFilter.new(current_user, params).list
  response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
  json_response(response)
end

并将所有这些 if/then/else 语句移动到 CompanyFilter#list 方法中

测试仍然通过?很好,您仍然会收到 Rubocop 警告,但与 CompanyFilter class.

相关

现在你需要理清所有的条件。我有点难以理解发生了什么,但看起来它应该可以简化为一个单一的案例陈述,有 5 种可能的结果。所以 CompanyFilter class 可能看起来像这样:

class CompanyFilter
  attr_accessors :current_user, :params

  def initialize(current_user, params)
    @current_user = current_user
    @params = params
  end

  def list
    case
    when no_filter_specified
      {}
    when user_is_admin
      @current_user.admin_companies
    when user_is_owned
      # etc
    when # other condition
      # etc
    end
  end

  private
  def no_filter_specified
    @params[:filter].blank?
  end

  def user_is_admin
    # returns boolean based on params hash
  end

  def user_is_owned
    # returns boolean based on params hash
  end
end

测试还在通过?完美的! [编辑] 现在您可以将大部分控制器测试移动到 CompanyFilter class.

的模型测试中

最后,我会将所有不同的 companies_list 查询定义为公司模型的范围,例如

class Company < ApplicationRecord
  # some examples, I don't know what's appropriate in this app
  scope :for_user, ->(user){ where("...") }
  scope :administered_by, ->(user){ where("...") }
end

编写数据库范围时 ActiveRecord::SpawnMethods#merge 是你的朋友。

Post.where(title: 'How to use .merge')
    .merge(Post.where(published: true))

虽然它看起来不太像,但它可以让您以编程方式组合作用域,而不会过度依赖变异赋值和 if/else 树。例如,您可以组成一个条件数组,并将它们合并到一个 ActiveRecord::Relation 对象中 Array#reduce:

[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" =  AND "posts"."author" =  LIMIT 

因此,让我们将其与您在单独对象中处理过滤的瘦控制器方法结合起来:

class ApplicationFilter
  include ActiveModel::Attributes 
  include ActiveModel::AttributeAssignment 
  attr_accessor :user

  def initialize(**attributes)
    super()
    assign_attributes(attributes)
  end

  # A convenience method to both instanciate and apply the filters
  def self.call(user, params, scope: model_class.all)
    return scope unless params[:filters].present?
    scope.merge(
      new(
        permit_params(params).merge(user: user)
      ).to_scope
    )
  end

  def to_scope
    filters.map { |filter| apply_filter(filter) }
           .compact
           .select {|f| f.respond_to?(:merge) }
           .reduce(&:merge)
  end

  private
  # calls a filter_by_foo method if present or 
  # defaults to where(key => value)
  def apply_filter(attribute)
    if respond_to? "filter_by_#{attribute}"
      send("filter_by_#{attribute}")
    else 
      self.class.model_class.where(
        attribute => send(attribute)
      )
    end
  end
  # Convention over Configuration is sexy.
  def self.model_class 
    name.chomp("Filter").constantize 
  end

  # filters the incoming params hash based on the attributes of this filter class
  def self.permit_params
    params.permit(filters).reject{ |k,v| v.blank? }
  end
  
  # provided for modularity
  def self.filters
    attribute_names
  end
end

这使用了 Rails 提供的一些优点来设置具有动态处理过滤属性的属性的对象。它会查看您已声明的属性列表,然后将这些属性从参数中分离出来,并为该过滤器应用一个方法(如果存在)。

然后我们可以写一个具体的实现:

class CompanyFilter < ApplicationFilter
  attribute :admin, :boolean, default: false
  attribute :owned, :boolean 

  private

  def filter_by_admin
    if admin
      user.admin_companies
    else
      user.non_admin_companies
    end
  end

  # this should be refactored to use an assocation on User
  def filter_by_owned
    case owned
    when nil
      nil
    when true
      Company.where(owner: user)
    when false
      Company.where.not(owner: user)
    end
  end
end

你可以这样调用它:

# scope is optional
@companies = CompanyFilter.call(current_user, params), scope: current_user.companies)