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 concepts and 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 the delegation method 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.

What is delegating in programming languages?

Delegating is a popular concept in object-oriented programming. 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.

What’s the difference between the delegate method and forwarding?

Delegation is quite similar to forwarding, except that in forwarding, the receiving object acts in its context. In contrast, the receiving object acts on behalf of the sender object 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.

You might have already used forwarding in some of your code. It looks like this:

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. 

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.

What is the delegator class?

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.

As you can see in the Ruby on Rails docs, a high-level view of the SimpleDelegator class (which we’ll see next) implemented using Delegator looks like this:

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.

What is the Simpledelegator class?

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_58C

How do I change a 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 summarize 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 summarize its details. You can also do this in a loop to handle the same operation on multiple objects at once.

How do I override 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

What is the Object.DelegateClass 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 appropriate initialization logic. You can then specify your custom methods using the target delegate class’s methods and properties.

How do I use the Ruby on 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

How do I improve the speed of the Ruby on Rails delegate method?

There are a couple of tweaks that you can do to this command to get your job done quickly:

What’s an example of a Rails delegate?

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 do I 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.

What’s an example of the Forwardable module in Rails?

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: How to use the delegate method in Ruby on Rails

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!