【问题标题】:Handle ARGV in Ruby without if...else block在没有 if...else 块的情况下在 Ruby 中处理 ARGV
【发布时间】:2016-12-26 18:56:04
【问题描述】:

在一篇关于 unconditional programming Michael Feathers 的博文中,展示了如何将限制 if 语句用作降低代码复杂性的工具。

他用一个具体的例子来说明他的观点。现在,我一直在思考其他可以帮助我更多地了解无条件/ifless/forless 编程的具体示例。

例如在这个cat 克隆中有一个if..else 块:

#!/usr/bin/env ruby

if ARGV.length > 0
  ARGV.each do |f|
    puts File.read(f)
  end
else
  puts STDIN.read
end

原来 ruby​​ 有ARGF,这让这个程序更简单:

#!/usr/bin/env ruby

puts ARGF.read

我想知道如果ARGF 不存在,如何重构上面的示例,所以没有if..else 块?

还对指向其他说明性具体示例的链接感兴趣。

【问题讨论】:

  • 我认为您将该博客文章误读为绝对规则,而不是您工具箱中的工具。但在您的情况下,您可以通过首先从ARGV 创建一个 IO 对象数组来简化代码,如果该列表为空,则使用STDINio = ARGV.map { |f| File.new(f) }; io = [STDIN] if !io.length; 然后你的代码可以用io 做它喜欢的事情。虽然这具有严格相同数量的条件,但它消除了 if/else 块,从而消除了一个分支:代码是线性的。由于它将收集数据与使用数据分开,因此您可以将其放入一个方法中并重用它,从而进一步降低复杂性。
  • 在 Stack Overflow 上克隆猫是题外话,请迁移到 biology.stackexchange.com :)
  • @Schwern 你对这个用法很有信心,当然如果它使你的代码更难阅读并且可能更慢,但在你的建议中你仍然使用 if
  • @peter 不要挂断if

标签: ruby oop


【解决方案1】:

技术上你可以,

inputs = { ARGV => ARGV.map { |f| File.open(f) }, [] => [STDIN] }[ARGV]
inputs.map(&:read).map(&method(:puts))

虽然那是code golf,而且太聪明了。

不过,它是如何工作的?

  • 它使用哈希来存储两个备选方案。
  • ARGV 映射到一组打开的文件
  • [] 映射到带有STDIN 的数组,如果ARGV 为空,则有效地覆盖它
  • 在哈希中访问ARGV,如果为空则返回[STDIN]
  • 读取所有打开的输入并打印出来

但不要编写该代码。

正如my answer to your other question 中提到的,无条件编程并不是要不惜一切代价避免if 表达式,而是要努力获得可读性和意图揭示的代码。有时这只是意味着使用if 表达式。

【讨论】:

    【解决方案2】:

    你不能总是摆脱条件(可能有大量的类),Michael Feathers 并不提倡这样做。相反,这是对过度使用条件句的一种强烈反对。我们都见过噩梦般的代码,它是无穷无尽的嵌套 if/elsif/else 链,他也是。

    此外,人们通常会在条件句中嵌套条件句。我见过的一些最糟糕的代码是嵌套条件的巨大噩梦,其中散布着一些奇怪的工作。我认为控制结构的真正问题在于它们经常与工作混在一起。我敢肯定,我们可以通过某种方式将此视为一种单一责任违规形式。

    您可以通过首先从 ARGV 创建一个 IO 对象数组来简化代码,而不是盲目地尝试消除该条件,如果该列表为空,则使用 STDIN。

    io = ARGV.map { |f| File.new(f) };
    io = [STDIN] if !io.length;
    

    然后您的代码可以使用io 做自己喜欢的事情。

    虽然这具有严格相同数量的条件,但它消除了 if/else 块,从而消除了一个分支:代码是线性的。更重要的是,由于它将收集数据与使用数据分开,您可以将其放入一个函数中并重用它,从而进一步降低复杂性。一旦它在一个函数中,我们就可以利用提前返回。

    # I don't have a really good name for this, but it's a
    # common enough idiom. Perl provides the same feature as <>
    def arg_files
        return ARGV.map { |f| File.new(f) } if ARGV.length;
        return [STDIN];
    end
    

    现在它在一个函数中,你的代码对所有文件或标准输入进行分类变得非常简单。

    arg_files.each { |f| puts f.read }
    

    【讨论】:

    • 你仍然使用if,这不是OP要求的,使用方法也不是OO方式
    • @peter 在编程中,客户并不总是正确的。我解释了为什么无条件编程在答案的顶部更像是一种练习,一种工具。至于 OO,它可以很好地用作假设的 ARGV 类的方法,但是没有这样的类,ARGVArray。把它放在Array 上是不合适的;它不适用于所有数组,仅适用于命令行参数。您为工作挑选工具;正如其他答案所证明的那样,在追求消除忽略文章目的的条件时,事情变得更加复杂。
    【解决方案3】:

    首先,虽然原理很好,但你必须考虑其他更重要的事情,例如可读性和执行速度。

    也就是说,您可以对 String 类进行猴子补丁以添加一个读取方法并将 STDIN 和参数放入一个数组中,然后从数组的开头开始读取,直到数组结尾减去 1,因此如果有参数,则在 STDIN 之前停止并且如果没有参数,则继续直到 -1(结束)。

    class String
      def read
        File.read self if File.exist? self
      end
    end
    
    puts [*ARGV, STDIN][0..ARGV.length-1].map{|a| a.read}
    

    在有人注意到我仍然使用 if 来检查文件是否存在之前,您应该在示例中使用两个 if 来检查这一点,如果不这样做,请使用救援来正确通知用户。

    编辑:如果您要使用该补丁,请阅读这些链接中可能出现的问题 http://blog.jayfields.com/2008/04/alternatives-for-redefining-methods.html http://www.justinweiss.com/articles/3-ways-to-monkey-patch-without-making-a-mess/

    由于 read 方法不是 String 的一部分,因此不需要使用 alias 和 super 的解决方案,如果您打算使用 Module,这里是如何做到的

    module ReadString
      def read
        File.read self if File.exist? self
      end
    end
    
    class String
      include ReadString
    end
    

    编辑:只需阅读有关猴子补丁的安全方法,有关您的文档,请参阅https://solidfoundationwebdev.com/blog/posts/writing-clean-monkey-patches-fixing-kaminari-1-0-0-argumenterror-comparison-of-fixnum-with-string-failed?utm_source=rubyweekly&utm_medium=email

    【讨论】:

    • -1 用于向String 建议一个猴子补丁。它作为一个函数工作得很好。 File.exist? 也是不够的,文件可以存在但不可读。它还引入了一个竞争条件,一个文件可以存在,然后在你打开它之前被删除。检查是否可以打开文件的最佳方法是尝试打开文件并在失败时处理异常。
    • @Schwern,你读过我的预订吗?并不是因为他不应该使用它,所以他不应该知道这种可能性以及相关的问题。 Monkey patching 通常用于 Ruby,是的,您需要知道自己在做什么,以确保他知道我添加了一些链接
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-25
    相关资源
    最近更新 更多