Gaurab Paul

Polyglot software developer & consultant passionate about web development, distributed systems and open source technologies

Support my blog and open-source work

Tags

Useful delegation patterns for Rails
Posted  8 years ago

This post has not been updated in quite some time and the content here may be out of date or not reflect my current my recommedation in the matter.

About delegation

Delegation is a very useful software pattern and this post focusses on how this pattern can be applied in the context of a Rails application and the associated advantages of doing so.

If you are unfamiliar with this pattern, Wikipedia has a very good explanation:

In software engineering, the delegation pattern is a design pattern in object-oriented programming where an object, instead of performing one of its stated tasks, delegates that task to an associated helper object.

There is an Inversion of Responsibility in which a helper object, known as a delegate, is given the responsibility to execute a task for the delegator.

Also, this Stackoverflow post has some interesting posts elaborating on the practical benefits of the concepts of delegation in a language neutral context.

Implementing Delegators in Ruby

Ruby's metaprogramming facilities enable us to implement delegation in a much easier and consise manner than many other object oriented languages.

Using dynamic interception of method calls, it is very straightforward to forward method invocations to a target object. This is very well illustrated by the implementation of Delegate class provided by Ruby standard library:

class Delegator < BasicObject

  #...

  #
  # Handles the magic of delegation through \_\_getobj\_\_.
  #
  def method_missing(m, *args, &block)
    r = true
    target = self.__getobj__ {r = false}

    if r && target.respond_to?(m)
      target.__send__(m, *args, &block)
    elsif ::Kernel.respond_to?(m, true)
      ::Kernel.instance_method(m).bind(self).(*args, &block)
    else
      super(m, *args, &block)
    end
  end

end

Wrapping objects with SimpleDelegator

In practice we usually deal with SimpleDelegator subclass of Delegator more often. The constructor takes a single object and any method invocations are delegated to the target instance.

We can simply subclass from SimpleDelegator and implement our customizations therein, and delegate to the parent object through a super call conveniently.

Example from docs:

class User
  def born_on
    Date.new(1989, 9, 10)
  end
end

class UserDecorator < SimpleDelegator
  def birth_year
    born_on.year
  end
end

> user = UserDecorator.new(User.new)

Using delegation for Presenter logic

This is arguably the most common use case for delegators. Often we end up adding a lot of methods in our models that have nothing to do with domain logic whatsoever. Does something like this look familiar?

class User

  # ...

  def formatted_date_of_birth
    date_of_birth.strftime("Born on %m/%d/%Y")
  end

end

Such methods which are primarily written for handling presentation concerns come into the perview of Presenter logic and are best left out of models. Instead we can decorate our model instances using presenter classes before passing them to views:

class UserDecorator < SimpleDelegator

 def formatted_date_of_birth
  date_of_birth.strftime("Born on %m/%d/%Y")
 end

end
class UsersController < ApplicationController

  def show
    user = User.find params[:id]
    @user = UserDecorator.new(user)
  end

end

I find this to be more elegant from an object oriented perspective compared to the conventional approach using Rails helper modules.

There are many libraries for implementing additional convenience utilities around presenters. While the above simple approach takes us quite far, if you find yourself repeating the decorator instantiation boilerplate, generators and conventions offered by a library like Draper can be helpful.

Note that one of the biggest strengths of decorators is on demand composability. For instance we may have various user centric helper methods for reporting. We can extract them into a UserReportPresenter decorator that is used only in the UserReportsController. Accordingly we can have multiple use-case specific delegators layered one upon another, each delegating to its immediate target transparently.

We may want to have some approach to restrict what can be decorated by a decorator. We may be tempted to define something like this:

class UserReportPresenter < SimpleDecorator

  def initialize(decorated)
    unless decorated.is_a? User
      raise ArgumentError.new("Expected entity being decorated to be User")
    end
    super
  end

end

However this is not something we would want to do as it breaks composability. One approach would be to "unwrap" the decorated before instance check:

while decorated.is_a? Decorator
  decorated = decorated.__getobj__
end
unless decorated.is_a? User
  raise ArgumentError.new("Expected entity being decorated to be User")
end

but I strongly recommend not resorting to is_a? checks at all and relying instead on behavior checks using responds_to?.

Delegation for memoization/caching

This is another good use case for delegation. We can wrap our models into Delegators that transparently cache or memoize method invocations:

Example using Rails.cache:

class UserReportCachedDelegator < SimpleDelegator

  def annual_performance_stats
    Rails.cache.fetch "user.#{id}.annual_performance_stats" do
      super
    end
  end

end

However one thing that we should keep in mind that SimpleDelegator allows the decorated target to be run time configurable. While this is not a problem in the above implementation as we use an entity specific key, this may become a problem if, for example, we were using transparent memoization through Memoist:

class UserReportMemoizedDelegator < SimpleDelegator

  extend Memoist

  def annual_performance_stats; super; end
  memoize :annual_performance_stats

end

The above implementation is broken because even if we were to change the decorated instance using __setobj__ our memoized method would continue returning the output of invocation of the method on previously decorated instance.

A simple solution for the above is to flush the cache when the decorated entity changes:

class MemoizedDelegator < SimpleDelegator

  extend Memoist

   def __setobj__(*args)
     super
     flush_cache
   end

end

Using delegation for collection objects

This is something I recently found to be useful. Sometimes when we need to define operations that make sense for a set of instances, we just resort to class methods in model. However a better object oriented design would be to implement such behaviors on a dedicated collection resource.

Decorating ActiveRecord::CollectionProxy is helpful because we get facilities like scope chaining, lazy-loading etc. for free.

class EmployeesReportDelegator < SimpleDelegator

  def month_wise_performance_stats
    joins(:assessments)
      .group('MONTH(assessments.created_at)')
      .select('assessments.evaluation_rank as rank')
  end

end

Using delegation to augment lifecycle hooks

This is occassionaly useful especially when dealing with third party SDKs. We can leverage ActiveSupport::Callbacks to wrap custom callback hooks around specific behaviors of decorated objects:

class MailDispatchDelegator < SimpleDelegator

  include ActiveSupport::Callbacks
  define_callbacks :dispatch

  def dispatch
    run_callbacks :dispatch do
      super      
    end
  end

end

class MailFilterDelegator < MailDispatchDelegator

  set_callback :dispatch, :before do |object|
    if validated_domains.include? object.email.domain
      raise ValidationError.new("Domain not whitelisted")
    end
  end

end

In Rails, any module can delegate

While creating a dedicated Delegator makes sense in a variety of use cases, often we want to just delegate just a few methods to a contained object. ActiveSupport Module extensions provide a convenient approach to delegate specific methods to any contained object. This comes in very handy in controllers:

Delegating helpers to model instances:

For example in a rails controller, we might want to expose a few model methods:

class ProductsController < ApplicationController

  before_action :ensure_logged_in!
  delegate :available_products, to: :current_user

  def index
    @available_products = available_products
  end

end

We can also simply expose the delegated methods as a helper_method to our views:

class ProductsController < ApplicationController

  before_action :ensure_logged_in!
  delegate :available_products, to: :current_user
  helper_method :available_products

end

I have found this to be a common use case, and a class method that wraps the two can help in DRYing things up:

def self.delegate_helper *args
  delegate *args
  loop do
    method_name = args.shift
    break unless method_name.is_a? Symbol
    helper_method method_name
  end
end

The above method allows us to take advantage of full api of Module#delegate which allows delegation of multiple methods to single target and configuration of generated method.

So for example, we can do the following:

class SomeController < ApplicationController

  before_action :ensure_logged_in!
  delegate_helper :designations, :products, 
                  to: :current_user, 
                  prefix: true

end

Now delegated methods would be available as current_user_designations and current_user_products in our views.

Module#delegate works very well with instance variables and class variables as well. Here is an example from the official documentation :

class Foo
  CONSTANT_ARRAY = [0,1,2,3]
  @@class_array  = [4,5,6,7]

  def initialize
    @instance_array = [8,9,10,11]
  end
  delegate :sum, to: :CONSTANT_ARRAY
  delegate :min, to: :@@class_array
  delegate :max, to: :@instance_array
end

Foo.new.sum # => 6
Foo.new.min # => 4
Foo.new.max # => 11

Chaining delegators

As you may have inferred at this point, we can easily chain delegated invocations:

class ProductsController < ApplicationController

  before_action :ensure_logged_in!
  delegate :manager, to: :current_user
  delegate :designation, to: :manager, prefix: :manager

end

Delegation as an alternative to Concerns

Since the official endorsement of concerns from Rails 4 concerns have soared in popularity, however I have often observend that concerns are overused, especially for use cases that are better handled by other patterns.

Since this post is about delegation, let us look at a few things that delegation has to offer over concerns:

Run time configurability:

While it is true that concerns can be injected at run time, however code that runs in the included hooks potentially modifies the host instance making run time switching of concerns not very practical in most cases. Delegation offers a clearer approach and we have already demonstrated that switching delegated instances in delegator instances is quite useful.

On demand specialization:

This is particularly useful for objects that did not originate in our code. Rather than polluting the library generated objects with application specific behavior, which may cause subtle unintended side-effects, it is much more elegant to pass around decorated instances in application code and pass the unwrapped instances back to the library should there be a need to do so.

All in all concnerns are more suitable for application specific classes where core functionality is shared among multiple classes, and delegation is more useful for implementing auxiliary use case specific behaviors or transparently augmenting existing behavior.

A good example of former use case would be a cross-cutting functionality like using a completed_at column for scoping on completion status or checking if a model (eg. Payment, Project etc.) has been completed :

module CompletionSupport
  extend ActiveSupport::Concern

  included do

    %i[complete completed].each do |name|
      scope name, -> { where 'completed_at is not null' }
    end

    scope :incomplete, -> { where completed_at: nil }
  end

  def complete!
    self.completed_at = DateTime.now
    save!
  end

  def complete?
    completed_at.present?
  end

  alias_method :completed?, :complete?

  def incomplete?
    ! complete?
  end

  alias_method :not_completed?, :incomplete?

end

Delegation in command line applications

Last but not the least, delegation makes sense in command line applications as well. An excellent example would be github's hub command line utility that makes a lot of github features like pull-requests accessible from the command line, while delegating everything else to git.

Some alternatives

Observers

While observer pattern can help towards decoupling and the advantags therein overlap with those offered by delegates in some cases, in general I refrain from using Observer pattern because it makes the flow of logic harder to trace - especially in larger applications.

Refinements

Refinements are a recent addition to the Ruby language where in we can selectively monkey-patch modules (and hence classes) with custom behavior. But a key aspect of refinements is lexical scoping. The offical docs explain this very well:

Refinements are lexical in scope. When control is transferred outside the scope the refinement is deactivated. This means that if you require or load a file or call a method that is defined outside the current scope the refinement will be deactivated:

class C
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

def call_foo(x)
  x.foo
end

using M

x = C.new
x.foo       # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError

While this explicit aspect of Refinements is a commendable improvement over adhoc monkey-patching in many scenarios - the convenience offered by transparent overlaying of behavior that decorators offer us, is, in most practical cases, more appealing.

The primary exception to the above would be cases where the code consuming the augmented instances rely on instance_of? checks to determine the identity of passed instance. Using refinements we are not changing the class of the instance, where as while passing the wrapped instance we fail on the instance_of? checks as the wrapped instances are actually instances of a different class though they implement interchangeable behavior.

This concludes our post on delegation. As always any insights on pragmatic usage of this pattern is more than welcome. Please use the comments section to share any feedback or criticism.