[Talking-Ruby] ActiveSupport Delegate module implement the delegation pattern

If your project includes ActiveSupport, and every Rails project does, you have a more clean and easy way to implement the delegation pattern: the Module#delegate extension. It provides a delegate module you can use in your class or in your modules to delegate a specific method to an associate object.

#delegate provides a delegate class method to easily expose contained objects’ public methods as your own.

delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)

Options

  • :to - Specifies the target object name as a symbol or string

  • :prefix - Prefixes the new method with the target name or a custom prefix

  • :allow_nil - If set to true, prevents a Module::DelegationError from being raised

  • :private - If set to true, changes method visibility to private

The macro receives one or more method names (specified as symbols or strings) and the name of the target object via the :to option (also a symbol or string).

Delegation is particularly useful with Active Record associations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Greeter < ActiveRecord::Base
def hello
'hello'
end

def goodbye
'goodbye'
end
end

class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, to: :greeter
end

Foo.new.hello # => "hello"
Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>

Multiple delegates to the same target are allowed:

1
2
3
4
5
6
class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, :goodbye, to: :greeter
end

Foo.new.goodbye # => "goodbye"

Methods can be delegated to instance variables, class variables, or constants by providing them as a symbols:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

It’s also possible to delegate a method to the class by using :class:

1
2
3
4
5
6
7
8
9
class Foo
def self.hello
"world"
end

delegate :hello, to: :class
end

Foo.new.hello # => "world"

Delegates can optionally be prefixed using the :prefix option. If the value is true, the delegate methods are prefixed with the name of the object being delegated to.

1
2
3
4
5
6
7
8
9
10
Person = Struct.new(:name, :address)

class Invoice < Struct.new(:client)
delegate :name, :address, to: :client, prefix: true
end

john_doe = Person.new('John Doe', 'Vimmersvej 13')
invoice = Invoice.new(john_doe)
invoice.client_name # => "John Doe"
invoice.client_address # => "Vimmersvej 13"

It is also possible to supply a custom prefix.

1
2
3
4
5
6
7
class Invoice < Struct.new(:client)
delegate :name, :address, to: :client, prefix: :customer
end

invoice = Invoice.new(john_doe)
invoice.customer_name # => 'John Doe'
invoice.customer_address # => 'Vimmersvej 13'

The delegated methods are public by default. Pass private: true to change that.

1
2
3
4
5
6
7
8
9
10
11
12
13
class User < ActiveRecord::Base
has_one :profile
delegate :first_name, to: :profile
delegate :date_of_birth, to: :profile, private: true

def age
Date.today.year - date_of_birth.year
end
end

User.new.first_name # => "Tomas"
User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
User.new.age # => 2

If the target is nil and does not respond to the delegated method a Module::DelegationError is raised. If you wish to instead return nil, use the :allow_nil option.

1
2
3
4
5
6
7
class User < ActiveRecord::Base
has_one :profile
delegate :age, to: :profile
end

User.new.age
# => Module::DelegationError: User#age delegated to profile.age, but profile is nil

But if not having a profile yet is fine and should not be an error condition:

1
2
3
4
5
6
class User < ActiveRecord::Base
has_one :profile
delegate :age, to: :profile, allow_nil: true
end

User.new.age # nil

Note that if the target is not nil then the call is attempted regardless of the :allow_nil option, and thus an exception is still raised if said object does not respond to the method:

1
2
3
4
5
6
7
8
9
class Foo
def initialize(bar)
@bar = bar
end

delegate :name, to: :@bar, allow_nil: true
end

Foo.new("Bar").name # raises NoMethodError: undefined method `name'

The target method must be public, otherwise it will raise NoMethodError.

References

[1] Module | Ruby on Rails 6.1.4 Class - https://api.rubyonrails.org/classes/Module.html#method-i-delegate

[2] Active Support Core Extensions — Ruby on Rails Guides - https://guides.rubyonrails.org/active_support_core_extensions.html#method-delegation

[2] Ruby’s Hidden Gems: Delegator and Forwardable | AppSignal Blog - https://blog.appsignal.com/2019/04/30/ruby-magic-hidden-gems-delegator-forwardable.html

[3] Understanding delegate in Ruby on Rails | by Meraj Molla | ITNEXT - https://itnext.io/understanding-delegate-in-ruby-on-rails-i-wish-i-knew-before-5edd341bad47

[4] Understanding Ruby and Rails: Delegate — Simone Carletti - https://simonecarletti.com/blog/2009/12/inside-ruby-on-rails-delegate/

[5] class SimpleDelegator - Documentation for Ruby 3.0.0 - https://docs.ruby-lang.org/en/3.0.0/SimpleDelegator.html

[6] Module: Forwardable (Ruby 3.0.0) - https://ruby-doc.org/stdlib-3.0.0/libdoc/forwardable/rdoc/Forwardable.html

[7] saturnflyer/casting: Delegate methods in Ruby and preserve self. Add behaviors to your objects without altering their superclass hierarchy. - https://github.com/saturnflyer/casting

[8] How To Delegate Methods in Ruby & Ruby on Rails - RubyGuides - https://www.rubyguides.com/2018/10/delegate-methods-in-ruby/