Over the last couple of week’s we’ve been looking at some of the basic concepts that go into building a Ruby application using Trailblazer.

A technique that is promoted heavily in the Trailblazer book is inheritance. Inheritance is of course one of the basic building blocks of Ruby (Understanding Inheritance in Ruby), and so you are probably already familiar with how it can be used (and abused!).

Inheritance is actually used in a number of Trailblazer components, but in today’s tutorial we are going to be looking at Inheritance in the context of Operations.

What is Operation Inheritance

Before we get into this article, lets first make it clear what we mean by Operation Inheritance.

As with normal inheritance in Ruby, when you inherit from an existing Operation class, the child class will have access to the methods and properties of it’s parent.

You can then modify anything in the child class that is different, or specialised from the parent.

For example, you might have an Article::Create Operation that defines certain business rules and methods.

You might also have an Article::Update Operation that has mostly the same rules and methods, but with a couple of slight adjustments.

In this case, instead of repeating that logic, you could inherit from the Article::Create and make the adjustments in the Article::Update.

This would ensure that any changes to the business rules in the future would not leave discrepancies between the two actions.

Of course, inheritance is often a really bad design decision, and so the choice to use inheritance is not always this clear. You should be confident that inheritance is the correct design decision, and not just a way of reducing duplication.

The problem we’re going to be solving

I typically don’t use inheritance that often in my day-to-day programming as I find it often causes more hassle than it’s worth (Understanding Inheritance in Ruby).

However, under certain circumstances, it is most definitely the correct decision.

In today’s tutorial I’m going to using Operation inheritance to deal with the different ways a user can be added to the application.

Firstly I’m going to need to import users from an existing system where I only know the user’s email address. These users will not have a username or password, and they will need to be confirmed by the user.

Secondly I’m going to need a way to create confirmed users so they don’t have to go through the process of confirming their email address. This will allow me to seed the database with users initially, and it will be useful as a factory in my tests.

And thirdly, I need to write the Operation that will be used to create new users once the application is live. This Operation will allow the user to add their username, email address, and password, and then trigger an email confirmation so they can confirm their email address.

So we need 3 different variations of the User::Create Operation. The rules are largely the same, but there are small variations between the versions. As each implementation is essentially a specialised version, this is a perfect opportunity to use inheritance.

Setting up the project

In this tutorial I’m going to be using Rails and Minitest. Trailblazer is decoupled from Rails, and you can use whatever testing framework you want, it doesn’t really matter.

So the first thing I’m going to do is to create a new Rails project. I’ve covered how to do this a number of times, but if you’re unsure, take a look at Getting started with Trailblazer and Ruby on Rails.

Next we need to create the User model, migration, and test file:

bin/rails g model User email:string username:string password_digest:string confirmed_at:timestamp imported_at:timestamp  
[/bash]

As you can see this is a fairly typical user database table with the email, username, and password digest columns.

I’m also including the `confirmed_at` timestamps to show confirmed users, and the `imported_at` timestamp for imported users.


## Defining the Contract

The first thing I’m going to do is to define the Contract for the Operations. As we saw in [What are Trailblazer Contracts?](http://culttt.com/2016/05/18/what-are-trailblazer-contracts), in this example I’m going to create the Contract as a standalone class as I feel it makes understanding the validation rules of these Operations easier.

Create a new file called `contract.rb` under your `user` concept directory. I’m splitting my Operation classes into a further `create` namespace, but you don’t have to if you don’t want to:  
```ruby  
require "reform/form/validation/unique_validator.rb"

module User::Create::Contract  
class Base < Reform::Form  
model User  
property :email  
property :username  
property :password, virtual: true

validates :email, presence: true, unique: true, email: true  
validates :username, username: true, unique: true, allow_blank: true  
validates :password, presence: true, length: { minimum: 8 }  
end

class Confirmed < Base  
validates :username, presence: true  
end

class Imported < Base; end

class Default < Base  
validates :username, presence: true  
end  
end  

First I define a Base class that holds the majority of the properties and validation rules.

I then create three child classes for Confirmed, Imported, and Default and then I add any additional validation rules that are required for those specialised instances.

In this case I simply require the presence of the username in the Confirmed and Default instances, and leave it not required in the Imported instance.

Building the Operation

Next up we need to build out the Operation classes for each of the different ways a new user can be created in the application.

Create a new file called operation.rb in the same concept directory as before:

require "bcrypt"

module User::Create::Operation  
end  

Again, I’m using an extra Create namespace, for this particular application, but that is not required so you don’t have to include it if you don’t want to.

First up I’ll add the Base class that acts as the abstract class that will be extended from. This class includes a couple of methods we will need in the child classes:

module User::Create::Operation  
class Base < Trailblazer::Operation  
include Model; model User, :create

def generate_digest  
model.password_digest = BCrypt::Password.create(contract.password)  
end

def generate_token  
model.confirmation_token = SecureRandom.urlsafe_base64  
end

def process(params)  
raise NotImplementedError, "User::Create::Operation::Base is an abstract class"  
end  
end  
end  

Ruby does not have the concept of abstract classes and so we can simply throw a NotImplementedError exception if this class is used as a concrete class. Using Ruby Exceptions.

As you can see, I’ve defined a couple of methods for generating digests and tokens.

Next up I will add the Confirmed implementation:

class Confirmed < Base  
contract User::Create::Contract::Confirmed

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
set_confirmed_timestamp  
f.save  
end  
end

def set_confirmed_timestamp  
model.confirmed_at = Time.now  
end  
end  

First I set the User::Create::Contract::Confirmed contract from earlier.

Next, I define the process method. In this method I validate that the incoming parameters are valid, and then I generate the password digest and I set the confirmed timestamp before saving the properties to the model.

Next up I will add the Imported implementation:

class Imported < Base  
contract User::Create::Contract::Imported

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
generate_token  
set_imported_timestamp  
f.save  
end  
end

def set_imported_timestamp  
model.imported_at = Time.now  
end  
end  

Again I will first set the Contract on this Operation.

This time, inside the process method I will validate the incoming parameters, generate the password digest, generate the confirmation token, and then I will set the imported timestamp to show that this was an imported user.

When importing users I’m going to generate a random password so all users do actually have a password in the database.

And finally I will add the Default implementation:

class Default < Base  
contract User::Create::Contract::Default

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
generate_token  
f.save  
send_confirmtion_email  
end  
end

def send_confirmtion_email  
UserMailer.confirm_membership(model).deliver_later  
end  
end  

Once again I first set the correct Contract on the Operation class.

This time inside of the process method I validate the incoming parameters, generate the password digest, generate the confirmation token, and then I save the new user to the database.

Once the new user is created I will send the confirmation email as an after save callback.

Here is this class in full:

require "bcrypt"

module User::Create::Operation  
class Base < Trailblazer::Operation  
include Model; model User, :create

def generate_digest  
model.password_digest = BCrypt::Password.create(contract.password)  
end

def generate_token  
model.confirmation_token = SecureRandom.urlsafe_base64  
end

def process(params)  
raise NotImplementedError, "User::Create::Operation::Base is an abstract class"  
end  
end

class Confirmed < Base  
contract User::Create::Contract::Confirmed

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
set_confirmed_timestamp  
f.save  
end  
end

def set_confirmed_timestamp  
model.confirmed_at = Time.now  
end  
end

class Imported < Base  
contract User::Create::Contract::Imported

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
generate_token  
set_imported_timestamp  
f.save  
end  
end

def set_imported_timestamp  
model.imported_at = Time.now  
end  
end

class Default < Base  
contract User::Create::Contract::Default

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
generate_token  
f.save  
send_confirmtion_email  
end  
end

def send_confirmtion_email  
# Send email  
end  
end  
end  

Writing the tests

With all of the Operation implementations in place I can now write some tests to make sure everything is working as it should.

I know this isn’t strictly TDD. I actually did use TDD to get to this point when I initially wrote this code, but I thought it would be easier to understand if I didn’t write this tutorial using TDD.

First up I will ensure that the Base class throws a NotImplementedError exception when it is run:

require "test_helper"

module User::Create::OperationTest  
class BaseTest < ActiveSupport::TestCase  
test "throw NotImplementedError exception" do  
assert_raises NotImplementedError do  
User::Create::Operation::Base.run({})  
end  
end  
end  
end  

Strictly speaking, you don’t really need this type of test, but I thought I would include it because it shows that “abstract” class technique of throw exceptions for methods that should be implemented in child classes.

Next up we have the Confirmed implementation tests:

class ConfirmedTest < ActiveSupport::TestCase  
def setup  
@user = User::Create::Operation::Confirmed.(user: attributes_for(:user)).model  
end

test "require presence of email username and password" do  
res, op = User::Create::Operation::Confirmed.run(user: {})

assert_not(res)  
assert_includes(op.errors[:email], "can’t be blank")  
assert_includes(op.errors[:username], "can’t be blank")  
assert_includes(op.errors[:password], "can’t be blank")  
end

test "email should be a valid email" do  
res, op = User::Create::Operation::Confirmed.run(user: {email: "invalid email"})

assert_not(res)  
assert_includes(op.errors[:email], "is invalid")  
end

test "email should be unique" do  
res, op = User::Create::Operation::Confirmed.run(user: {email: @user.email})

assert_not(res)  
assert_includes(op.errors[:email], "has already been taken")  
end

test "username should be a valid username" do  
res, op = User::Create::Operation::Confirmed.run(user: {username: "invalid username"})

assert_not(res)  
assert_includes(op.errors[:username], "is invalid")  
end

test "username should be unique" do  
res, op = User::Create::Operation::Confirmed.run(user: {username: @user.username})

assert_not(res)  
assert_includes(op.errors[:username], "has already been taken")  
end

test "password should be greater than 8 characters" do  
res, op = User::Create::Operation::Confirmed.run(user: {password: "abc"})

assert_not(res)  
assert_includes(op.errors[:password], "is too short (minimum is 8 characters)")  
end

test "create confirmed user" do  
res, op = User::Create::Operation::Confirmed.run(user: attributes_for(:user))

assert(res)  
assert(op.model.confirmed?)  
end  
end  

I’ll not go through each test as we’ve covered this kind of testing a number of times now and so it should be fairly easy to see what’s going on in each test.

A couple of things I will note.

First I’m seeding a user in the database in the setup method using the Operation itself.

I’m using attributes_for(:user) from factory girl Replacing Fixtures with Factory Girl in Ruby on Rails just as a way of generating hashes of correct data when creating users. That’s the only bit of factory girl I’m using, I’m not actually using the factories.

And I’ve added a confirmed? method to the User model for checking whether a user is confirmed or not.

Next up we have the Imported implementation tests:

class ImportedTest < ActiveSupport::TestCase  
def setup  
@user = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end

test "require presence of email and password" do  
res, op = User::Create::Operation::Imported.run(user: {})

assert_not(res)  
assert_includes(op.errors[:email], "can’t be blank")  
assert_includes(op.errors[:password], "can’t be blank")  
end

test "email should be a valid email" do  
res, op = User::Create::Operation::Imported.run(user: {email: "invalid email"})

assert_not(res)  
assert_includes(op.errors[:email], "is invalid")  
end

test "email should be unique" do  
res, op = User::Create::Operation::Imported.run(user: {email: @user.email})

assert_not(res)  
assert_includes(op.errors[:email], "has already been taken")  
end

test "password should be greater than 8 characters" do  
res, op = User::Create::Operation::Imported.run(user: {password: "abc"})

assert_not(res)  
assert_includes(op.errors[:password], "is too short (minimum is 8 characters)")  
end

test "create imported user" do  
res, op = User::Create::Operation::Imported.run(user: attributes_for(:imported_user))

assert(res)  
assert_not(op.model.confirmed?)  
assert(op.model.imported?)  
end  
end  

And finally we have the Default implementation tests:

class DefaultTest < ActiveSupport::TestCase  
def setup  
@user = User::Create::Operation::Confirmed.(user: attributes_for(:user)).model  
end

test "require presence of email username and password" do  
res, op = User::Create::Operation::Default.run(user: {})

assert_not(res)  
assert_includes(op.errors[:email], "can’t be blank")  
assert_includes(op.errors[:username], "can’t be blank")  
assert_includes(op.errors[:password], "can’t be blank")  
end

test "email should be a valid email" do  
res, op = User::Create::Operation::Default.run(user: {email: "invalid email"})

assert_not(res)  
assert_includes(op.errors[:email], "is invalid")  
end

test "email should be unique" do  
res, op = User::Create::Operation::Default.run(user: {email: @user.email})

assert_not(res)  
assert_includes(op.errors[:email], "has already been taken")  
end

test "username should be a valid username" do  
res, op = User::Create::Operation::Default.run(user: {username: "invalid username"})

assert_not(res)  
assert_includes(op.errors[:username], "is invalid")  
end

test "username should be unique" do  
res, op = User::Create::Operation::Default.run(user: {username: @user.username})

assert_not(res)  
assert_includes(op.errors[:username], "has already been taken")  
end

test "password should be greater than 8 characters" do  
res, op = User::Create::Operation::Default.run(user: {password: "abc"})

assert_not(res)  
assert_includes(op.errors[:password], "is too short (minimum is 8 characters)")  
end

test "create default user" do  
res, op = User::Create::Operation::Default.run(user: attributes_for(:user))

assert(res)  
assert_not(op.model.confirmed?)  
assert_not(op.model.imported?)  
end  
end  

If you run all of those tests you should see them pass.

Conclusion

So hopefully this was a good illustration of how to use Trailblazer Operations and inheritance.

In today’s tutorial I required the functionality to create users in three different ways. This is the perfect opportunity to use inheritance because we can say that each method is a specialisation of the create user process.

In this example we benefit from being able to reuse the methods to create the password digest and create a confirmation token. But probably more important is the semantic importance of understanding the relationship between the three classes.

As I mentioned in the introduction to this post, I usually tend to avoid inheritance as I find there is often a much better solution.

But inheritance does have a time and a place, and hopefully as you can see from today’s tutorial, it makes our code a lot better when used correctly.