Stop de-Railing apps with leaky MVC abstractions: Trailblazer for better OO abstractions in Rails apps
We’ve been noticing that our controllers and models are straying from the principle of single responsibility. Our controllers bloat, and we transfer the bloat to our models in response. We recently discovered a gem that provides some patterns to escape from this trap.
Trailblazer is a set of object-oriented patterns that augment Rails’s MVC. You can find it athttps://github.com/apotonick/trailblazer. Trailblazer is a high-level , object-oriented architecture for web applications that offers additional structures for business logic, views, authentication, and authorization.
Trailblazer offers a set of robust, interactive components, but its patterns can be used in isolation from one another. You can introduce only parts of this framework into your Rails app if you so desire.
The rationale behind Trailblazer is that unaware Rails programmers can often fall into patterns that violate the principles behind MVC. It provides additional structures for encapsulation and division of responsibilities. For example, controllers should be lean HTTP endpoints for flow control. A controller shouldn't really know about the underlying domain model or business logic informing the app.
The model shouldn't be responsible for validations or complicated business logic, but should instead represent underlying entities.
Trailblazer fills in some of those "missing" layers on top of the basic MVC stack. It divests models and controllers of additional responsibilities, simplifying them and making them easier to maintain.
It adds new abstraction layers like operations, form objects, authorization policies, data twins and view models.
Views, policies, and models are grouped under a packaging principles called “concepts” if they are operating and on and displaying related data. You can get a feeling for this encapsulation principle below.
Trailblazer focuses on structure. It helps re-organize existing code into smaller components where different concerns are handled in separate classes.
Business logic is encapsulated into operations, the fundamental and pivotal element in Trailblazer. Operations decouple your application code from the framework.
Have a look at the diagram's left-hand side. Each request will handle:
Post-processing, aka callbacks
Presentation, rendering a response
On the right-hand side, you can see how Trailblazer introduces exciting new abstraction layers. Layers are implemented as objects. Each object handles only one specific aspect, minimizing the responsibility per layer.
A vertical authorization layer allows authorization or validation policies at every point in your code, i.e. controller, model, or other object authorization.
Enough philosophy, let’s take this out for a ride:
We’ll build a simple blog. First, run
rails new Blog
Add the following gems to your gemfile.
Now it’s time to start building your app. You’re going to create a controller as you would in Rails, but you’ll extract the business logic into operations when you do that. Operations are invocable objects like procs, which can be invoked with run or call methods. This means that for every feature of your app (such as create, update etc.), you will write an operation class which is then attached to a framework’s endpoint.
In your controller:
rails g controller comments :-
class CommentsController < ApplicationController
# you still have to take care of rendering.
Each operation has only one responsibility and exposes only one public method: ‘call’. Ruby automatically invokes the call method when the method name is omitted.
You can pass whatever you want to the operation, such as parameters as received by the controller. The operation expects a hash as its input. Each function is implemented with one public operation. For every feature of your app (like create, update etc.), you will write an operation class which is then hooked to the framework’s endpoint.
Now let’s create an operation. Create an operation.rb file at app/concepts/comment/operation.rb. The Comment::Create operation is a class taking care of the process of creating, validating, and persisting a comment.
Here’s what that looks like:
class Comment < ActiveRecord::Base
class Create < Trailblazer::Operation
Model Comment, :create
Validates :body, presence: true
Every operation must implement the process method. You will embed business logic and coordinate with other business processes here.
The Operation class offers you the validate method to deserialize and validate incoming data. To do so, the operation uses a form object, also known as a contract in Trailblazer.
Contracts and Validations
You can define contracts inline using the declarative contract class method. A contract is a Reform class, which allows you to specify the fields of the input and validations specific to that operation. (Seehttp://trailblazer.to/gems/reform )
The operation’s validation first instantiates the contract. Then, the incoming data is written to the contract object, and afterwards, validation of the entire object graph is performed using the Reform API.
An important fact here is that the contract is an intermediate object. Instead of writing input to the model, this all happens within the contract. The model is not accessed at all for validation, e.g.
Validates :body, presence: true
Persistence of data:
After having validated data, to persist it, a model object is needed.
model Comment, :create # will assign @model = Comment.new
Now push the data to the model and save it. This, again, happens using the contract’s API.
The model is now populated with the validated data and saved to the database.
If you’d like to perform operations after saving a model, instead of polluting the model or controller with this code, callbacks can take place through an operation.
class Create < Trailblazer::Operation
Builders behave like Policy objects, but provide reusable code for polymorphic associations.
For example, when you have polymorphic models and operations, such as a Car::Create and a Bike::Create, and a Vehicle::Create operation that contains generic code, you can dispatch to this code during instantiation.
You can use “builds” blocks to decouple object instantiation from operations.
builds -> (model, policy, params)
return admin if policy.admin?
return signedIn if params[:current_user]
Use policies to authorize users. They can be attached to operations and prevent the operation from running its #process method by raising an exception if the policy rule returns false.
Store this class at app/concepts/chapter/policy.rb.
def initialize(user, post)
@user, @post = user, post
Use ::policy to connect the policy class along with a query action to your operation.
policy Chapter::Policy, :create?
It is a great way to protect your operations from unauthorized users.This will raise a Trailblazer::NotAuthorizedError.
Instead of using policies, you can also use a simple guard. A guard is like an inline policy that doesn’t require you to define a policy class.
Policy do |params|
Params[:current_user].present? #true when present
As you can see, Trailblazer helps you encapsulate logic in ways that a bare metal MVC framework may not. You can see a full sample app at https://github.com/tejaswinic-webonise/blog. Enjoy.