Skip to content

Instantly share code, notes, and snippets.

@timfenney
Last active March 21, 2019 04:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timfenney/a7017cc47b61fcbb1eec5eb8f2cbdf51 to your computer and use it in GitHub Desktop.
Save timfenney/a7017cc47b61fcbb1eec5eb8f2cbdf51 to your computer and use it in GitHub Desktop.
Avoiding conditionals and early return in Ruby...
#!/usr/bin/env ruby
# This is a refactored example from the Travis CI Project. The file originally had several conditionals, as well as early
# returns (`exit` expressions, with exit statuses).
# (Original file: https://raw.githubusercontent.com/travis-ci/travis-api/master/bin/users)
# What follows is the code reworked for the removal of conditionals and early returns.
# You may be thinking, "But doesn't the program in the end have the same complexity?" And you'd be almost right, except for
# the fact that that early return elimination is an immediate win. However cyclomatic complexity is a problem for individual
# code units; what we've done here will allow easy extraction of the various Hash sets (eg. OPTIONS with OPTIONS_DEFAULT) to
# be extracted into another unit or method (the top level of a Ruby script could be considered a method in itself). A program
# may have many, many possible paths through it, but if each method has only a few, that can potentially save a lot of money
# in the long run.
# NB: Unrelated refactorings are possible, but haven't been performed as it would confuse the example.
# NB #2: This is an experiment; while this transformation will allow lower complexity per code unit, that will require
# code which appears unconventional in style, and also moving some of the logic into separate code units. This is surely
# a tradeoff, and one would have to make a judgment about this kind of refactoring on a case-by-case basis. In a simple
# example like this one, I would be tempted to leave it as is; or perhaps just capture the status code early on and have
# all the code paths exit at the same point. If there were nested conditionals, I would see a stronger case to make sure
# they are pulled out into separate methods.
require_relative './user_mgmt'
option = ARGV.first
users = nil
# The option '--all' or 'nil' both give the same result; hence they get the same lambda.
ALL_OR_NIL = lambda do
users = User.all.alpha
counts = "Total: #{users.count} Active: #{User.active.count} Inactive: #{User.inactive.count} Suspended: #{User.suspended.count}"
0, users, counts
end
# Each option maps to a lambda to be executed, with returns a 3-list of <status-code>, <users>, <counts>.
OPTIONS = {
'--active' => lambda do
users = User.active.alpha
counts = "Total: #{users.count}"
0, users, counts
end,
'--inactive' => lambda do
users = User.inactive.alpha
counts = "Total: #{users.count}"
0, users, counts
end,
'--suspended' => lambda do
users = User.suspended.alpha
counts = "Total: #{users.count}"
0, users, counts
end,
'--all' => ALL_OR_NIL,
nil => ALL_OR_NIL,
'--help' => lambda do
puts "Shows status of users in Travis Enterpise installation"
puts "--all or no arguments shows all users and their status"
puts "--active shows active users"
puts "--inactive shows inactive users"
puts "--suspended shows suspennded users (suspended using `travis suspend [username]` command)"
1, nil, nil
end
}
# The else clause here replaced by a default lambda. It returns exit code 1, which will be used for the exit call at the
# end.
OPTIONS_DEFAULT = lambda do
puts "Option #{option} not recognised."
1, nil, nil
end
status, users, counts = OPTIONS.fetch(option, OPTIONS_DEFAULT)[]
# Here we guard against a nil value for users.
USERS = {
nil => ->{}
}
# Here the default is the lambda to cover the case when we have users.
USERS_DEFAULT = lambda do
col_width = users.pluck(:login).map(&:length).max
users.each do |user|
puts [user.login.ljust(col_width), user.enterprise_status.ljust(col_width)].join(' ' * 5)
end
puts counts
end
# Here we map from exit code, to a lambda which returns an exit code.
FINAL = {
1 => ->{ 1 }
}
# Only in the case in which we didn't previously want to exit, we may do additional work; 0 (success) is returned.
FINAL_DEFAULT = ->{ USERS.fetch(users, USERS_DEFAULT)[] }
FINAL.fetch(status, FINAL_DEFAULT)[]
exit status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment