Rails procedure design pattern

Have you ever came across a code that verifies a lot of conditions to allow for some action? In normal life, we would name such process as a procedure. Programming is no different. Let's have a quick look at the procedure definition:

The definition of procedure is order of the steps to be taken to make something happen, or how something is done. An example of a procedure is cracking eggs into a bowl and beating them before scrambling them in a pan

Knowing the definition, we can consider a simple example of a procedure in a typical Ruby on Rails application. We would like to allow a user to download a report with some useful information but only if he meets the following criteria:

  • is more than 17 years old (we would use User#age column)
  • created his account, not more than a year ago (we would use User#created_at column)
  • his name is Tom or John (we would use User#first_name column)

We can use the following simple class to verify if a given user can download our imagined report:

class UserReportDownloadPolicy
  def initialize(user)
    @user = user
  end

  def eligible?
    adult? && created_account_in_last_year? && is_tom_or_john?
  end

  private

  def adult?
    @user.age > 17
  end

  def created_account_in_last_year?
    @user.created_at >= 1.year.ago
  end

  def is_tom_or_john?
    %w[john tom].include?(@user.first_name.downcase)
  end
end

The class, defined above, itself is quite readable and the usage is pretty simple. It's a typical representation of the policy object pattern where the main goal of the class is to return boolean value and do not perform any complex action besides comparing and checking the values.

Now imagine that we would like to tell the user why he is not able to pull the report. We would have to rebuild our policy object and create some class that would check exactly which step of verification failed and prepare a proper message. This is the perfect scenario where the procedure design pattern can be used.

Installation

Yes, there is a gem for that. You could implement this design pattern on your own but it will be faster and easier to understand the idea by using something that will speed up the development.

bundle add procedure

The above command will add the gem to your Gemfile and install it.

The structure of a procedure

The procedure has its name and it should consist of two or more steps (otherwise a policy object should be enough for checking only one step). Let's consider the example we are already familiarized with - the procedure of checking if a given user can download a report.

We would create a class named UsersReport::DownloadProcedure that will be a wrapper for the following steps:

  • UsersReport::Steps::VerifyAge
  • UsersReport::Steps::VerifyAccountCreationTime
  • UsersReport::Steps::VerifyName

A few rules were used here:

  • The main class of the procedure ends with Procedure and the class name along with the namespace should be enough to tell for what the given procedure can be used. In our case, it's a procedure for checking if the report can be downloaded by the given user
  • Step classes are inside the Steps namespace to separate the procedure classes from steps. Single step can be also used in multiple procedures at the same time
  • It is a good practice to put procedure logic to app/procedures directory

As we are familiarized with the structure and rules, we can begin coding our procedure.

Building the procedure

Let's start with building steps so we can define them later in the procedure class and make the test calls.

Building procedure steps

Verifying user's age

Create new file called app/procedures/users_report/steps/verify_age.rb with the following code:

module UsersReport
  module Steps
    class VerifyAge
      include Procedure::Step

      def passed?
        context.user.age > 17
      end

      def failure_message
        'you should be more than 17 years old'
      end
    end
  end
end

We had to include Procedure::Step module to make our class a step class that can be used later in the procedure class. We also used the context variable which contains all the data passed to the procedure.

You can also use the step class as a simple policy object:

User = Struct.new(:age)
user = User.new(18)

UsersReport::Steps::VerifyAge.passed?(user: user) # => true

Verifying account creation time

Create new file called app/procedures/users_report/steps/verify_account_creation_time.rb with the following code:

module UsersReport
  module Steps
    class VerifyAccountCreationTime
      include Procedure::Step

      def passed?
        context.user.created_at >= 1.year.ago
      end

      def failure_message
        'your account should be created in the last year'
      end
    end
  end
end

Verifying the candidate name

Create new file called app/procedures/users_report/steps/verify_name.rb with the following code:

module UsersReport
  module Steps
    class VerifyName
      include Procedure::Step

      def passed?
        %w[john tom].include?(context.user.first_name.downcase)
      end

      def failure_message
        'your name should be John or Tom'
      end
    end
  end
end

Building the procedure class

We have our steps defined so it's time to build the main procedure class that will be called each time we would like to verify if a given user should be eligible to download the report.

Create new file called app/procedures/users_report/download_procedure.rb with the following code:

module UsersReport
  class DownloadProcedure
    include Procedure::Organizer

    steps UsersReport::Steps::VerifyAge,
          UsersReport::Steps::VerifyAccountCreationTime,
          UsersReport::Steps::VerifyName
  end
end

Our procedure is now ready. The class consists of two main things:

  • We extended the class by including Procedure::Organizer which provides the procedure functionality to the class
  • We defined steps by passing step classes. The order matter as the steps will be executed in the order they are defined. If the step VerifyAccountCreationTime won't pass, then VerifyName won't be called.

Playing with the procedure

At the beginning of the article, we build a simple policy object and noticed that it won't be easy to keep the code readable in one class if we would like to tell the user why he can't download the report. With our new procedure is pretty simple:

User = Struct.new(:age, :created_at, :first_name, keyword_init: true)
user = User.new(age: 19, created_at: 6.months.ago, first_name: "Tim")

outcome = UsersReport::DownloadProcedure.call(user: user)
outcome.failure_message # => your name should be John or Tom

The above user is not eligible to download the report because his name is Tim and we accept only John or Tom. If you would like to check if the procedure was successfully verified you can use #success? or #failure? methods:

outcome.success? # => false
outcome.failure? # => true

Everything you would pass to the .call method of the procedure is available via context variable in both passed? and failure_message method so you can prepare even more meaningful failure messages like "Your name is Tim and we accept only John or Tom".

Summary

The procedure design pattern is a good choice for complex verification processes where multiple steps need to be checked and meaningful feedback message should be returned but it's an overkill if you need to verify just one or two simple conditions.

Let's recall what we have learned in this article:

  • The procedure design pattern is a combination of the policy object and interactor patterns. It allows for performing complex validations and provides meaningful feedback message when the verification was not successful
  • The procedure consists of the procedure class and step classes where each step is a single class that implements the passed? method and it's executed one after one unless the step before was not verified successfully.
  • Each step class can be used standalone as a simple policy object

The source code of the procedure gem is available on Github