DEV Community

Cover image for Keeping my Ruby Class Methods small and focussed
Arjun Rajkumar
Arjun Rajkumar

Posted on

Keeping my Ruby Class Methods small and focussed

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 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

    @filter_check_results = []
  end

  def passes?
    return false if no_filters?

    return atleast_one_filter_passes? if any_conditions_filter?
    return all_filters_passes? if all_conditions_filter?
  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?
    @workflow.filters.each do |filter| 
      perform_filter_check(filter)
      break if includes_passed_filter?
    end
    includes_passed_filter?
  end

  def all_filters_passes?
    @workflow.filters.each do |filter| 
      perform_filter_check(filter)
      break if includes_failed_filter?
    end
    !includes_failed_filter?
  end

  def perform_filter_check(filter)
    @filter_check_results << CheckOrderWithFilter.new(shop: shop, order_id: @order_id, filter: filter).get_result
  end

  def includes_passed_filter?
    @filter_check_results.include?(true)
  end

  def includes_failed_filter?
    @filter_check_results.include?(false)
  end
end

Enter fullscreen mode Exit fullscreen mode

The CheckWorkflowConditions service object has followed the composed method technique from Eloquent Ruby. That is:

  1. 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.
  2. Each method operates at single conceptual level. The high level logic is not mixed with the details in any method.
  3. 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.

This post originally appeared on Weightless.

Top comments (0)