Decorator pattern in Ruby on Rails
Table of Contents
Decorator Pattern #
Have you ever heard anything about the Decorator pattern? #
To start from the word go, we should determine what the Decorator pattern is. The Decorator is a structural design pattern that gives developers the ability to add new behaviors to objects by placing these objects inside of special wrapper objects that contain the behaviors.
The main advantages of using it are:
-
Developers can extend an object’s behavior without making a new subclass, as well as add or remove responsibilities from an object at runtime.
-
It helps reduce the subclass count and could be combined with other wrappers and classes.
-
It aligns with the Single Responsibility Principle (you can break down large monolithic classes into smaller ones that are easier to read, maintain and test) and the Open-Closed Principle (promoting code that is open for extension but closed for modification).
But what about Rails? #
Well… How large your models are and how much logic have you encapsulated within them?
In most Rails applications, developers add logic related to a particular model directly into the Model.rb
file. As a result, it can become extremely challenging to maintain methods as an application and models grows larger.
Let’s imagine that you need to create a formatted string that contains id, name and registration_date of your Customer
. How could you do it?
The most common way to achieve this is just by adding a new method to the model file.
# app/models/customer.rb
class Customer < ApplicationRecord
# has_many: ...
# some model validation and callbacks logic
def formatting_with_id
"(#{id}) #{name}: #{registration_date}"
end
end
Then, to execute it, you will probably do something like this:
my_customer = Customer.find_by(id: 123)
my_customer.formatting_with_id
# => "(123) MyFirstCustomer: 20-08-2023"
But what will you do if you need to add more logic to this string because the business requirements have changed?
Let’s give Decorator a shot.
SimpleDelegator approach #
All our future decorators will inherit BaseDecorator
class.
# app/decorators/base_decorator.rb
class BaseDecorator < SimpleDelegator
end
The SimpleDelegator is a part of the delegate standard library. The main idea of this class is to redirect method calls to another object, commonly referred to as the “delegate” object. Since SimpleDelegator is a subclass of Delegator, it provides a straightforward approach to delegate method calls to the wrapped object.
SimpleDelegator uses __getobj__
method to access the wrapped object.
# app/decorators/customer_decorator.rb
class CustomerDecorator < BaseDecorator
def formatting_with_id
"(#{id}) #{name}: #{registration_date}"
end
end
my_customer = Customer.find_by(id: 123)
decorated_customer = CustomerDecorator.new(my_customer)
decorated_customer.formatting_with_id
# => "(123) MyFirstCustomer: 20-08-2023"
# Since our decorator class is inherited from the SimpleDelegator class,
# our decorated object still have access to all fields and methods
# of the Customer's model
decorated_customer.name
# => "MyFirstCustomer"
Whenever you will need to match new business logic, you could wrap objects into the new wrappers:
# app/decorators/outdated_customer_decorator.rb
class OutdatedCustomerDecorator < BaseDecorator
def formatting_with_id
super + " - requested account deletion"
end
end
Now you could easily use recently created decorator objects and decorate them with new functionality.
my_customer = Customer.find_by(id: 123)
decorated_customer = CustomerDecorator.new(my_customer)
decorated_customer.formatting_with_id
# => "(123) MyFirstCustomer: 20-08-2023"
OutdatedCustomerDecorator.new(decorated_customer).formatting_with_id
# => "(123) MyFirstCustomer: 20-08-2023 - requested account deletion"
Summary #
To summarize everything above, Decorator classes in Rails applications offer benefits that contribute to a cleaner and maintainable codebase. It aligns with the DRY and SOLID principles as well as gives freedom of object customization.
Generally, it is a powerful tool to enhance the modularity, maintainability, and extensibility of applications by allowing developers to add behavior in a flexible and reusable manner. This results in cleaner code that is easier to understand, maintain, adapt to changing requirements, and which significantly speeds up the development process.
We are ready to provide expert's help with your product
or build a new one from scratch for you!