Introduction
The decorator pattern is used to define additional functionality on a resource.
This is useful when a resource becomes bloated and unwieldy due to all the other requirements the resources implement.
In Ruby & Rails the Draper gem can be used to implement this design pattern.
Imagine for example, a User resource in a Ruby on Rails application
Purpose
The decorator pattern allows you to add responsibilities or behavior to an object dynamically without modifying its original class. It promotes flexibility by letting you compose behaviors at runtime instead of creating large, monolithic classes or an explosion of subclasses.
Use Cases
- Adding presentation logic to models without cluttering them (e.g., formatting a User’s name or displaying status labels).
- Extending objects with additional features, like logging, caching, or validation, without altering their core implementation.
- Layering multiple behaviors on a single object in a flexible and reusable way.
- Wrapping API responses or third-party objects to adapt or enhance their interface for your application.
1class User < ApplicationRecord2 validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true, unless: ->(user) {3 user.social_signon != nil4 }5 attr_accessor :skip_password_validation6 :recoverable, :rememberable, :trackable, :validatable7 8 has_many :uploads, as: :uploadable, dependent: :destroy9 has_one :first_upload, -> { limit(1).order('created_at ASC') },10 class_name: 'Upload'11 12 has_many :user_conversations, dependent: :destroy13 has_many :conversations, through: :user_conversations14 has_many :stages, through: :conversations15 has_many :messages, dependent: :destroy16 17 has_many :stage_conversations, -> { where('conversations.stage_id IS NOT NULL') }, through: :user_conversations, source: :conversation18 has_many :private_conversations, -> { where('conversations.stage_id IS NULL') }, through: :user_conversations, source: :conversation19 20 has_many :posts, dependent: :destroy21 has_many :reactions, dependent: :destroy22 has_many :comments, dependent: :destroy23 24 has_many :friendships, dependent: :destroy25 has_many :friends, through: :friendships26 27 has_many :blocks, class_name: 'Block', foreign_key: :user_id, dependent: :destroy28 has_many :blockees, through: :blocks, source: :blockee29 30 has_many :blocker_blocks, class_name: 'Block', foreign_key: :blockee_id, dependent: :destroy31 has_many :blockers, through: :blocker_blocks, source: :blocker32 33 has_many :sent_gifts, foreign_key: 'sender_id', class_name: 'VirtualGift'34 has_many :received_gifts, foreign_key: 'receiver_id', class_name: 'VirtualGift'35 36 has_many :user_reports37 has_many :notifications, foreign_key: :recipient_id38 39 has_many :customer_orders, class_name: 'Order', foreign_key: :customer_id, dependent: :destroy40 has_many :working_orders, class_name: 'Order', foreign_key: :employee_id, dependent: :destroy41 42 enum gender: {43 male: 0,44 female: 145 }46 47 def full_name48 [49 first_name,50 last_name51 ].compact.join(' ')52 end53 54 def block(user_id)55 BlockBuilder.new(id, user_id)56 end57 58 def unblock(user_id)59 blocks.where(blockee_id: user_id).first&.destroy60 if (conversation = private_conversations61 .collect(&:user_conversations)62 .flatten.find { |uc| uc.user_id == user_id }63 &.conversation)64 conversation.conversation_type = nil65 end66 true67 end68 69 def report(params)70 user_reports.create(params)71 end72 73 def find_existing_conversation(id)74 conversation_id =75 private_conversations76 .collect(&:user_conversations)77 .flatten78 .find { |uc| uc.user_id == id }79 &.conversation_id80 {81 conversation_id: conversation_id,82 other_user_name: other_user(id).first_name83 }84 end85 86 def other_user(id)87 User.find(id)88 end89 90 def blocked_users_ids91 (blocker_ids + blockee_ids).uniq92 end93 94 95 protected96 97 def password_required?98 return false if skip_password_validation99 super100 end101 102 class << self103 def search(search)104 terms = search.split(' ')105 location = where('city ILIKE :search OR ward ILIKE :search', search: "%#{terms[0]}%")106 location.uniq107 end108 end109endComments, relationships, and instance & class methods accounted for, our class becomes large and difficult to maintain.
The decorator design pattern is usually used to help us to extract out logic related generating views, hence the name, decorator.
After adding the gem to our Gemfile and bundling we'll see the Draper creates a new file where we can extract out the presentation logic of our resources(as opposed to business logic).
We run:
rails generate decorator UserAnd we'll see that a decorator file is generated.
1class UserDecorator < Draper::Decorator2 delegate_all3 4 # Code5endAfter that, we'd just need to call .decorate in our controllers before sending the resources to the view layer.
1def index2 @users = User.all3end4 5# becomes6 7def index8 @users = User.all.decorate9endIn the decorator we place the presentation logic.
1def full_name2 if object.first_name && object.last_name3 "#{object.first_name} #{object.last_name}"4 elsif object.first_name5 object.first_name6 else7 'Anonymous'8 end9 end10 11 def location12 if object.city && object.country13 "#{object.city}, #{object.country}"14 else15 object.country16 end17 end18 19 def profile_uploads20 object.uploads21 end22 23 def most_recent_profile_photo24 if object.uploads[0].present?25 url_for(object.uploads[0].media)26 else27 if object.gender.present?28 object.gender == 'female' ? 'https://cdn0.iconfinder.com/data/icons/social-messaging-ui-color-shapes/128/user-female-circle-pink-512.png' : 'https://cdn1.iconfinder.com/data/icons/business-charts/512/customer-512.png'29 else30 'https://cdn1.iconfinder.com/data/icons/business-charts/512/customer-512.png'31 end32 end33 endThe logic isn't that complicated, but if we performed this type of conditional checks in our partials we might find that the same logic was repeated quickly.
Conclusion
The Decorator Design Pattern does the following:
-
Extracts conditional logic out of the front end partials.
-
Extracts presentation logic out of Active Record resources/models.
-
Reduces the need to for helper methods which pollute the global namespace.