One of the benefits of programming in ruby is that it is easy to write short methods. This is helpful as it makes it easier to test different methods in isolation. Also, most importantly it's easier to read the class when the class is constructed of small methods. Often, when I get back to a program after a while, I may not have enough context, and writing short, focussed methods helps with this.
For example, in one of my Shopify apps, I let users add multiple filters to a workflow.
For example, when an order is created, I need to check the order with each of these two workflows conditions, and then add tags accordingly, if the check passes. For each of the conditions, I also need to check if the user added AND (all conditions must match), or OR (any condition must match), and then make a call to the Shopify API to get the data.
I like using service objects for handling the business logic for the app. The service object below is the one that checks if the workflow conditions passes or not.
class CheckWorkflowConditions attr_accessor :shop def initialize(shop:, workflow:, order_id:) @shop = shop @workflow = workflow @order_id = order_id end def passes? return false if no_filters? return true if any_conditions_filter? && atleast_one_filter_passes? return false if any_conditions_filter? && no_filters_passes? return false if all_conditions_filter? && atleast_one_filter_fails? return true if all_conditionss_filter? && all_filters_passes? end private def no_filters? @workflow.filters.empty? end def any_conditions_filter? !all_conditions_filter? end def all_conditions_filter? @workflow.all_conditions end def atleast_one_filter_passes? @filter_check_results =  @workflow.filters.each do |filter| perform_filter_check(filter) break if alteast_one_filter_has_passed end alteast_one_filter_has_passed end def no_filters_passes? !alteast_one_filter_has_passed end def atleast_one_filter_fails? return alteast_one_filter_has_failed if @filter_check_results.any? @workflow.filters.each do |filter| perform_filter_check(filter) break if alteast_one_filter_has_failed end alteast_one_filter_has_failed end def all_filters_passes? !alteast_one_filter_has_failed end def perform_filter_check(filter) @filter_check_results << CheckOrderWithFilter.new(shop: shop, order_id: @order_id, filter: filter).get_result end def alteast_one_filter_has_passed @filter_check_results.include?(true) end def alteast_one_filter_has_failed @filter_check_results.include?(false) end end
CheckWorkflowConditions service object has followed the composed method technique from Eloquent Ruby. That is:
- Each method does one single thing. The
no_filters?method only checks if filters are present? Similarly all the other methods in this class focusses only on doing one thing each.
- Each method operates at single conceptual level. The high level logic is not mixed with the details in any method.
- Each method has a name that specifies what it does. The method name kind of tells us what the method is trying to do.
The above service object also does not follow the advice that each method should only have one way out, so that all the logic results in a return at the bottom of the method. Although the
passes? method has multiple returns, because we have followed the composite technique, it is easy to read and understand. Also, the logic related to making the Shopify API calls has been delegated to a different service object:
CheckOrderWithFilter.new(shop: shop, order_id: @order_id, filter: filter).get_result. This makes the whole class more focussed on just checking if the workflow conditions pass or fail, making it easier to test.
Writing (and reading) ruby code becomes more enjoyable when the class is made of methods that are short, focussed.
If you are looking for a senior Rails developer to join your team, do send me an email to firstname.lastname@example.org. I took 6 months off as I became a new dad, but am currently open to new Ruby/Rails related work.