Published on

What is the decorator design pattern?

Authors

Decorator Design Pattern

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

 # frozen_string_literal: true

# == Schema Information
#
# Table name: users
#
#  id                     :bigint(8)        not null, primary key
#  email                  :string
#  encrypted_password     :string           default(""), not null
#  reset_password_token   :string
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  sign_in_count          :integer          default(0), not null
#  current_sign_in_at     :datetime
#  last_sign_in_at        :datetime
#  current_sign_in_ip     :inet
#  last_sign_in_ip        :inet
#  first_name             :string
#  last_name              :string
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#  city                   :string
#  country                :string
#  occupation             :string
#  description            :text
#  age                    :integer
#  phone_number           :string
#  gender                 :integer
#

class User < ApplicationRecord
  validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true, unless: ->(user) {
    user.social_signon != nil
  }
  attr_accessor :skip_password_validation  # virtual attribute to skip password validation while saving

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :uploads, as: :uploadable, dependent: :destroy
  has_one :first_upload, -> { limit(1).order('created_at ASC') },
          class_name: 'Upload'

  has_many :user_conversations, dependent: :destroy
  has_many :conversations, through: :user_conversations
  has_many :stages, through: :conversations
  has_many :messages, dependent: :destroy

  has_many :stage_conversations, -> { where('conversations.stage_id IS NOT NULL') }, through: :user_conversations, source: :conversation
  has_many :private_conversations, -> { where('conversations.stage_id IS NULL') }, through: :user_conversations, source: :conversation

  has_many :posts, dependent: :destroy
  has_many :reactions, dependent: :destroy
  has_many :comments, dependent: :destroy

  has_many :friendships, dependent: :destroy
  has_many :friends, through: :friendships

  has_many :blocks, class_name: 'Block', foreign_key: :user_id, dependent: :destroy
  has_many :blockees, through: :blocks, source: :blockee

  has_many :blocker_blocks, class_name: 'Block', foreign_key: :blockee_id, dependent: :destroy
  has_many :blockers, through: :blocker_blocks, source: :blocker

  has_many :sent_gifts, foreign_key: 'sender_id', class_name: 'VirtualGift'
  has_many :received_gifts, foreign_key: 'receiver_id', class_name: 'VirtualGift'

  has_many :user_reports
  has_many :notifications, foreign_key: :recipient_id

  has_many :customer_orders, class_name: 'Order', foreign_key: :customer_id, dependent: :destroy
  has_many :working_orders, class_name: 'Order', foreign_key: :employee_id, dependent: :destroy

  enum gender: {
    male: 0,
    female: 1
  }

  def full_name
    [
      first_name,
      last_name
    ].compact.join(' ')
  end

  def block(user_id)
    BlockBuilder.new(id, user_id)
  end

  def unblock(user_id)
    blocks.where(blockee_id: user_id).first&.destroy
    if (conversation = private_conversations
       .collect(&:user_conversations)
       .flatten.find { |uc| uc.user_id == user_id }
                        &.conversation)
      conversation.conversation_type = nil
    end
    true
  end

  def report(params)
    user_reports.create(params)
  end

  # OPTIMIZE Find a users existing conversation with another user more efficiently
  def find_existing_conversation(id)
    conversation_id =
      private_conversations
      .collect(&:user_conversations)
      .flatten
      .find { |uc| uc.user_id == id }
      &.conversation_id
    {
      conversation_id: conversation_id,
      other_user_name: other_user(id).first_name
    }
  end

  def other_user(id)
    User.find(id)
  end

  def blocked_users_ids
    (blocker_ids + blockee_ids).uniq
  end


  protected

  def password_required?
    return false if skip_password_validation
    super
  end

  class << self
    def search(search)
      terms = search.split(' ')
      location = where('city ILIKE :search OR ward ILIKE :search', search: "%#{terms[0]}%")
      location.uniq
    end
  end
end

Comments, 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 User

And we'll see that a decorator file is generated.

# app/decorators/user.rb
class UserDecorator < Draper::Decorator
  delegate_all

  # Code
end

After that, we'd just need to call .decorate in our controllers before sending the resources to the view layer.

# app/controllers/users_controller.rb
def index
  @users = User.all
end

# becomes

def index
  @users = User.all.decorate
end

In the decorator we can presentation logic.

# app/decorators/user.rb
  def full_name
    if object.first_name && object.last_name
      "#{object.first_name} #{object.last_name}"
    elsif object.first_name
      object.first_name
    else
      'Anonymous'
    end
  end

  def location
    if object.city && object.country
      "#{object.city}, #{object.country}"
    else
      object.country
    end
  end

  def profile_uploads
    object.uploads
  end

  def most_recent_profile_photo
    if object.uploads[0].present?
      url_for(object.uploads[0].media)
    else
      if object.gender.present?
        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'
      else
        'https://cdn1.iconfinder.com/data/icons/business-charts/512/customer-512.png'
      end
    end
  end

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

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.