Level Up Your Ruby Code with Draper Decorators
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 sugarIn 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
endIn 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
endNow,
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.decorateAdvance 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
# ...
endInstead of inheriting from Draper::Decorator,
use ApplicationDecorator for the Post decorator.
class PostDecorator < ApplicationDecorator
# decorator methods
endMethod 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
endPassing 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
endWhen 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
}
)
endKey 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.