Today I'm going to review and refactor some code from Ninefold CLI. I picked a method I could make better. Ninefold is a hosting company and pick_app is apparently responsible for selecting an app for CLI to work with.
| def pick_app(&block) | |
| require_user | |
| if app = app_from_dot_ninefold_file || app_from_env_id | |
| block.call app | |
| else | |
| load_apps do |apps| | |
| if apps.count > 1 | |
| if app = interaction(:pickapp, apps) | |
| save_app_in_dot_ninefold_file(app) | |
| block.call app | |
| end | |
| elsif apps.count == 1 | |
| say "▸ You have only one app (#{apps[0].name}) proceeding...", :yellow | |
| block.call apps.first | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| end | |
| end |
What's good about this code:
- Long method names like
app_from_dot_ninefold_file. - Easy to understand what the method does.
Step 1
On line 4, if app was specified in a dotfile or environment variable, we use that app. I think that there's no need to go into detail here about how exactly app was specified, so I extract method user_specified_app and end up with this:
| if app = user_specified_app | |
| block.call app | |
| ... | |
| private | |
| def user_specified_app | |
| app_from_dot_ninefold_file || app_from_env_id | |
| end |
It is a bit easier to read now.
Step 2
pick_app method uses block.call everywhere, instead of yield for no obvious reason. yield is better in my opinion because:
- It's one word instead of two.
- You get syntax highlighting for it.
- It's a standard Ruby way of calling passed block, so it adheres to the Rule of Least Surprise.
So it goes from this:
| def pick_app(&block) | |
| require_user | |
| if app = user_specified_app | |
| block.call app | |
| else | |
| load_apps do |apps| | |
| if apps.count > 1 | |
| if app = interaction(:pickapp, apps) | |
| save_app_in_dot_ninefold_file(app) | |
| block.call app | |
| end | |
| elsif apps.count == 1 | |
| say "▸ You have only one app (#{apps[0].name}) proceeding...", :yellow | |
| block.call apps.first | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| end | |
| end |
To this:
| def pick_app | |
| require_user | |
| if app = user_specified_app | |
| yield app | |
| else | |
| load_apps do |apps| | |
| if apps.count > 1 | |
| if app = interaction(:pickapp, apps) | |
| save_app_in_dot_ninefold_file(app) | |
| yield app | |
| end | |
| elsif apps.count == 1 | |
| say "▸ You have only one app (#{apps[0].name}) proceeding...", :yellow | |
| yield apps.first | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| end | |
| end |
Step 3
At this point, lines 9-17 are still too complex. It's possible to understand what's going on, but it takes more effort than it should. So, I extract two more methods:
| def pick_app(&block) | |
| require_user | |
| if app = user_specified_app | |
| yield app | |
| else | |
| load_apps do |apps| | |
| if apps.count > 1 | |
| yield app if app = ask_user_to_specify_app(apps) | |
| elsif apps.count == 1 | |
| yield auto_select_the_only_app(apps) | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| end | |
| end | |
| private | |
| def ask_user_to_specify_app(apps) | |
| if app = interaction(:pickapp, apps) | |
| save_app_in_dot_ninefold_file(app) | |
| app | |
| end | |
| end | |
| def auto_select_the_only_app(apps) | |
| say "▸ You have only one app (#{apps[0].name}) proceeding...", :yellow | |
| apps.first | |
| end |
Step 4
I notice that yield is called in 3 different places and decide to DRY it up.
| def pick_app(&block) | |
| require_user | |
| app = user_specified_app || load_apps do |apps| | |
| if apps.count > 1 | |
| ask_user_to_specify_app(apps) | |
| elsif apps.count == 1 | |
| auto_select_the_only_app(apps) | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| yield app if app | |
| end |
Step 5
apps.count > 1 isn't particularly easy to read and can be replaced by app.many? from Rails' ActiveSupport. apps.count == 1 can be replaced by apps.one?, but it's not so straightforward as many? because it returns number of Enumerable elements that are truthy, not the same semantics as size of 1.
| def pick_app(&block) | |
| require_user | |
| app = user_specified_app || load_apps do |apps| | |
| if apps.many? | |
| ask_user_to_specify_app(apps) | |
| elsif apps.one? | |
| auto_select_the_only_app(apps) | |
| else | |
| puts "You don't have any apps" | |
| end | |
| end | |
| yield app if app | |
| end |
For further reading, the pull request conversation with a nice person from Ninefold.
Happy Hacking!