Last active
March 21, 2019 04:51
-
-
Save timfenney/a7017cc47b61fcbb1eec5eb8f2cbdf51 to your computer and use it in GitHub Desktop.
Avoiding conditionals and early return in Ruby...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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