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 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.