在Ruby中处理ARGV而不使用if...else代码块

4
在一篇关于无条件编程的博客文章中,Michael Feathers展示了如何使用限制if语句作为减少代码复杂度的工具。
他使用了一个具体的例子来说明他的观点。现在,我一直在思考其他具体的例子,以帮助我更好地学习无条件/ifless/forless编程。
例如,在这个猫咪克隆中有一个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块?
同时也对其他说明性具体示例的链接感兴趣。

1
我认为你误读了那篇博客文章,把它当作绝对规则而不是工具箱中的工具。但在你的情况下,你可以通过首先从ARGV创建一个IO对象数组,并且如果该列表为空,则使用STDIN来简化代码。io = ARGV.map { |f| File.new(f) }; io = [STDIN] if !io.length; 然后你的代码可以随意使用 io。虽然这严格来说有相同数量的条件语句,但它消除了if/else块,因此分支是线性的。由于它将数据收集与使用分开,你可以将其放入方法中并重复使用,从而进一步减少复杂性。 - Schwern
1
在Stack Overflow上,克隆猫是不相关的话题,请迁移到http://biology.stackexchange.com/ :) - Andrew Grimm
@Schwern,你对使用的观点是正确的,虽然它可能会使你的代码更难阅读,也可能会变慢,但在你的建议中仍然使用了if语句。 - peter
@peter 不要被 if 绊住。 - Schwern
3个回答

1
技术上来说,你可以这样做,
inputs = { ARGV => ARGV.map { |f| File.open(f) }, [] => [STDIN] }[ARGV]
inputs.map(&:read).map(&method(:puts))

虽然这很 代码高尔夫,但也过于聪明。

那么它是如何工作的呢?

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

不要写那段代码。

正如我在回答你其他问题中提到的,无条件编程并不是为了不惜一切代价避免if表达式,而是为了追求可读性和意图明确的代码。有时这只意味着使用一个if表达式。


-1

你并不能总是摆脱条件语句(除非使用大量的类),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所要求的,同时使用方法也不符合面向对象编程的方式。 - peter
@peter 在编程中,客户并不总是正确的。我在答案顶部解释了无条件编程更多的是一种练习、工具。至于面向对象编程,它可以作为一个假设的 ARGV 类的方法,但实际上并没有这样的类,ARGV 是一个 Array。把它放在 Array 上是不合适的;它并不适用于所有数组,只适用于命令行参数。你需要根据任务选择合适的工具;正如其他答案所证明的那样,为了消除忽略文章目的的条件而使事情变得更加复杂。 - Schwern

-2

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

话虽如此,您可以monkeypatch String类以添加read方法,并将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语句来进行检查,如果您没有这样做,请使用rescue来向用户提供正确的信息。

编辑:如果您使用了补丁,请阅读以下链接中可能出现的问题 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 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
@Schwern,你看到我的保留意见了吗?这不是因为他不应该使用它,而是他应该知道相关问题的可能性。猴子补丁通常用于Ruby,是的,你需要知道自己在做什么,为了确保他知道我添加了一些链接。 - peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接