Trailblazer

Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation, an intuitive code structure and gives you an object-oriented architecture.

Gitter Chat TRB Newsletter Gem Version

Trailblazer In A Nutshell

  1. All business logic is encapsulated in operations (service objects).
    • An optional Reform form object in the operation deserializes and validates input. The form object can also be used for rendering.
    • An optional policy object blocks unauthorized users from running the operation.
    • Optional callback objects allow declaring post-processing logic.
  2. Controllers instantly delegate to an operation. No business code in controllers, only HTTP-specific logic.
  3. Models are persistence-only and solely define associations and scopes. No business code is to be found here. No validations, no callbacks.
  4. The presentation layer offers optional view models (Cells) and representers for document APIs.

Trailblazer is designed to handle different contexts like user roles by applying inheritance between and composing of operations, form objects, policies, representers and callbacks.

Wanna see some code? Jump right here!

Mission

While Trailblazer offers you abstraction layers for all aspects of Ruby On Rails, it does not missionize you. Wherever you want, you may fall back to the "Rails Way" with fat models, monolithic controllers, global helpers, etc. This is not a bad thing, but allows you to step-wise introduce Trailblazer's encapsulation in your app without having to rewrite it.

Trailblazer is all about structure. It helps re-organize existing code into smaller components where different concerns are handled in separated classes.

Again, you can pick which layers you want. Trailblazer doesn't impose technical implementations, it offers mature solutions for recurring problems in all types of Rails applications.

Trailblazer is no "complex web of objects and indirection". It solves many problems that have been around for years with a cleanly layered architecture. Only use what you like. And that's the bottom line.

Trailblazer Likes 'Em All

Since Trailblazer decouples the High-Level Stack from the framework, it runs with virtually any Ruby framework. We are constantly working on documenting how to do that.

  • Trailblazer with Rails Book | Repository
  • Trailblazer with Sinatra Guide | Repository
  • Trailblazer with Hanami - coming soon!
  • Trailblazer with Roda - coming soon!
  • Trailblazer with Grape - coming very soon!

A Concept-Driven OOP Framework

Trailblazer offers you a new, more intuitive file layout in Rails apps where you structure files by concepts.

app

Files, classes and views that logically belong to one concept are kept in one place. You are free to use additional namespaces within a concept. Trailblazer tries to keep it as simple as possible, though.

Architecture

Trailblazer extends the conventional MVC stack in Rails. Keep in mind that adding layers doesn't necessarily mean adding more code and complexity.

The opposite is the case: Controller, view and model become lean endpoints for HTTP, rendering and persistence. Redundant code gets eliminated by putting very little application code into the right layer.

The Trailblazer stack.

Routing

Trailblazer uses Rails routing to map URLs to controllers, because it works.

Rails.application.routes.draw do
  resources :comments
end

Controllers

Controllers are lean endpoints for HTTP. They do not contain any business logic. Actions immediately dispatch to an operation.

class CommentsController < ApplicationController
  def create
    run Comment::Create # Comment::Create is an operation class.
  end

The #run method invokes the operation. It allows you to run a conditional block of logic if the operation was successful.

class CommentsController < ApplicationController
  def create
    run Comment::Create do |op|
      return redirect_to(comment_path op.model) # success!
    end

    render :new # invalid. re-render form.
  end

Again, the controller only dispatchs to the operation and handles successful/invalid processing on the HTTP level. For instance by redirecting, setting flash messages, or signing in a user.

Learn more.

Operation

Operations encapsulate business logic and are the heart of a Trailblazer architecture.

Operations don't know about HTTP or the environment. You could use an operation in Rails, Hanami, or Roda, it wouldn't know. This makes them an ideal replacement for test factories.

An operation is not just a monolithic replacement for your business code. It's a simple orchestrator between the form object, models and your business code.

class Comment::Create < Trailblazer::Operation
  def process(params)
    # do whatever you feel like.
  end
end

Operations only need to implement #process which receives the params from the caller.

Learn more.

Validations

In Trailblazer, an operation (usually) has a form object which is simply a Reform::Form class. All the API documented in Reform can be applied and used.

The operation makes use of the form object using the #validate method.

class Comment::Create < Trailblazer::Operation
  contract do
    # this is a Reform::Form class!
    property :body, validates: {presence: true}
  end

  def process(params)
    @model = Comment.new

    validate(params[:comment], @model) do |f|
      f.save
    end
  end
end

The contract (aka form) is defined in the ::contract block. You can implement nested forms, default values, validations, and everything else Reform provides.

In the #process method you can define your business logic.

Learn more.

Callbacks

Post-processing logic (also known as callbacks) is configured in operations.

Callbacks can be defined in groups. They use the form object's state tracking to find out whether they should be run.

class Comment::Create < Trailblazer::Operation
  include Callback
  callback(:after_save) do
    on_change :markdownize_body! # this is only run when the form object has changed.
  end

Callbacks are never triggered automatically, you have to invoke them! This is called Imperative Callback.

class Comment::Create < Trailblazer::Operation
  include Callback
  def process(params)
    validate(params) do
      contract.save
      callback!(:after_save) # run markdownize_body!, but only if form changed.
    end
  end

  def markdownize_body!(comment)
    comment.body = Markdownize.(comment.body)
  end
end

No magical triggering of unwanted logic anymore, but explicit invocations where you want it.

Learn more.

Models

Models for persistence can be implemented using any ORM you fancy, for instance ActiveRecord or Datamapper.

In Trailblazer, models are completely empty. They solely contain associations and finders. No business logic is allowed in models.

class Comment < ActiveRecord::Base
  belongs_to :thing

  scope :latest, lambda { all.limit(9).order("id DESC") }
end

Only operations and views/cells can access models directly.

Policies

You can abort running an operation using a policy. "Pundit-style" policy classes define the rules.

class Comment::Policy
  def initialize(user, comment)
    @user, @comment = user, comment
  end

  def create?
    @user.admin?
  end
end

The rule is enabled via the ::policy call.

class Comment::Create < Trailblazer::Operation
  include Policy

  policy Comment::Policy, :create?

The policy is evaluated in #setup!, raises an exception if false and suppresses running #process.

Learn more.

Views

View rendering can happen using the controller as known from Rails. This is absolutely fine for simple views.

More complex UI logic happens in View Models as found in Cells. View models also replace helpers.

The operation's form object can be rendered in views, too.

class CommentsController < ApplicationController
  def new
    form Comment::Create # will assign the form object to @form.
  end

Since Reform objects can be passed to form builders, you can use the operation to render and process the form!

= simple_form_for @form do |f|
  = f.input :body

Representers

Operations can use representers from Roar to serialize and parse JSON and XML documents for APIs.

Representers can be inferred automatically from your contract, then may be refined, e.g. with hypermedia or a format like JSON-API.

class Comment::Create < Trailblazer::Operation
  representer do
    # inherited :body
    include Roar::JSON::HAL

    link(:self) { comment_path(represented.id) }
  end

The operation can then parse incoming JSON documents in validate and render a document via to_json.

Learn more.

Tests

In Trailblazer, you only have operation unit tests and integration smoke tests to test the operation/controller wiring.

Operations completely replace the need for leaky factories.

describe Comment::Update do
  let(:comment) { Comment::Create.(comment: {body: "[That](http://trailblazer.to)!"}) }

More

Trailblazer has many more architectural features such as

  • Polymorphic builders and operations
  • Inheritance and composition support
  • Polymorphic views

Check the project website and the book.

Installation

The obvious needs to be in your Gemfile.

gem "trailblazer"
gem "trailblazer-rails" # if you are in rails.
gem "cells"

Cells is not required per default! Add it if you use it, which is highly recommended.

The Book

Please buy it: Trailblazer - A new architecture for Rails.

The demo application implements what we discuss in the book.