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 stepVerifyAccountCreationTime
won't pass, thenVerifyName
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