'Rubocop Metrics/CyclomaticComplexity in case when
Metrics/CyclomaticComplexity: Cyclomatic complexity for notification_path is too high. [9/8] And I don't understand how to fix that. Can you help me, someone? Thanks
def notification_path
return h.company_trial_activation_index_path if User.current.company.trial_pending?
dot = object.eventable
case object.event.to_sym
when :new_app, :updates_approved, :updates_disapproved, :new_partial_app, :completed_app, :received_lead
h.company_dot_application_path(id: dot.id)
when :bg_check_received, :bg_check_failed, :bg_check_completed
h.company_dot_application_background_checks_path(dot)
when :voe_completed, :voe_corrected, :voe_invalid,
:voe_driving_school_completed, :voe_driving_school_corrected, :voe_driving_school_invalid
h.company_dot_application_applications_requests_path(dot_application_id: dot.id)
when :voe_instructions, :voe_driving_school_instructions
h.company_dot_application_applications_requests_path(dot.id)
when :email_reply, :note, :sms_reply
h.company_dot_application_communications_path(dot.id)
when :follow_up_task, :follow_up_task_on_due_date
h.company_dot_application_follow_up_tasks_path(dot_application_id: dot.id)
when :bulk_actions_background_location, :bulk_actions_background_assign_user, :bulk_actions_background_lead_source
h.home_users_path
else
h.home_users_path
end
end
Solution 1:[1]
Before we get to cyclomatic complexity, let's address two bigger problems with that function. The giant lists of arrays, and the globals. Because the purpose of Rubocop is to make your code easier to read and maintain and reduce the possibility of bugs, not ticking things off a list.
Things would be a lot more readable if the symbol lists were named.
WHATEVER_THESE_ARE = [
:new_app, :updates_approved, :updates_disapproved,
:new_partial_app, :completed_app, :received_lead
].freeze
BG_CHECK_DONE = [
:bg_check_received, :bg_check_failed, :bg_check_completed
].freeze
...and so on...
def notification_path
return h.company_trial_activation_index_path if User.current.company.trial_pending?
dot = object.eventable
case object.event.to_sym
when WHATEVER_THESE_ARE
h.company_dot_application_path(id: dot.id)
when BG_CHECK_DONE
h.company_dot_application_background_checks_path(dot)
...and so on...
end
end
Then notice that your function takes no arguments. It uses two globals, the vaguely named object and h. I don't know what these really are, the names don't help, but maybe you sanitized them for the question. Using them as globals makes the code more complex. They should be passed in as parameters, or at least descriptively named.
def notification_path(better_name_for_h, better_name_for_object)
...
end
Ok, on to cyclonic complexity. A case is basically a big if/elsif/else with each when adding a point of complexity. With a table this large it seems like there isn't much we can do, but let's start by observing that the case only depends on object.event, object.eventable (event table? why is it named dot?), and h. We can do an extract method.
def notification_path(h, object)
return h.company_trial_activation_index_path if User.current.company.trial_pending?
dot_to_application_path(object.event, object.eventable, h)
end
def dot_to_application_path(event, dot, h)
case event.to_sym
when WHATEVER_THESE_ARE
h.company_dot_application_path(id: dot.id)
when BG_CHECK_DONE
h.company_dot_application_background_checks_path(dot)
...
else
h.home_users_path
end
end
That cut off a grand total of 1 giving us 8. Others have observed the final condition is redundant, so cut that off giving 7. Sometimes you just know better than Rubocop that the logic really is this complex and can add an exception.
# rubocop:disable Metrics/CyclonicComplexity
def dot_to_application_path(event, dot, h)
This is a last resort. It's always worth considering Rubocop's complaints. We've already improved the code quite a bit by trying to fix the Rubocop issues.
Or we can dig further.
We can observe that this table is doing two things. It's selecting the appropriate path method and normalizing how it's called. Every path method takes the dot slightly differently and this makes things more complex and error prone.
If the methods were consistent we could replace the case with a hash lookup.
WHATEVER_THESE_ARE = [
:new_app, :updates_approved, :updates_disapproved,
:new_partial_app, :completed_app, :received_lead
].freeze
BG_CHECK_DONE = [
:bg_check_received, :bg_check_failed, :bg_check_completed
].freeze
...
# You'll want to put this somewhere else.
def flatten_keys(hash)
hash.each_with_object({}) { |(keys, value), new_hash|
keys.each { |key| new_hash[event] = value }
}
end
EVENT_TO_APPLICATION_PATH_METHOD = flatten_keys({
WHATEVER_THESE_ARE => :company_dot_application_path,
BG_CHECK_DONE => :company_dot_application_background_checks_path,
...
}).freeze
def notification_path(h, object)
return h.company_trial_activation_index_path if User.current.company.trial_pending?
method = EVENT_TO_DOT_APPLICATION_PATH_METHOD[object.event.to_sym] || :home_users_path
h.send(method, dot)
end
Now we can see how simple this code can be!
To accomplish you could refactor the methods so they are consistent, or you can write wrapper methods which make it consistent. Refactoring is the preferable option, but that might be well out of your scope.
Finally, this might all be complex enough to warrant putting all the logic into a class.
Solution 2:[2]
I noticed that you use
h.company_dot_application_applications_requests_path(dot_application_id: dot.id)
in one place and
h.company_dot_application_applications_requests_path(dot.id)
in another. If these calls are equal, the method can be simplified a bit:
def notification_path
return h.company_trial_activation_index_path if User.current.company.trial_pending?
dot = object.eventable
case object.event.to_sym
when :new_app, :updates_approved, :updates_disapproved, :new_partial_app, :completed_app, :received_lead
h.company_dot_application_path(id: dot.id)
when :bg_check_received, :bg_check_failed, :bg_check_completed
h.company_dot_application_background_checks_path(dot)
when :voe_completed, :voe_corrected, :voe_invalid, :voe_driving_school_completed, :voe_driving_school_corrected, :voe_driving_school_invalid, :voe_instructions, :voe_driving_school_instructions
h.company_dot_application_applications_requests_path(dot.id)
when :email_reply, :note, :sms_reply
h.company_dot_application_communications_path(dot.id)
when :follow_up_task, :follow_up_task_on_due_date
h.company_dot_application_follow_up_tasks_path(dot_application_id: dot.id)
else
h.home_users_path
end
end
Solution 3:[3]
Just a comment to say that I really disagree with @Schwern answer.
The purpose of this rubocop rule is to improve readability, because a high cyclomatic complexity is often an indication of bad readability (but not always).
I think your statements could be refactor and improved, but the switch-case pattern is here far more appropriate than meta-programming !
Meta-programming will obfuscate the business logic of that code and make thing harder to refactor later.
Against complexity, the key is simplicity, not more complexity :).
I don't have the whole context, but I have so clues :
- try to refactor your
whenconditions. Each one should correspond to a specific and clear business case company_dot_application_applications_requests_pathinterface does not seems to be optimal. Keep in mind that clear class and module interface (methods, Constants, etc...) is one key point of clean code- remove the last
when(useless)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Ilya Konyukhov |
| Solution 3 | Jerome Doucet |
