There’s not a defined set of practices in the Ruby on Rails ecosystem that could be described as “advanced”, the framework itself right out of the box could already be described as an advanced platform for web development. Going past
It’s hard to find content that I would describe as “Advanced” Ruby on Rails, so it was a welcome breath of fresh air to pick up “Sustainable Web Development with Ruby on Rails” and find that it came pretty close to what I would describe as advanced.
I’m always on the search for new and interesting ideas in the Ruby on Rails ecosystem, and often, I find things I already know or things that could be described as beginner—or intermediate-level Rails knowledge. For example, how to set up backend processing or the truly miraculous benefits of adopting Rubocop. There’s no shortage of these types of articles. “ Occasionally, “advanced content makes an appearance where topics related to building, deploying, and maintaining Rails projects over the long run are touched upon
What I’m after is “advanced” content,
Advanced Ruby on Rails is a rarely mentioned topic because there is no definition of what exactly constitutes categories such as beginner, intermediate, and advanced.
I found a book that I would describe as “Advanced” Ruby on Rails. The book is “Sustainable Web Development with Ruby on Rails” by David Copeland and if it were up to me I would rename it “Advanced Ruby on Rails” and give it the tagline “Secrets the experts keep to themselves”. It’s a juicier, click-bait title that would be a valid description of the book but kind of a lie because the book itself is evidence that the experts are publishing their tricks rather than keeping them locked up.
The book published in 2020 describes sustainable applications as “one in which changes we make tomorrow are as easy as changes are today, for whatever the application might need to do and whoever might be tasked with working on it.” It proceeds to describe how this might be achieved with Rails apps by going through the framework’s various layers and pieces to give tips and tricks that might result in an application that is more resilient to change in the future.
Each chapter of the book includes practical suggestions that you can immediately apply to a Rails application and more importantly the author does a pretty thorough job of explaining why you might want to make the effort to apply a certain tip to your Rails app. Here are three changes that I was persuaded to make with my own development practice.
Use ActiveRecord models for accessing the database (and nothing else), and put business logic into service objects. One of the most common issues that arise out of long-running Rails apps is the inevitable feature creep that results in “God” objects. God objects are model classes like “User” that over time balloon with so much functionality that they become a real chore to work with even in the simplest cases. A well-described solution to this problem was to implement an additional layer called service objects. The first issue one may encounter when trying to figure out if they should adopt service objects is finding the answer to “What do you mean when you say service object?”
The book describes business logic as the part of the app that makes it special, it’s essentially the reason why the app exists and that this kind of necessary complexity can’t be avoided. Business logic is a magnet for churn because of the unavoidable complexity and in a typical Rails app, the business logic is located within ActiveRecord models that also carry the burden of database access. Service objects are a way to spread this complexity out so that it’s not clumping up in spot resulting in the emergence of a god class.
class Car::Creator
def initialize(car)
@car = car
end
def create
@car.save
Result.new(@car)
end
private
class Result
attr_reader :car
def initialize(car)
@car = car
end
def created?
@car.valid? && @car.persisted?
end
end
end
Previously my position on service objects was either hostile or skeptical. My experience with them came from an existing app I was hired to help develop and the service object layer was already created. It was a nightmare of some of the most difficult procedural code that I ever had the pleasure of stepping through and I just had a hard time understanding why someone quite smart and capable would build something like that. Now to be clear I’m convinced that what I saw was service objects poorly implemented and not the damning evidence that the entire concept was bunk.
Looking at service objects through a different lens meant leaning into an idea that was slowly coming to be adopted regarding being a developer of Ruby on Rails web applications. At some point, I had the realization that when I coded Rails apps I wasn’t programming as so much as I was doing very involved configuration. So much of a Rails app is essentially configuration. Defining an ActiveRecord model with relationships to other model objects, scopes on data access, and validations is just setting one configuration after the other. Once I started looking at Rails apps this way it made it easier to widen my stance on code organization from the more object-oriented approach of having an object like user (a noun) and then calling methods on user, towards a service object approach of having a verb-noun “CreateUser” and calling some form of execute on it.
Copeland is careful to point out that service objects should not be implementing the command design pattern(«») but rather should be flexible enough to allow each service object to support multiple commands. I’m not sure I’m on board with this I found that I actually am drawn to the command pattern type structure and I’m also partial to having the execute method being the same name in each class so that they can be batched easier. I’m not sure that these slight differences matter that much.
What I like about service objects is the concept of the Result class. The resulting class is the key to making the service object useful to the caller because the result class can be tailored to return results in a format useful to the caller. If a service object is simple you can get by just passing a simple boolean successful?. If a service object may fail for multiple reasons each reason can be its boolean. The results class provides so much utility I intend to write a separate article describing the different shapes they might take.
The biggest takeaway is the idea that ActiveRecord models become data access objects only and then service objects take over for business logic. I like this split because ActiveRecord objects that only are configured for relationships, validations, and accessing the database are still quite a bit of responsibility and I wouldn’t be surprised if some of the classes still get quite large. Additionally, the use of a service object eliminates the need to use ActiveRecord callbacks a feature that’s notorious for causing more problems than it fixes.
Sustainable Web Development with Ruby on Rails also helped me crystallize my thoughts on configuring routes. My idea regarding routing and controllers in part influenced by DHH take’s on the matter was that custom routes should be eschewed in favor of creating more controllers even if the whole controller exists just to support one single action. I like Copeland’s advice that you never need more than the default seven actions of index, show, new, edit, update, create, and destroy. Limit yourself to those seven and then get creative with your resource routes to handle any cases that feel like you might want a custom route. I also adopted being strict with how the routes are configured by using the only modifier to signal exactly which routes are being used.
Rails.application.routes.draw do
resources :links, only: [:new, :destroy]
resources :cars, only: [:index, :show, :edit, :new, :create, :update, :destroy]
end
In the view I was convinced that even though Rails supports a wide variety of methods for calling partials in the view. There only needs to be one format and that format should be the one that calls partials explicitly. What I like about this idea is that I no longer have to think too hard about what format I want for calling the partial. It also makes reading the view code easier in that when I see a render block it’s more clear that this is a partial.
The other change with partials is to stop directly referencing instance variables from inside of them and instead reference all variables as local arguments. Admittedly, I haven’t had many problems with this but it just feels like the right thing to do.
I tried some ideas in the book that I ended up abandoning as good ideas but maybe not for me. One of them was the idea of switching the schema.rb file to use SQL instead of Ruby. I thought this was a good idea but I abandoned it after realizing I don’t have a real need for it in SQL and after all these years I’m used to reading it as ruby code, also in the case where I do want to have it as SQL I can just generate for whatever I’m doing at the moment and then delete it after I’m done.
System tests were also something I gave a try based on the idea that testing should also exercise javascript in a way similar to how it’s being used in the browser but I noticed my old complaints about how much work it is to maintain view tests returned and I grew frustrated with it.
Overall I think this book is well worth its price and that with all the of the tips it provides any Rails developer would be able to use something from the book that would be of significant value. Copeland does a good job of explaining the reasoning for each tip and this goes a long way toward allowing the reader to decide for themselves if they want to go ahead and implement the tip in their codebase. Building long-lived Rails applications that don’t get stuck in the mud is not easy, web applications are hard, and I quite enjoyed a book that tries to provide practical advice on how to make code today accommodate changes in the future.