Posted on

Using decorators in Ruby on Rails has many benefits. Models become less fat, views less complicated, and using the procedural view helpers becomes a thing of the past.

When applying decorators to your Rails project, you might be tempted to maintain a 1:1 mapping between models and decorators. All the presentation logic of a Article would go into an ArticleDecorator. While the decorators are small, this may be a valid approach.

But over time, the decorators will grow as requirements are added. They’ll collect more and more methods, get more responsibilities and slowly the decorators grow fat. Bugs will soon creep out from under the big pile of methods.

Stuffing everything inside a single decorator isn’t really a big step up from using Rails’ view helpers. If you want to get the most out of decorators, it’s important to apply the principles of object oriented programming to them as well.

Here are a couple of code smells I commonly find in decorators.

Divergent Change

Large decorators often span multiple pieces of functionality. As requirements change, one decorator finds itself changed multiple times for different requirements. This smell is called Divergent Change.

It’s an indicator that the Single Responsibility Principle is being violated. And no, “taking care of presentation logic” doesn’t count as a single responsibility here ;-) Groups of closely related methods responsible for a single part of functionality should probably be moved out into their own classes.

Feature Envy

When moving unnecessary logic out of a view, it’s often placed into the nearest possible decorator. When doing this, a decorator will start to reach deep into other objects and eventually will be more concerned with other objects rather than itself.

This this introduces a lot of coupling between objects and makes it hard to refactor. This kind of functionality needs to be moved to a more appropriate decorator.

A smelly decorator

Here’s a decorator to handle the presentation logic of a Git commit. I’ve omitted quite some parts for brevity of the example. I’m using the Draper gem, though you could go with any other solution, or even your own.

class CommitDecorator < Draper::Decorator
  delegate_all

  def author_link
    h.link_to(author.name, h.profile_path(author.username))
  end

  def parent_link
    h.link_to(parent.truncated_sha, h.project_commit_path(project, parent.sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end

  def file_changes
    diffs.map do |diff|
      DiffLine.new(
        status_class_for(diff), diff.path, diff.additions, diff.deletions)
    end
  end

  private

  DiffLine = Struct.new(:status_class, :path, :additions, :deletions)

  def status_class_for(diff)
    if diff.deleted?
      'deletion'
    elsif diff.added?
      'addition'
    else
      'change'
    end
  end
end

A commits controller may look like this:

class CommitsController < ApplicationController
  decorates_assigned :commits, :commit

  def index
    @commits = find_project.commits
  end

  def show
    @commit = find_project.find_commit_by_sha(params[:sha])
  end

  private

  def find_project
    Project.find(params[:project_id])
  end
end

There are two scenarios here:

  • A list of commits in the project is displayed
  • One commit is displayed along with its changed files

Problems

The decorator incorporates both of the smells described above.

It seems awkward to have a decorator with all these functionalities if all you want to do is to display a summarized commit with only a bit of information.

It’s reaching into the tiny little details of the other objects quite a lot. It’s very coupled to the internals of diffs especially.

Making it less envy

The most alarming issue is the reaching into diffs to gather the file changes. Let’s quickly refactor that into a DiffDecorator:

class DiffDecorator < Draper::Decorator
  delegate_all

  def status_class
    if deleted?
      'deletion'
    elsif added?
      'addition'
    else
      'change'
    end
  end
end

Note how much that already cleans up the CommitDecorator. The file_changes is now synonymous to the decorated diffs. I’ve added an alias_method to keep the same interface, though this is not strictly necessary.

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs

  alias_method :file_changes, :diffs

  def author_link
    h.link_to(author.name, h.profile_path(author.username))
  end

  def parent_link
    h.link_to(parent.truncated_sha, h.project_commit_path(project, parent.sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end
end

The links to the author and parent commit are better off in their own decorator too. Since parent is also a commit object itself, this implies adding a link method to CommitDecorator.

class AuthorDecorator < Draper::Decorator
  delegate_all

  def link
    h.link_to(name, h.profile_path(username))
  end
end

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs
  decorates_association :author
  decorates_association :parent

  alias_method :file_changes, :diffs

  delegate :link, to: :author, prefix: true
  delegate :link, to: :parent, prefix: true

  def link
    h.link_to(truncated_sha, h.project_commit_path(project, sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end
end

Now the CommitDecorator only has some delegations for the author_link and parent_link.

Dealing with Divergent Change

I always like to have some kind of default or base decorator for a model which defines the methods commonly used in different contexts. It makes sense for this to be called CommitDecorator. Whenever commit.decorate is called, it’s automatically decorated with the default decorator.

Some decorated functionality may only be required in a single context. I prefer to put these into their own decorator classes, so it becomes more obvious where it’s being used, and it’s not hiding in between the common functionality.

For this example, I’d consider a summarized commit sufficient to be decorated by a default decorator. But the detailed commit definitely needs its own:

module Commits
  class DetailedCommitDecorator < Draper::Decorator
    delegate_all

    def initialize(*args)
      super(CommitDecorator.new(*args))
    end

    def diff_stats
      h.t('commits.show.diff_stats_html',
          changed: diffs.count,
          additions: diffs.sum(&:additions),
          deletions: diffs.sum(&:deletions))
    end
  end
end

I always namespace these context-specific decorators by the class they’re decorating. When you’ve got many decorators, this allows you to still have an overview.

I dislike using inheritance with decorators, it feels to be against the nature of a decorator. It’s also confusing when overriding methods from the parent class. Overriding inherited methods may need another approach than overriding methods already defined on the model.

Therefore, I first wrap the original commit object into a CommitDecorator. This would be equivalent to doing something like Commits::DetailedCommitDecorator.new(CommitDecorator.new(commit)), but I don’t want to have to think about applying both decorators all the time. Note I’m using delegate_all in all of my decorators to make sure any method unknown to the decorator is being delegated to the decorated object.

Extracting the functionality of a detailed commit leaves the CommitDecorator greatly simplified:

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs
  decorates_association :author
  decorates_association :parent

  alias_method :file_changes, :diffs

  delegate :link, to: :author, prefix: true
  delegate :link, to: :parent, prefix: true

  def link
    h.link_to(truncated_sha, h.project_commit_path(project, sha))
  end
end

The last thing to do is to make sure the controller applies the appropriate decorators:

class CommitsController < ApplicationController
  decorates_assigned :commits
  decorates_assigned :commit, with: Commits::DetailedCommitDecorator

  def index
    @commits = find_project.commits
  end

  def show
    @commit = find_project.find_commit_by_sha(params[:sha])
  end

  private

  def find_project
    Project.find(params[:project_id])
  end
end

Conclusion

Avoid turning your decorators into your view helper surrogates. Avoid creating a tangled mess of methods and responsibilities by appropriately dealing with code smells in your decorators. Don’t be afraid to introduce new decorator classes, but only do so if you can justify their existence.

What kind of code smells do you frequently encounter in your decorators? Let me know in the comments below!


If you like this post, follow me:


Or share this post: