为什么有太多行的方法是坏事?

时间:2015-06-07 03:24:51

标签: ruby coding-style rubocop

在我的rails应用程序中,我有一个这样的方法:

def cart
    if user_signed_in?
        @user = current_user
        if @user.cart.present?
            @cart = @user.cart
        else
            @cart = false
        end
    else
        cart_id = session[:cart_id]

        if cart_id.present?
            @cart = Cart.find(cart_id.to_i)
        else
            @cart = false
        end
    end
end

Rubocop将此方法标记为Method had too many lines。为什么写一个行太多的方法不好?如果我们必须做很多工作怎么办?我怎样才能重新考虑这一点并编写好的代码?

4 个答案:

答案 0 :(得分:8)

一种方法是你可以使用ternary operator重构它,但代价是可读性。

def cart
  if user_signed_in?
    @user = current_user
    @cart = @user.cart.present? ? @user.cart : false
  else
    cart_id = session[:cart_id]
    @cart = cart_id.present? ? Cart.find(cart_id.to_i) : false
  end
end

其次,如果你被迫编写一个很长的方法,那就意味着你的面向对象设计有问题。也许,您需要为额外功能设计一个新类,或者您需要通过编写多个方法将功能拆分到同一个类中,这些方法在组合时可以完成单个long方法的工作。

  

为什么写一个行太多的方法不好?

就像带有大段落的文章更难阅读一样,同样使用较长方法的程序难以阅读,并且不太可能重复使用。您划分代码的块越多,代码的模块化,可重用性和可理解性就越大。

  

如果我们必须做很多工作怎么办?

如果你需要做很多工作;这肯定意味着你以一种不好的方式设计你的课程。也许,您需要设计一个新类来处理此功能,或者您需要将此方法拆分为更小的块。

  

如何重新考虑这一点并编写好的代码?

为此,我强烈推荐一本名为:Refactoring的书,由Martin Fowler撰写,他在解释如何,何时以及为何重构代码方面令人难以置信。

答案 1 :(得分:2)

当我使用许多非常短的方法阅读代码时,我发现通常需要更长的时间来理解所有内容是如何组合在一起的。您的示例代码中很好地说明了一些原因。让我们尝试将其分解为微小的方法:

def cart
  if user_signed_in?
    process_current_user
  else
    setup_cart
  end
end

private

def process_current_user
  @user = current_user
  if @user.cart.present?
    assign_cart_when_user_present
  else
    assign_cart_when_user_not_present
  end
end

def assign_cart_when_user_present
  @cart = @user.cart
end

def assign_cart_when_user_not_present
  @cart = false
end

def setup_cart
  cart_id = session[:cart_id]
  if cart_id.present?
    assign_cart_when_id_present
  else
    assign_cart_when_id_present
  end
end

def assign_cart_when_id_present
  @cart = Cart.find(cart_id.to_i)
end

def assign_cart_when_id_not_present
  @cart = false
end

马上就有几个大问题:

  • 阅读方法cart后,我不知道它的作用。这部分是因为将值分配给实例变量而不是将值返回到调用cart的方法。
  • 我希望方法process_current_usersetup_cart的名称可以告诉读者他们做了什么。那些名字不这样做。它们可能会得到改进,但它们是我能想到的最好的。关键是,并不总是可以设计信息丰富的方法名称。

我想将cart以外的所有方法设为私有。这引入了另一个问题。我是否将所有私有方法放在一起 - 在这种情况下,我可能需要滚动几个不相关的方法才能找到它们 - 或者我是否通过代码在公共方法和私有方法之间进行交替? (当然,通过保持代码文件较小并使用mixin可以稍微缓解这个问题,假设我能记住哪个代码文件做了什么。)

还要考虑代码行数发生了什么。随着更多的代码行存在更多的错误机会。 (我故意用一个常见的错误来说明这一点。你有没有发现它?)测试单个方法可能更容易,但我们现在需要测试许多单独的方法是否能够正常工作。

现在让我们将它与稍微调整一下的方法进行比较:

def cart
  if user_signed_in?
    @user = current_user
    @cart = case @user.cart.present?
            when true then @user.cart
            else           false
            end
  else
    cart_id = session[:cart_id]
    @cart = case cart_id.present?
       when true then Cart.find(cart_id.to_i)
       else           false
    end
  end
end

这让读者一眼就能看出发生了什么:

    如果用户已登录,则
  • @user设置为current_user;和
  • @cart被分配给一个值,该值取决于用户是否已登录,并且在每种情况下,是否存在ID。

我不认为测试这种大小的方法比用较小的方法测试我早期的代码细分要困难得多。事实上,确保测试是全面的可能更容易。

我们也可能返回cart而不是将其分配给实例变量,并且如果用户登录时只需要确定@user,则应避免为cart分配值:

def cart
  if user_signed_in?
    cart = current_user.cart        
    case cart.present?
    when true then cart
    else           false
    end
  else
    cart_id = session[:cart_id]
    case cart_id.present?
    when true then Cart.find(cart_id.to_i)
    else           false
    end
  end
end

答案 2 :(得分:1)

  • 假设错误数量与代码长度成正比,最好缩短代码。
  • 即使代码长度保持不变(或者可能稍微长一点),最好将long方法分解为short方法,因为人们更容易一次读取一个简短的方法并找到它的bug而不是读取通过很长的一个。

答案 3 :(得分:0)

上面你有很好的答案,但请允许我建议一种不同的方法来编写相同的方法......:

# by the way, I would change the name of the method, as cart isn't a property of the controller.
def cart
    # Assume that `current_user` returns nil or false if a user isn't logged in.
    @user = current_user

    # The following || operator replaces the if-else statements.
    # If both fail, @cart will be nil or false.
    @cart = (@user && @user.cart) || ( session[:cart_id] && Cart.find(session[:cart_id]) )
end

正如您所看到的,有时候语句会被浪费掉。了解你的方法在“失败”时返回的内容可能会使编写代码更容易阅读和维护。

作为附注,为了理解||陈述,请注意:

"anything" && 8 # => 8 # if the statements is true, the second value is returned
"anything" && nil && 5 # => nil # The statement stopped evaluating on `nil`.
# hence, if @user and @user.cart exist:
@user && @user.cart #=> @user.cart # ... Presto :-)
祝你好运!

<强> P.S。

我会考虑在Cart类中编写一个方法(或者你认为这是Controller逻辑吗?):

# in cart.rb
class Cart
   def self.find_active user, cart_id
      return user.cart if user
      return self.find(cart_id) if cart_id
      false
   end
end

# in controller:

@cart = Cart.find_active( (@user = current_user), session[:cart_id] )