Trailblazer: A New Architecture For Rails
Codementor Ruby on Rails expert mentor Nick Sutterer is a Rails contributor, gem author, and frequent conference speaker. He is the maintainer of popular gems such as cells, reform, and roar. Nick recently sat down with us during office hours to discuss the problems he sees with Rails architecture, in which he proposed his solution to those problems—Trailblazer.
The text below is a summary done by the Codementor team and may vary from the original video and if you see any issues, please let us know!
You can watch the video recording of his office hour or take a look at the a roundup of the session in this post.
What is Trailblazer?
Trailblazer is a framework on top of Rails to serve as architectural guidance. It’s like a mesh-up of all the gems I’ve done before with some new concepts like operation, which will model every high level domain action. A high level domain operation, as I call it, is something like updating a comment, deleting a user, getting a list of all the recent comments, and so on. In essence, it encompasses everything the user can do through the user interface or via the API.
For the last few years I’ve been hired from different companies to refactor messed up Rails applications, and one of the most common problems I’ve run into are legacy projects with huge models of PHP scripts put into a Ruby model, where all the domain, rendering, processing, validation and etcetera are put in the model. Another one I’ve run into are horrible view layers, where the partials weren’t encapsulated and therefore every partial had access to every variable, which ends up in a messy view layer.
The underlying cause of this problem is that there is no guidance in Rails architecture. For example, to me, using service objects is common sense, though for beginners it may be confusing. No one tells you where to use a service object and no one tells you how a service object looks like. Additionally, no one tells you about the API but if you read blogs, everyone says they use service objects and tells you to use one. This advice doesn’t help me at all, and I have a lot of years of experience. Many of my friends who just started Rails keep asking me where they’re supposed to use a service object, form, model, and etcetera, so what I’m trying with Trailblazer is to bring structure to Rails where it will serve like a conventional structure to make it clear to the user what every step is. (E.g. which step is an operation, this step requires a form, and so on.)
Additionally, I personally don’t want to work on actual models in the Rails. I want to work on models to trigger high level domain actions and have an operation or class to encapsulate the entire step.
How Trailblazer Works
Although there are other features like view components,
operation is the principle element of the Trailblazer. It works as the diagram above, in which you model this instead of pushing all of that functionality into one monolithic model. Therefore, you’ll have different operation passes for every step. Every operation has a contract, and the contract is a
reform object. So, every object will have the same entry point and will use the contract to validate incoming data. Additionally, the operation can also present the validation to the viewer or API, and after that has happened, your actual domain logic is random.
To give an example of how
operation works, let’s say you click on a delete button. What happens when you do that is you go to the control that knows a little bit of this action because it does authentication—if it’s valid it does one thing and if it’s not it does another thing. Instead of delegating to a model that has 10 lines of code delegating to different models and eventually ends up needing three layers of application code, the controller in the Trailblazer framework will delegate this delete action to a service object. You can have as many operations as you want.
In order to use an
operation in the controller, all I have to do is use the
run method, in which all I have as a class is
run. To make it obvious that this is an atomic set in your domain, you won’t be able to do anything else in this
def create @contract = Comment::Operation::Create.run(params[:comment]) do |contract| return redirect_to(contract.model) # success. end render :new # failure. re-render form. end
run method accepts a block which is only called when the
operation is valid, which I think is pretty nifty.
return redirect_to(contract.model) # success.
Is yielded when valid, so if you use
return in the block, you’re done—the controller action will finish and the request is done.
If the operation is not valid, the code will carry on and run into the render block
render :new # failure. re-render form. end
Which is the response for handling an invalid operation stage.
This will be a handy way of using the operation because you won’t need
elses. You’ll simply have to use the block for a valid state and a
return, and this will just jump out of the action or wherever you call it.
There are different ways to call an
operation, but this will cover 99% of the cases when you want to trigger domain logic in a controller. I call this the “flow usage”. When you call
run, a reform object, which I call a
contract, is internally instantiated. A contract is instantiated because every object is a contract per definition—you don’t have to contract every operation, but it’s kind of the convention.
Reform in Trailblazer
Reform is a pretty cool gem that validates data without touching the model, which allows me to define a field and the validation for the field, and I can use active models, unique validations and other things that are usually sitting in the model. However, it’s very explicit, so you’ll have to define the field(s), which can be nested, and you can also add methods to that
So, you can see in the code sample
class Comment::Operation::Create < Trailblazer::Operation class Contract < Reform::Form property :body validates :body, presence: true end def process(params) comment = Comment.new validate(params, comment) do |contract| contract.save # further after_save logic happens here end end end
class Comment::Operation::Create < Trailblazer::Operation class Contract < Reform::Form property :body validates :body, presence: true end
Of the operation covers the entire process of validation.
Therefore, when I call
create.run in the controller, it will jump into the process instant method. It’s the only method you’ll have to implement in an
operation, and in that method you can create whatever model(s) you need for that domain action step. Thus, if I want to create a new comment, I will call the built-in method validate as in the parameters and the model I created. This step will then instantiate the form object (e.g. validate), and then call the block with the form, which is valid. I’ll then save the model in this block:
contract.save # further after_save logic happens here
Of course, you can call other operations. It’s up to you to decide what happens after the form is found valid.
Personally, I think having this convention of putting domain logic in operations is a big step forward, because people using Trailblazer have a problem, they will talk about operations instead of a model in the depths of somewhere. So, the cool thing about operations is:
1. You can also render the contract from the operation. In other words, you can use the operation for rendering and processing incoming data
2. You can use operations in tests as factories.
To elaborate on the second point: When I write tests, I don’t use Factory Girl for example, as it is guaranteed to have a different application state when you use your actual API. Factory Girl is great, but it’s not what I want. In
operations, you’ll always have the exact same state you’d get in production. No more tweaking—you can crop images in your tests by using my API, because the API is modeled in operations.
Other posts in this series with Nick Sutterer:
- Tutorial: Validations and Classes in Trailblazer
- Tutorial: Decoupling Requests in Trailblazer Operations
Or Become a Codementor!
Codementor is your live 1:1 expert mentor helping you in real time.