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

Ruby 为什么有太多行的方法是一件坏事?,ruby,coding-style,rubocop,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]

在我的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将此方法标记为
方法的行太多
。为什么写一个有太多行的方法是不好的?如果我们要在里面做很多工作呢?如何重新考虑这一点并编写好代码?

一种方法是,您可以使用重构,但要以可读性为代价

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

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

就像一篇有大段落的文章更难阅读一样,同样一个有较长方法的程序也很难阅读,并且不太可能重复使用。将代码分成的块越多,代码的模块化程度、可重用性和可理解性就越高

如果我们要在里面做很多工作呢

如果你必须在这方面做很多工作;这当然意味着你设计课程的方式并不好。也许,您需要设计一个新类来处理这个功能,或者您需要将这个方法分成更小的块

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

为此,我向您强烈推荐一本很棒的书,名为:Martin Fowler,他在解释如何、何时以及为什么重构代码方面令人难以置信

  • 假设bug的数量与代码的长度成正比,最好缩短代码
  • 即使保持了代码的长度(或者可能会稍微长一点),将一个长方法分解为短方法也会更好,因为对人类来说,一次阅读一个短方法并发现它的错误要比通读一个长方法更容易

当我用许多非常简短的方法阅读代码时,我发现要理解所有东西是如何组合在一起的,通常需要更长的时间。示例代码中很好地说明了其中的一些原因。让我们试着把它分成几个小方法:

def cart
  if user_signed_in?
    process_current_user
  else
    setup_cart
  end
end

马上就有两个大问题:

  • 在阅读了方法
    cart
    之后,我不知道它是做什么的。这部分是因为将值分配给实例变量,而不是将值返回给名为
    cart
    的方法
  • 我想通过
    process\u current\u user
    setup\u 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
    设置为
    当前用户
    ;及
  • @cart
    分配给一个值,该值取决于用户是否登录,以及在每种情况下是否存在id
我不认为测试这种大小的方法比用更小的方法测试我之前的代码分解更困难。事实上,确保测试是全面的可能更容易

我们还可以返回
cart
,而不是将其分配给实例变量,如果只需要确定用户是否登录
cart
,则可以避免将值分配给
@user

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

上面的答案很好,但请允许我提出一种不同的方法来编写相同的方法…:

# 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类中编写一个方法(或者这个控制器逻辑在你看来):


不幸的是,由于这是基于意见的,因此被搁置。但绝对不是。方法越短越好。这里给出了具体的实际原因:“以可读性为代价”——当您重构代码时,这是最终结果,您应该三思而后行
"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 :-)
# 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] )