Ruby on Rails uphold the philosophy of convention over configuration. And once we get the hold of the framework, it becomes really easy to navigate any other codebases which use Rails. The complexity we’d face would be more of the domain at hand rather than the framework. As a Rails developer, we work with and build on top of the abstractions that the framework provides.

When starting with Rails, I grew fond of the abstractions within the framework. We could achieve so much with a few lines of code. I needlessly tried to write more of abstract code wherever possible - until a senior dev advised me to write simple and dumb code instead. This sounded absurd to me at first but then ended up sticking with it.

“There’s a sweet spot that represents the perfect compromise between comprehension and changeability, and it’s your job as a programmer to find it.” - Excerpt From: Sandi Metz, Katrina Owen. “99 Bottles of OOP”

Code is more often read than it is written. And for each abstraction that we’d introduce there is a level of indirection along with it. These indirections in code add to the mental cost for a reader. The reader would need to know about the contracts that these abstractions honors to extend or modify it. Unless the code is to be reused or extended elsewhere, it is better to stay away from it. Moreover trying to do abstractions early could result in having the wrong ones in the code. It becomes more harmful than being beneficial.

Patterns to add abstraction with simplicity

As the codebases grow with time, abstractions would eventually be necessary to DRY out the code to provide more changeability. We must do it in a way such that code comprehension is not affected by much. There are certain community adopted abstraction patterns which can be made use of to tackle this. And these are pretty much the patterns everyone agrees on. This is great, as we can leverage these patterns to increase readability and composability in our codebase. Moreover, they can be called as unofficial conventions within Rails community which mostly everyone would know of.

Service Objects

Service object helps you to extract out the logic that shouldn’t necessarily reside within your model or controller. Thus keeping your controller and models lean. These can be logic for making an API call, specific model callbacks or any other domain-specific action. If this logic is to be shared b/w controllers or models then concerns are what you are looking for rather than service objects.

Say you have a blog app wherein once a post is published, you’d want to notify your slack workgroup, create a tweet and then also send out emails to all the users subscribed to your mailing list. Of course you could add these all cases onto the Post model callbacks. But I’d advise you not to as callbacks make your code tightly coupled and will later be a pain in the ass. Why do you ask? Check here.

Let’s assign this responsibilty to a PORO like this:

class PostNotificationService
  def initialize(post)
    @post = post
  end

  def perform
    return false unless @post.published?

    send_notifications
  end

  private

  def send_notifications
    SlackNotificationService.new(@post).perform
    TwitterNotificationService.new(@post).perform
    MailingListNotificationService.new(@post).perform
  end
end

Things to adhere to:

  • Have a separate directory to create app/services/*
  • Keep the class name descriptive to imply its responsibility explicitly.
  • Have a single signature such as #perform across all services.
  • Stick with single responsibility for the service at hand for more composability.
  • Exceptions from its responsibility are to be handled within the service itself.
  • Introduce namespaces when necessary.

Form Objects

Ever had trouble when dealing with multiple forms for a single model such that you’d need to perform validations based on the form context? Or say you want certain actions to be performed after a particular form is submitted?

Fear not! Form objects help you to decouple such contexts or actions out from your model. And with ActiveModel::Model you still get the attribute assignment and validations like with a model on a PORO.

Let us consider an example wherein a user creates a post. The form object for it would look like this:

class PostCreateForm
  include ActiveModel::Model

  attr_accessor :title, :content

  validates :title, :content, presence: true

  def initialize(user, params)
    @user = user
    super(params)
  end

  def submit
    return false if invalid?

    @user.posts.create(title: @title, content: @content)
  end
end

This includes the validations necessary for the creation and also conforms with #save as used with a model. It’d return false if any of the validations fail else will create the record and return true.

Now let us consider an example for updating the form and making it published. We need to make sure that only the author of the post can update it and send out notifications if it is being published for the first time.

class PostUpdateForm
  include ActiveModel::Model

  attr_accessor :title, :content, :published

  validates :title, :content, :published, presence: true
  validate :user_authorized

  def initialize(user, post, params)
    @user = user
    @post = post
    @currently_published = @post.published
    super(params)
  end

  def submit
    return false if invalid?

    result = update_post
    send_notifications unless @currently_published
    result
  end

  private

  def user_authorized
    errors.add(:user_id, :not_authorized) if @post.user != @user
  end

  def update_post
    @post.update(
      title: @title,
      content: @content,
      published: @published,
      published_at: @post.published_at.presence || published_at
    )
  end

  def published_at
    Time.current unless @published
  end

  def send_notifications
    PostNotificationService.new(@post).perform
  end
end

I hope you see the flexibility achieved here. We also plugged in the PostNotificationService conveniently.

Things to adhere to:

  • Have a separate directory to create app/forms/*
  • Keep the class name to specify form at hand.
  • Have a single signature such as #submit across all forms.
  • The invoking signature should conform to the model’s #save API.

The corresponding PostsController would now look something like this now:

class PostsController < BaseController
  ........

  def create
    form = PostCreateForm.new(current_user, params)

    if form.submit
      flash[:success] = "Post created sucessfully"
      redirect_to posts_path
    else
      flash.now[:errors] = form.errors.full_messages.join(', ')
      render locals: { form: form }
    end
  end

  def update
    form = PostUpdateForm.new(current_user, @post, params)

    if form.submit
      flash[:success] = "Post updated sucessfully"
      redirect_to posts_path
    else
      flash.now[:errors] = form.errors.full_messages.join(', ')
      render locals: { form: form }
    end
  end

  ........
end

Decorators

This is a pattern which helps us to decouple view specific logic from the model. It is a bad smell to keep such decoration logic within the model which should contain only business-related logic.

And yes, we do have helpers for this purpose. But the thing with helpers is that it is made available to all the views globally. Furthermore, you can make use of #helpers method to even access it outside the views. It makes no sense to have a certain view specific details to be made available globally.

This is where decorators enter. You can implement easily with the help of SimpleDelegator. Have a PORO to inherit from this class and we are good to go.

class PostDecorator < SimpleDelegator
  def title
    super.upcase
  end

  def published_at
    I18n.l(super, format: '%d %b %Y')
  end

  def author
    "#{user.first_name} #{user.last_name}"
  end

  def likes
    "#{likes_count} likes"
  end
end

All the methods called on this object will be delegated onto the object we pass onto it’s constructor. And we also get an opportunity to extend upon it. Pretty neat I would say.

The controller and the corresponding view would be something like this:

class PostsController < BaseController
  ........

  def show
    post = Post.find(params[:id])
    post_decorator = PostDecorator.new(post)

    render locals: { post_decorator: post_decorator }
  end

  ........
end
<div class="container">
  <div class="post-title">
    <%= post_decorator.title %>
  </div>
  <div class="post-content">
    <p>
      <%= post_decorator.content %>
    </p>
  </div>
  <div class="post-info">
    <span class="likes"><%= post_decorator.likes %></span>
    <span class="author"><%= post_decorator.author %></span>
  </div>
</div>

Conclusion

By implementation of these patterns, we get to make our code loosely coupled, easily manageable, sanely testable and extendable. Since we honor the contracts for each of these abstractions - one can effortlessly navigate the codebase.