如何用Ruby中的闭包来提高代码的可读性

109 阅读5分钟

代码的可读性对于任何项目的可维护性来说都是一个非常关键的参数。 严肃的业务需要对故障做出反应,并尽快修复错误。 这样就不会失去老客户,并使服务对新客户更具吸引力。

但如何提高可读性呢?人们可能认为,安装、配置和遵循RubocopReek等自动工具就足够了。的确,这些工具很好。不幸的是,它们并不是银弹。它们不能消除一个重要的因素--与自然语言的距离。 代码越接近自然语言,其可读性越好。这是每个人都应该努力达到的标准。

支持高阶函数的语言有一个很好的概念,叫做闭包。闭包就是被其他函数作为参数接收的函数(不管是不是匿名的)。 接收函数可以在需要的时候选择性地调用传递的闭包。一个有趣的事实是,闭包是以它们被传递的上下文为界限的。这就允许干扰到远处上下文的执行。

如果这个定义听起来很麻烦,很吓人,我建议你看看这个基于JavaScript的例子的介绍。

Ruby有等同于函数的方法。尽管Ruby不允许将方法名作为参数,但还是有可能实现闭包。

请看这个例子,它证明了方法不能作为参数使用:

def foo
  puts 'hello from foo'
end

def bar(f)
  f()
end

bar(foo) # => NoMethodError (undefined method `f' for main:Object)

Ruby有lambdas和procs来实现闭包的概念。

进一步说,方法和其他东西一样是一个对象,这意味着可以得到一个代表方法的对象。下面是如何做到这一点的:

method(:foo) # => #<Method: main.foo>

但是上面的例子仍然无法工作:

bar(method(:foo)) # => NoMethodError (undefined method `f' for main:Object)

() 执行lambdas、procs和Method 对象是不可能的。他们#call 方法来做这个。用这个知识来应用修复:

def bar(f)
  f.call()
end
bar(method(:foo)) # => hello from foo

Ta-da!最后,闭包工作了!

顺便说一下,有一个f.call() 的缩写,那就是f.():

def bar(f)
  f.()
end
bar(method(:foo)) # => hello from foo

它在商业开发中很少见,但在流行的开源项目中经常使用。

理论够了。转入实际工作。假设,我们需要写一个应用程序,向特定的收件人发送文本信息。看看下面的代码,一个人可能会想出实现这个想法的方法:

User = Struct.new(:phone, :active)

class SMSGateway
  # @param phone [String]
  # @param message [String]
  def self.send_message(phone, message)
    puts "Hello #{phone}, #{message}"
  end
end

class MessageService
  # @param message [String]
  # @param recipients [Array<User>]
  def broadcast(message, recipients)
    recipients.each do |recipient|
      SMSGateway.send_message(recipient.phone, message) if recipient.active
    end
  end
end

recipients = [
  User.new('+12222222222', true),
  User.new('+13333333333', false)
]
service = MessageService.new
service.broadcast('have a good day!', recipients)

现在,想象一下人类如何阅读这段代码。我将从我自己的角度说起。为了快速掌握代码的想法,我首先从学习应用编程接口(API)开始。 我不从细枝末节的细节开始。这至少对我来说是有效的,我发现这种学习代码的方式非常有用。

所以,我从这两行开始:

service = MessageService.new
service.broadcast('have a good day!', recipients)

我立即看到了API。MessageService#broadcast 方法是我们的 "入口点"。在这一点上,它的作用已经很直观了。这是因为这个方法有一个好名字:

不幸的是,并不总是这样。想象一下,如果有人把这个方法命名为callperform ,那么它的责任就不会那么明显了。在实践中,有一些例子是完全混乱的,甚至比模糊的call/perform/whatever 更糟。API和命名是非常重要的!通过这个例子感受到了这一点。

然后我想了解更多关于#broadcast 的细节,并跳到它的实现:

def broadcast(message, recipients)
  recipients.each do |recipient|
    SMSGateway.send_message(recipient.phone, message) if recipient.active
  end
end

它很直接,但尽管如此,它已经迫使我停下眼睛思考了一会儿。 我需要在我的记忆中至少保留3个变量来理解这段代码。对于这段特定的代码来说,这不是一个问题。 但是在现实世界中,像这样的方法不是3行代码,尽管它位于API的最顶端。 它通常是一大块......(你知道我的意思,对吗?)因此,改进它的问题是最重要的。

这里是我的解决方案,利用上述理论使它变得更好:

class MessageService
  # @param message [String]
  # @param recipients [Array<User>]
  def broadcast(message, recipients)
    recipients.each(&send_message(message))
  end

  private

  # @param message [String]
  # @return [Proc] a closure sends the given message to the recipient
  def send_message(message)
    ->(recipient) { SMSGateway.send_message(recipient.phone, message) if recipient.active}
  end
end

工作实例位于这里

我把所有的低级细节都移到了#send_message ,并使用了另一个从方法到程序的过渡(这就是& 所做的)。这使得我可以把方法作为each 所期望的一个块。

#send_message 返回一个用 速记法构造的proc的肉体的闭包。把它看作是一个延迟的消息发送到来自 迭代器的 。-> recipients.each recipient

在这里阅读更多关于 "方法到proc "的技术。

不管你同意与否,最终的#broadcast 方法实现比之前的版本更具可读性:

# @param message [String]
# @param recipients [Array<User>]
def broadcast(message, recipients)
  recipients.each(&send_message(message))
end

当我阅读它时,我只需使用我的直觉和自然语言知识就能立即看到正在发生什么。如果我需要更多的细节,我可以跳到#send_message,了解更多的细节。要穿越一个错误,在这一点上停下来就足够了。 这一点上已经没有很多细节了。因此,发现一个bug会更容易和更快。

作为奖励,方法分离提供了一个记录它的机会,并概述了参数和返回值的类型。在Ruby中,缺乏严格的API的类型是另一个痛苦,我们应该尽一切努力来改善它,我相信。

祝你在你的Ruby代码中缓解复杂性