Handle command-line switch in Ruby without if…else

2019-09-20 03:27发布

问题:

In a blog post about unconditional programming Michael Feathers shows how limiting if statements can be used as a tool for reducing code complexity.

He uses a specific example to illustrate his point. Now, I've been thinking about other specific examples that could help me learn more about unconditional/ifless/forless programming.

For example, using OptionParser I made a cat clone that will upcase the stream if the --upcase switch is set:

#!/usr/bin/env ruby

require 'optparse'

options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: cat [options] [file ...]"

  opts.on("-u", "--upcase", "Upcase stream") do
    options[:upcase] = true
  end
end.parse!

if options[:upcase]
  puts ARGF.read.upcase
else
  puts ARGF.read
end

How would I handle that switch without an if..else block?

Also interested in links to other illustrative specific examples.

回答1:

Try this,

#!/usr/bin/env ruby
require 'optparse'

options = { :transform => :itself }
OptionParser.new do |opts|
  opts.banner = "Usage: cat [options] [file ...]"
  opts.on("-u", "--upcase", "Upcase stream") do
    options[:transform] = :upcase
  end
  # add more options for downcase, reverse, etc ...
end.parse!

puts ARGF.read.send(options[:transform])

This worked quite well, I am actually surprised how well that worked.

What has been changed?

  • The option is internally renamed to :transform
  • The internal default value is :itself
  • The command line switch sets the internal option to :upcase
  • Call the method with send

Not all if statements can be improved upon like this though. I would guess the idea of unconditional programming is to prefer a combination of meaningful default values, as I did above, and intention revealing functions whenever it seems reasonable but not at all costs.

Here are some examples of intention revealing functions,

  • max
  • min
  • Hash#fetch
  • Enumerable#detect
  • Enumerable#select
  • Enumerable#chunk
  • Enumerable#drop_while
  • Enumerable#slice_when
  • Enumerable#take_while
  • etc...

Another related practice is forless programming.

If you want to practice unconditional and forless programming best look for examples that process arrays and strings and make use of the many "functional" methods in Ruby's enumerable module.

Here is an example of string justification without for and if,

str = 'This is an example to be aligned to both margins'    
words = str.split
width, remainder = (50 - words.map(&:length).inject(:+)).divmod(words.length - 1)
words.take(words.length - 1).each { |each| width.times { each << 32 }}
words.take(words.length - 1).shuffle.take(remainder).each { |each| each << 32 }
p words.join
# => "This  is an example to  be aligned to both margins"


回答2:

Eliminating conditions is a tool for reducing complexity, not an end goal. I explained that better in my other answer. In this case the condition must be there because whether or not options[:upcase] is set is part of the logic. But you can at least eliminate the duplication.

Because the else clause is the same as the if clause but just adds a step, you can remove the repetition and make your code linear by unconditionally doing the common bit, and then conditionally doing the extra stuff.

data = ARGF.read;
data.upcase! if options[:upcase];