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
- created his account, not more than a year ago (we would use
- his name is Tom or John (we would use
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.
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:
A few rules were used here:
- The main class of the procedure ends with
Procedureand 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
Stepsnamespace 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
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::Organizerwhich provides the procedure functionality to the class
- We defined
stepsby passing step classes. The order matter as the steps will be executed in the order they are defined. If the step
VerifyAccountCreationTimewon't pass, then
VerifyNamewon'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
outcome.success? # => false outcome.failure? # => true
Everything you would pass to the
.call method of the procedure is available via
context variable in both
failure_message method so you can prepare even more meaningful failure messages like "Your name is Tim and we accept only John or Tom".
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