May 11, 2016
Table of contents:
A couple of weeks ago I gave my review of Trailblazer. Trailblazer is a “framework” for managing the inevitable abstraction you will need to introduce to your Ruby application once it reaches a certain level of complexity.
One of the most important aspects of Trailblazer is that your Models and Controllers should be very thin. Controllers should be lean HTTP endpoints, and your Models should only deal with associations and querying.
Trailblazer introduces Operations, which are classes that encapsulate the business logic around a given action of your application.
In today’s tutorial we are going to start to explore the concept of Operations in Trailblazer.
Last week we looked at setting up a Trailblazer project with Ruby on Rails. If you missed that tutorial I would recommend going back and reading it before continuing with this tutorial.
Before we take a look at some code, lets first understand exactly what an Operation is.
An Operation is a class that encapsulates the business logic of a given action. For example, the action of creating a new user, or updating an existing user.
Operations hold the responsibility of mapping the incoming data, validation, creating the object graph, and saving to the database.
Although this seems like the Operation has a lot of responsibility, the Operation itself is actually just conducting these responsibilities to other classes. The Operation is simply a single interface to a collection of composed collaborators.
Typically the code that would normally be in your Model or your Controller would be moved into the Operation. Your Model will no longer be responsible for validation or callbacks, and your Controller will no longer be responsible for dealing with any business logic that spills outside of the Model.
In my opinion, one of the big benefits of Operations is that it makes it immediately obvious as to how the actions of the application are implemented.
In a Trailblazer application, if you wanted to see the process of creating a user, you would go to the user concept and look at the Operation. I find encapsulating actions in a single class makes understanding a new application much easier.
Secondly, testing a given action is also much easier.
When testing a given action of your application, you can very easily Unit Test the Operation. This makes it easy to test the various scenarios your application will find itself in, and it gives you a lot of confidence that your code is working correctly.
I find it easier to focus my tests on a given action, rather than as separate Model and Controller tests that try to cover every eventuality.
A third benefit of using Operations is that you can use them as factories in your tests. So whenever you wanted to create a new user in one of your tests, you could use an Operation, rather than something like Factory Girl.
This ensure there aren’t any weird discrepancies between your application and your test factories.
The first Operation we will look at will be for creating a new user in the application. This will be an easy place to begin with, but we’ll look at some of the interesting aspects of Operations along the way.
So the first thing we need to do is to create the User
model and migration using the Rails generator:
bin/rails g model User email:string password_digest:string confirmed_at:timestamp
This will create the model, migration, and the model unit test class.
The next thing we need to do is to create the user concept directory, and the Operation class.
As I mentioned in Getting started with Trailblazer and Ruby on Rails, in a Trailblazer application, classes are grouped by concept, rather than type.
So the first thing we need to create is a user
directory that sits under the concepts
directory.
Next we can create the operation.rb
file that will hold the Operation class.
The first action we will be looking at will be the user create action:
class User < ActiveRecord::Base
class Create < Trailblazer::Operation
include Model
model User, :create
end
end
As you can see, I’ve created the Create
class that extends the Trailblazer::Operation
class.
You will also notice that I’m nesting the Create
class inside of the User
Active Record class. This does not couple the Create
class to the User
class or to Active Record, it simply gives us the nice syntax of User::Create
. This provides the context of the action.
Next I’m including the Model
module, and I’m setting the model to User
, and the action to :create
This allows the Operation to know what type of model we’re working with, and the given action of this Operation. This simply means the Operation will instantiate the correct class automatically, so we don’t have to do that manually.
With the basics of the Operation class now set up, we can take a look at writing some tests.
As usual I’m going to be using Minitest for my tests. You can use whatever test framework you like, it doesn’t really make a difference:
require 'test_helper'
class UserCreateTest < ActiveSupport::TestCase
end
The first test I will write will be to ensure that the Operation requires the presence of the email
and the password
fields:
test 'require presence of email and password' do
res, op = User::Create.run(user: {})
assert_not(res)
assert_includes(op.errors[:email], "can't be blank")
assert_includes(op.errors[:password], "can't be blank")
end
The res
variable is a boolean that shows if the Operation ran successfully or not. The op
variable is the Operation itself.
In this example I’m asserting that the res
is false
and the errors
array on the Operation has been filled with the errors that I’m looking for.
Next I’m going to test to ensure that the email is valid:
test 'require valid email' do
res, op = User::Create.run(user: { email: 'invalid email' })
assert_not(res)
assert_includes(op.errors[:email], 'is invalid')
end
Next I will test to ensure that the email address must be unique:
test 'require unique email' do
User::Create.(user: { email: 'name@domain.com', password: 'password' })
res, op =
User::Create.run(user: { email: 'name@domain.com', password: 'password' })
assert_not(res)
assert_includes(op.errors[:email], 'has already been taken')
end
In this test I’m using the Operation as a factory to seed the database with a user and then I’m attempting to add a new user with an existing email address.
Although this is a simple example, it shows how you can use Operation classes instead of factories.
And finally I will assert that the Operation can create a new user successfully:
test 'create user' do
res, op =
User::Create.run(user: { email: 'name@domain.com', password: 'password' })
assert(res)
assert_instance_of(User, op.model)
end
Here I’m asserting that the response was true
and that the model
property of the Operation is an instance of User
. I should probably check to make sure that the User
has been persisted with the correct data, but you get the idea.
With these tests in place, we can now take a look at adding the functionality to the Operation to make them pass!
The first thing we need to do is to define the Operation contract. The contract defines the properties of the Operation and any validation rules.
require 'reform/form/validation/unique_validator.rb'
class User < ActiveRecord::Base
class Create < Trailblazer::Operation
include Model
model User, :create
contract do
property :email
property :password, virtual: true
validates :email, presence: true, email: true, unique: true
validates :password, presence: true
end
end
end
First I define the two properties for email
and password
. The password
property is virtual because the property of the User
model is password_digest
. The Operation will automatically sync the properties to the model, and so by defining the password
property as virtual, this property will not be added to the model.
Next I define the validation rules. If you’re familiar with Rails validation, this should look pretty familiar (Working with Validation in Ruby on Rails) I’m also using the valid_email gem to check for valid email addresses.
When the Operation is run, the process
method is called. So the next thing we need to do is to implement that method:
require 'reform/form/validation/unique_validator.rb'
class User < ActiveRecord::Base
class Create < Trailblazer::Operation
include Model
model User, :create
contract do
property :email
property :password, virtual: true
validates :email, presence: true, email: true, unique: true
validates :password, presence: true
end
def process(params)
validate(params[:user]) { |f| f.save }
end
end
end
First we validate that the params
that were passed in are valid. If the validation rules are satisfied we can take an action inside of the block. The block in this case will only be run if the validation is satisfied.
Inside of the block we can simply call the save
method that will sync the properties of the contract to the model and then save it to the database.
Notice how we didn’t have to do anything to sync the properties and we didn’t instantiate a new instance of User
, as the operation automatically knows how to do this.
The final thing that we have to do is to generate the digest for the user’s password:
require 'bcrypt'
require 'reform/form/validation/unique_validator.rb'
class User < ActiveRecord::Base
class Create < Trailblazer::Operation
include Model
model User, :create
contract do
property :email
property :password, virtual: true
validates :email, presence: true, email: true, unique: true
validates :password, presence: true
end
def process(params)
validate(params[:user]) do |f|
generate_digest
f.save
end
end
def generate_digest
model.password_digest = BCrypt::Password.create(contract.password)
end
end
end
Here I’ve defined a generate_digest
method that will create a new digest from the given password and set it on the model. This method is called inside of the block of the process
method, but before the model is saved.
You can think of this as a before_create
callback that you would typically see on an Active Record model.
But instead of lumping this functionality into the model, we have simply defined it in the Operation so there’s no chance of this being inadvertently executed.
If you run those tests from earlier you should see them all pass.
In today’s tutorial we have looked at a fairly simple example of creating a Trailblazer Operation, but we have touched upon some very important concepts.
An Operation encapsulates the business logic of a given action. This makes it really easy to understand a given action because all of the code is in one file, rather than distributed between Models, Controllers, and Service Objects.
At first glance, the Operation seems to have a lot of responsibility. But on closer inspection, we can see that the Operation is simply providing an interface to the underlying collaborators.
As we continue to explore Trailblazer we will look at a lot more of the interesting benefits of encapsulating your business logic in Operations.