How to Use the Delegate Method in Rails
How to Use the Delegate Method in Rails
In most modern programming, there are objects that get involved with every aspect of an application. They are on a very high level in the hierarchy and need to interact with almost all other objects directly to ensure the proper functioning of the app. More often than not, these are objects involved in the overlaps of business logic: a User, Booking, or Account.
However, one of the core principles of object-oriented programming is to encapsulate and abstract application components from each other as much as possible. A perfect application, by OOP standards, would be one that would have minimal and discrete associations between its objects and one that would have a minimal number of “god” objects discussed above.
While there are many ways to align these “god” objects with the OOP principles, one of the simplest ways is to reassign some of their responsibility to another class. This is what delegation helps to achieve. This article will look at how delegation works in Ruby and the various programming constructs that Ruby provides to facilitate easy and robust delegation.
Feel free to use these links to navigate the guide:
- What Do We Mean by Delegating?
- How to Use the Rails Delegate Method
- How to Use the Forwardable Module in Rails
- Putting it All Together
What Do We Mean by Delegating?
Delegating is a popular concept in object-oriented programming. The textbook definition of delegation goes like this:
Delegation means evaluating a member (method or property) of one object in the context of another original object. It can either be done explicitly by passing the second object to the first or implicitly by member lookup rules of the programming language.
However, many people tend to use the same term to describe an object calling a method of another object without passing itself as an argument. This is known as forwarding and is a different concept.
Delegation vs. Forwarding
You might have already used forwarding in some of your code. If you can not recall that, here’s how it looks like:
class Worker
def doWork
puts "WORK DONE"
end
end
class Manager
def initialize(worker)
@worker = worker
end
def doWork
worker.doWork
end
private
attr_reader :worker
end
worker = Worker.new
manager = Manager.new(worker)
manager.doWork
# => WORK DONE
This is how a class passes on its work to another. We will discuss more on this in the Forwardable section of this piece. Delegation is quite similar to forwarding, except that in forwarding, the receiving object (here, the worker) acts in its context. In contrast, the receiving object acts on behalf of the sender object (here, the manager) in the case of delegation. Delegation is similar to asking your accountant to donate your money to a charity, while forwarding is asking a friend to donate their money to the charity.
However, defining a method for each corresponding method in the target object can be a tiresome task. When you know you are setting up a class for forwarding, you should define a global catching mechanism to forward all methods by their names directly to the target class.
Ruby provides such a mechanism with its method_missing method. Here’s how you can use it:
class Worker
def doWork
puts "WORK DONE"
end
def takeBreak
puts "ON A BREAK"
end
end
class Manager
def initialize(worker)
@worker = worker
end
def doWork
worker.doWork
end
def method_missing(method, *args)
if worker.respond_to?(method)
worker.send(method, *args)
else
super
end
end
private
attr_reader :worker
end
worker = Worker.new
manager = Manager.new(worker)
manager.doWork
# => WORK DONE
manager.takeBreak
# => ON A BREAK
As you can see, even though the Manager class had no explicit definition of the takeBreak method, the method_missing mechanism could redirect the call to the correct method in the Worker class. Similarly, you can add more methods to the Worker class and try calling them via the Manager object.
This can be an excellent tool for simple forwarding cases. However, if you are looking to do more with delegation, the Ruby programming language offers a host of methods. Let us take a look at some of the popular methods in Ruby that allow for easy delegation of control.
Delegator
The delegate standard library provides two classes for delegating control: Delegator and SimpleDelegator. Delegator is an abstract class that is meant to define custom delegation methodology. It works by allowing you to provide custom implementations for __getobj__ and __setobj__ methods. These methods help to set the delegation target.
Going by the docs, here’s a high-level view of the SimpleDelegator class (which we’ll see next) implemented using Delegator:
class SimpleDelegator < Delegator
def __getobj__
@delegate_sd_obj # return object we are delegating to, required
end
def __setobj__(obj)
@delegate_sd_obj = obj # change delegation object,
# a feature we're providing
end
end
Here’s how you can use the Delegator class to create your implementation of a delegator class:
require 'delegate'
Booking = Struct.new(:id, :user_id)
class MyDelegator < Delegator
attr_accessor :delegate_sd_obj
def __getobj__
@delegate_sd_obj # return object we are delegating to, required
end
def __setobj__(obj)
@delegate_sd_obj = obj # change delegation object, optional
end
end
class BookingDecorator < MyDelegator
def details
"Booking ID: #{id}, User ID: #{user_id}"
end
end
decorated_booking = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorated_booking.details
# => booking_34
As you can see, the BookingsDecorator class has no id or user_id, but when it looks for those inside its details method, it can access the value from the delegated class. This is how delegation works in Ruby.
Simpledelegator
As the other one of the two classes provided by the delegate standard library, SimpleDelegator is a very basic, straightforward implementation of the Delegator class. It helps you wrap up an object (passed while initializing) and delegate all of its supported methods to itself. Here’s how you can use it:
require 'delegate'
Booking = Struct.new(:id, :user_id)
class BookingDecorator < SimpleDelegator
def details
"Booking ID: #{id}, User ID: #{user_id}"
end
end
decorated_booking = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorated_booking.details
# => Booking ID: booking_34, User ID: user_58
Changing The Delegated Object
Since the SimpleDelegator class is based on the Delegator abstract class, you can access __setobj__ and __getobj__ methods. You can use them to create a decorator class that can change the delegated object within itself. This can be used to build classes that summarise or process objects of another class, such as an array or a Booking in our case:
require 'delegate'
Booking = Struct.new(:id, :user_id)
class BookingDecorator
def initialize
@delegator = SimpleDelegator.new(Booking.new("", ""))
end
def new_booking(booking)
@delegator.__setobj__(booking)
end
def details
"Booking ID: #{@delegator.id}, User ID: #{@delegator.user_id}"
end
end
class BookingDecorator2 < SimpleDelegator
def details
"Booking ID: #{id}, User ID: #{user_id}"
end
end
booking_decorator = BookingDecorator.new
booking_decorator.new_booking(Booking.new("booking_34", "user_58"))
puts booking_decorator.details
# => Booking ID: booking_34, User ID: user_58
booking_decorator.new_booking(Booking.new("booking_35", "user_68"))
puts booking_decorator.details
# => Booking ID: booking_35, User ID: user_68
You can pass another booking object to the decorator and summarise its details. You can also do this in a loop to handle the same operation on multiple objects at once.
Overriding Delegated Methods
The SimpleDelegator class also allows you to override delegated methods by using the keyword super. You can call the corresponding method from the wrapped class using super and then define your custom logic. Here’s a quick example to help you get started:
class BookingDecorator < SimpleDelegator
def id
"#{super.split('_',)[1]}"
end
end
This change will now print only the numeral part from the previous booking ID. A typical booking id in the previous example looked like booking_34, from which this method will now print 34. Here’s how you can check for yourself:
decorated_booking.id
# => 34
Delegate method
Another handy delegation method that Ruby offers is the Object.DelegateClass method. It allows you to generate a delegator class for any class on the fly. You can then instantly use this class to inherit from. Let’s take a quick look at an example to understand it better:
require 'delegate'
Booking = Struct.new(:id, :user_id)
class BookingDecorator < DelegateClass(Booking)
def initialize(id, user_id)
@booking = Booking.new(id, user_id)
super(booking)
end
def details
"Booking ID: #{@booking.id}, User ID: #{@booking.user_id}"
end
end
decorator = BookingDecorator.new("booking_34", "user_58")
puts decorator.details
As you can see, you can define a Delegator class on the fly using the DelegateClass method and an appropriate initialize logic. You can then specify your custom methods using the target delegate class’s methods and properties.
How to Use the Rails Delegate Method
While the methods discussed above work perfectly in Ruby, you might want to look for more options when working in Rails. If you have used the ActiveSupport gem in your Rails app, you get the delegate method by default.
This method’s essence is to explicitly define the properties that have to be delegated to the target class. Here’s how you can do that:
delegate :property_one, :property_two, :property_three, to :target_object
There are a couple of tweaks that you can do to this command to get your job done quickly:
- Easy prefixing: You can use the prefix: true option to prefix the delegated method names with the name of the object you are delegating to.
delegate :property_one, :property_two, :property_three, to :target_object, prefix: true
This will generate methods by the names target_object_property_one, target_object_property_two, target_object_property_three, etc.
- Custom prefixing: You can also provide your custom prefixes to add to the delegated method names.
delegate :property_one, :property_two, :property_three, to :target_object, prefix: :another_object
This will generate methods by the names another_object_property_one, another_object_property_two, another_object_property_three, etc.
- Allowing nil: If the object you are delegating to is nil currently, you will typically end up with a NoMethod error. However, setting the :allow_nil option will enable this call to succeed and return non-erroneous nil instead.
delegate :property_one, :property_two, :property_three, to :target_object, allow_nil: true
Rails Delegate Example
Here’s how you can rewrite the BookingDecorator class using the rails delegate method:
# In a real-world rails app, this snippet should be a part of the ApplicationRecord class.
Booking = Struct.new(:id, :user_id)
class BookingDecorator
attr_reader :booking
delegate :id, :user_id, to :booking
def initialize(booking)
@booking = booking
end
def details
"Booking ID: #{@booking.id}, User ID: #{@booking.user_id}"
end
end
decorator = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorator.details
# => Booking ID: booking_34, User ID: user_58
How to Use the Forwardable Module in Rails
Ruby’s standard library also offers one more library for delegation in the form of forwarding. The Forwardable module provides its def_delegator and def_delegators methods to help you define the forwarding rules for your target object easily. This is quite different from the method_missing approach that we mentioned earlier. Instead of setting a global catch for all missing methods and redirecting them to the target class, this approach allows you to pick the methods you want to be forwarded carefully.
This approach gives you more control over how delegation occurs within your objects. You can choose to skip on some methods of the target object to hide them from the client. You can also rename delegated methods using the def_delegator command, which accepts a third, optional argument to specify the new name.
Forwardable Module Example
Here’s how you can rewrite the BookingDecorator class using the Forwardable module:
require 'forwardable'
Booking = Struct.new(:id, :user_id)
class BookingDecorator extend Forwardable
def_delegators :@booking, :id, :user_id
def initialize(booking)
@booking = booking
end
def details
"Booking ID: #{id}, User ID: #{user_id}"
end
end
decorator = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorator.details
# => Booking ID: booking_34, User ID: user_58
If you want to hide some methods or members of the target object, here’s how you can do that:
require 'forwardable'
Booking = Struct.new(:id, :user_id)
class BookingDecorator extend Forwardable
# Intentionally leave out user_id
def_delegators :@booking, :id
def initialize(booking)
@booking = booking
end
def details
"Booking ID: #{id}"
end
end
decorator = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorator.details
# => Booking ID: booking_34
puts decorator.user_id
# => undefined method `user_id' for #<BookingDecorator:0x0055d26d3f5580> (NoMethodError)
And finally, here’s how you can create aliases for the members of the target object via the def_delegator statement:
require 'forwardable'
Booking = Struct.new(:id, :user_id)
class BookingDecorator extend Forwardable
def_delegator :@booking, :id, :booking_id
def_delegator :@booking, :user_id, :uid
def initialize(booking)
@booking = booking
end
def details
"Booking ID: #{booking_id}, User ID: #{uid}"
end
end
decorator = BookingDecorator.new(Booking.new("booking_34", "user_58"))
puts decorator.details
# => Booking ID: booking_34
puts decorator.booking_id
# => booking_34
puts decorator.uid
# => user_58
Putting it All Together
In any Object-Oriented Programming Language, delegation is an instrumental design pattern. It helps you reuse classes and modules to enhance code cleanliness and reduce redundancies. In Ruby, there are multiple ways to implement this. While it seems like an elusive, hard-to-grasp concept, it is, instead, one of the simplest and most frequently used tools in real-world programming.
Delegation, other than being simple, is quite powerful as it allows you to control the scope of your existing objects. The use of delegation enables programmers to finally bring the “god objects” down to earth by making them as reusable as possible.
In Ruby, Delegator and Forwardable are equally great contenders for the best delegate method. When it comes to rails, you can also consider the delegate method as it is one of the methods that comes bundled with a frequently used gem (ActiveSupport). Irrespective of the method you find best, it is vital that you implement any one of these to increase your code’s readability and reusability.
As long as developer experience and cost-efficiency are involved, Scout APM is the way to go for most application performance monitoring use-cases. The simplicity that it offers in terms of interfaces and dashboards is unmatched, keeping its pricing model in mind. You can try it out for a 14-day free trial (no credit card required).
For more in-depth content around web development and a reliable tool for optimizing your application’s performance, navigate our blog and feel free to explore Scout APM with a free 14-day trial!