Level Up Your Ruby Code with Draper Decorators

railsApril 02, 2024Dotby Alkesh Ghorpade

Introduction

In web development, Rails stands tall as one of the most potent and efficient frameworks for building web applications. With its convention over configuration principle, Rails offers developers a productive environment to quickly create robust applications. However, code becomes increasingly challenging as applications become complex, clean, readable, and maintainable. This is where gems like Draper come to the rescue.

What is Draper?

Draper is a popular gem in the Rails ecosystem designed to help developers clean up their views and controllers by moving presentation logic out of models and controllers into decorators. Draper allows you to decorate your Rails models with presentation-related methods, making your code more organized, readable, and maintainable.

What is Decorator?

In Ruby, the Decorator pattern is a structural design pattern that dynamically changes the behaviour of individual objects without affecting the behaviour of other objects from the same class. This pattern is proper when you need to extend the functionality of objects at runtime or when subclassing is impractical.

Here's a basic implementation of the Decorator pattern in Ruby:

# Component interface
class Coffee
  def cost
    5
  end

  def description
    "Simple coffee"
  end
end

# Base decorator
class CoffeeDecorator < Coffee
  def initialize(coffee)
    @coffee = coffee
  end

  def cost
    @coffee.cost
  end

  def description
    @coffee.description
  end
end

# Concrete decorators
class MilkDecorator < CoffeeDecorator
  def cost
    super + 1
  end

  def description
    super + ", with milk"
  end
end

class SugarDecorator < CoffeeDecorator
  def cost
    super + 0.5
  end

  def description
    super + ", with sugar"
  end
end

## Usage

simple_coffee = Coffee.new
puts "Cost: #{simple_coffee.cost}, Description: #{simple_coffee.description}"
Cost: 5, Description: Simple coffee

milk_coffee = MilkDecorator.new(simple_coffee)
puts "Cost: #{milk_coffee.cost}, Description: #{milk_coffee.description}"
Cost: 6, Description: Simple coffee, with milk

sugar_milk_coffee = SugarDecorator.new(milk_coffee)
puts "Cost: #{sugar_milk_coffee.cost}, Description: #{sugar_milk_coffee.description}"
Cost: 6.5, Description: Simple coffee, with milk, with sugar

In this example, Coffee is the base component class that defines the basic functionality of a coffee. CoffeeDecorator is the base decorator class inherited from Coffee and wraps another Coffee object. MilkDecorator and SugarDecorator are concrete decorators that add functionality to the base Coffee object. Decorators add their behaviour (like cost or description) by delegating to the wrapped Coffee object and modifying its result.

Draper in Rails

Draper decorators (found in app/decorators) inherit from Draper::Decorator and share the name of the model they decorate.

Let's say you have a Post model in your Rails application. You can create a PostDecorator by executing the below command:

rails generate decorator Post

# app/decorators/post_decorator.rb
class PostDecorator < Draper::Decorator
  delegate_all

  def formatted_title
    "<h2>#{title}</h2>".html_safe
  end
end

In your Post controller, you'll need to decorate the @post instance variable before passing it to the view:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id]).decorate
  end
end

Now, in your show.html.erb view for the Post model (app/views/posts/show.html.erb), you can call the formatted_title method defined in the decorator:

<!-- app/views/posts/show.html.erb -->

<%= @post.formatted_title %>
<p><%= @post.content %></p>

With Post.find(params[:id]).decorate, you applied the PostDecorator on a single object. To apply decorator on the collection, use the decorate_collection method.

@posts = PostDecorator.decorate_collection(Post.all)

If your collection is an ActiveRecord Relation object, you can use this:

@posts = Post.most_viewed.decorate

Advance Usage

Shared Decorator Methods

Just like controllers inherit from a common base class in Rails, decorators can benefit from shared functionality. Since decorators are regular Ruby objects, you can leverage any standard technique for code reuse.

# app/decorators/application_decorator.rb

class ApplicationDecorator < Draper::Decorator
# ...
end

Instead of inheriting from Draper::Decorator, use ApplicationDecorator for the Post decorator.

class PostDecorator < ApplicationDecorator
  # decorator methods
end

Method Delegation

Using delegate_all in your decorator allows any method call (including super) to be passed on to the decorated object, even those not defined in the decorator itself. This offers a flexible approach, but you can selectively delegate specific methods for stricter view control.

class PostDecorator < Draper::Decorator
  delegate :title, :body
end

Passing Context

Imagine you have a ProductDecorator that decorates a Product model. You want to display a different price based on the current user's role (e.g., discounted price for admins).

class ProductDecorator < ApplicationDecorator
  delegate_all

  def initialize(product, context = {})
    super(product)
    @context = context
  end

  def price
    if @context[:current_user]&.admin?
      # Discounted price for admins
      product.price * 0.8
    else
      product.price
    end
  end
end

When calling the ProductDecorator, you need to pass current_user in the context.

def show
  @product = Product.find(params[:id])
  @product_decorator = @product.decorate(
    context: {
      current_user: current_user
    }
  )
end

Key Features and Benefits:

  • Separation of Concerns: One of the core principles of software engineering is the separation of concerns. Draper facilitates this by providing a clean separation between your application's presentation and business logic. This separation makes your codebase more modular and easier to understand.

  • Cleaner Views: Views in Rails tend to become cluttered with presentation-related logic over time, making them difficult to maintain. Using Draper decorators, you can extract this logic from your views, resulting in cleaner and more readable templates.

  • Testability: Draper enhances the testability of your codebase by allowing you to test your decorators in isolation from the models and controllers. This enables more focused testing and makes writing robust test suites for your application easier.

  • Reusability: With Draper, you can encapsulate common presentation patterns within decorators and reuse them across multiple views and controllers. This promotes code reuse and helps avoid duplication of code.

  • Flexibility: Draper provides a flexible and intuitive API for defining decorators, allowing you to customize the presentation of your models according to your specific requirements. Whether you need to format dates, truncate strings, or perform any other presentation-related tasks, Draper makes it easy to do so.

Conclusion

The Rails Draper gem offers a convenient and elegant solution for managing presentation logic in Rails applications. By embracing the principles of separation of concerns, testability, reusability, flexibility, and cleanliness, Draper empowers developers to build maintainable and scalable Rails applications quickly. Whether you're working on a small personal project or a large-scale enterprise application, Draper can be a valuable addition to your Rails toolkit, helping you write better code and deliver better user experiences.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right