Design Patterns: Decorator

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.
./app/models/user.rb
1class User < ApplicationRecord
2 validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true, unless: ->(user) {
3 user.social_signon != nil
4 }
5 attr_accessor :skip_password_validation
6 :recoverable, :rememberable, :trackable, :validatable
7
8 has_many :uploads, as: :uploadable, dependent: :destroy
9 has_one :first_upload, -> { limit(1).order('created_at ASC') },
10 class_name: 'Upload'
11
12 has_many :user_conversations, dependent: :destroy
13 has_many :conversations, through: :user_conversations
14 has_many :stages, through: :conversations
15 has_many :messages, dependent: :destroy
16
17 has_many :stage_conversations, -> { where('conversations.stage_id IS NOT NULL') }, through: :user_conversations, source: :conversation
18 has_many :private_conversations, -> { where('conversations.stage_id IS NULL') }, through: :user_conversations, source: :conversation
19
20 has_many :posts, dependent: :destroy
21 has_many :reactions, dependent: :destroy
22 has_many :comments, dependent: :destroy
23
24 has_many :friendships, dependent: :destroy
25 has_many :friends, through: :friendships
26
27 has_many :blocks, class_name: 'Block', foreign_key: :user_id, dependent: :destroy
28 has_many :blockees, through: :blocks, source: :blockee
29
30 has_many :blocker_blocks, class_name: 'Block', foreign_key: :blockee_id, dependent: :destroy
31 has_many :blockers, through: :blocker_blocks, source: :blocker
32
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_reports
37 has_many :notifications, foreign_key: :recipient_id
38
39 has_many :customer_orders, class_name: 'Order', foreign_key: :customer_id, dependent: :destroy
40 has_many :working_orders, class_name: 'Order', foreign_key: :employee_id, dependent: :destroy
41
42 enum gender: {
43 male: 0,
44 female: 1
45 }
46
47 def full_name
48 [
49 first_name,
50 last_name
51 ].compact.join(' ')
52 end
53
54 def block(user_id)
55 BlockBuilder.new(id, user_id)
56 end
57
58 def unblock(user_id)
59 blocks.where(blockee_id: user_id).first&.destroy
60 if (conversation = private_conversations
61 .collect(&:user_conversations)
62 .flatten.find { |uc| uc.user_id == user_id }
63 &.conversation)
64 conversation.conversation_type = nil
65 end
66 true
67 end
68
69 def report(params)
70 user_reports.create(params)
71 end
72
73 def find_existing_conversation(id)
74 conversation_id =
75 private_conversations
76 .collect(&:user_conversations)
77 .flatten
78 .find { |uc| uc.user_id == id }
79 &.conversation_id
80 {
81 conversation_id: conversation_id,
82 other_user_name: other_user(id).first_name
83 }
84 end
85
86 def other_user(id)
87 User.find(id)
88 end
89
90 def blocked_users_ids
91 (blocker_ids + blockee_ids).uniq
92 end
93
94
95 protected
96
97 def password_required?
98 return false if skip_password_validation
99 super
100 end
101
102 class << self
103 def search(search)
104 terms = search.split(' ')
105 location = where('city ILIKE :search OR ward ILIKE :search', search: "%#{terms[0]}%")
106 location.uniq
107 end
108 end
109end

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
1class UserDecorator < Draper::Decorator
2 delegate_all
3
4 # Code
5end

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

./controllers/users_controller.rb
1def index
2 @users = User.all
3end
4
5# becomes
6
7def index
8 @users = User.all.decorate
9end

In the decorator we place the presentation logic.

./app/decorators/user.rb
1def full_name
2 if object.first_name && object.last_name
3 "#{object.first_name} #{object.last_name}"
4 elsif object.first_name
5 object.first_name
6 else
7 'Anonymous'
8 end
9 end
10
11 def location
12 if object.city && object.country
13 "#{object.city}, #{object.country}"
14 else
15 object.country
16 end
17 end
18
19 def profile_uploads
20 object.uploads
21 end
22
23 def most_recent_profile_photo
24 if object.uploads[0].present?
25 url_for(object.uploads[0].media)
26 else
27 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 else
30 'https://cdn1.iconfinder.com/data/icons/business-charts/512/customer-512.png'
31 end
32 end
33 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.

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.