ClosureForwardable
The ClosureForwardable
module provides delegation of specified methods to a designated object, using the methods delegate
, def_delegator
, and def_delegators
. It allows to define methods in a class or module which will be forwarded to any arbitrary object. This allows the user of this module to build meta-objects which provide a unified access to other objects which can be of great help when trying to adhere to the Law of Demeter.
This module is intended to be used very similar to the Forwardable
module in the Ruby standard library. In fact, it provides almost the same interface and can be used in its place.
Generally, you should use the simple Forwardable
module if possible as method calls will be slightly faster while providing the same functionality.
Use ClosureForwardable
if you need to forward methods to a receiver that is not available by the including module itself. Using ClosureForwardable
, you can forward methods to arbitrary objects.
Installation
Add this line to your application's Gemfile:
gem 'closure_forwardable'
And then execute:
$ bundle
Or install it yourself as:
$ gem install closure_forwardable
Usage
The closure_forwardable
gem provides a single module called ClosureForwardable
. You can use it to delegate methods on to arbitrary other objects.
In the following example, we are going to define two classes, Consumer
and Producer
whose instances share a single queue object.
class Consumer
extend ClosureForwardable
def consume!
return unless any?
element = get_element
puts element.inspect
element
end
end
class Producer
extend ClosureForwardable
end
queue = []
# Add the methods any? to the Consumer class which simply calls any on our
# single queue object.
Consumer.def_delegator queue, :any?
# We also add a get_element method to the Consumer which in turn calls shift
# on the queue.
Consumer.def_delegator queue, :shift, :get_element
# Finally, we add the queue! method to the Producer class which calls the
# push method of the queue array.
Producer.def_delegator queue, :push, :queue!
producer = Producer.new
producer.queue! :job1
producer.queue! :job2
queue
# => [:job1, job2]
consumer = Consumer.new
consumer.consume!
# => job1
consumer.consume!
# => job2
consumer.consume!
# => nil
queue
# => []
Note that all instances of the classes share the same single queue
object. Also note that the queue is not added in any way to the classes. There are no instance variables or accessors being defined on the classes. The queue is just made available to the delegation methods and is effectively invisible to the rest of the class.
This is the main difference to the Forwardable
module of the Ruby standard library: for Forwardable to be able to delegate the methods, it needs to be able to ge access to the delegation object from
the class instance. Typically, this is achieved using accessors or instance variables. When using ClosureForwardable
, this is no longer necessary as we can delegate methods to any arbitrary object.
Why does this work?
The delegated object is in fact made available to the generated method. However, it is only visible though a block closure. With this mechanism which is provided by the block implementation of Ruby, when a block is executed, it can access all local variables which where available at the time the block was defined. This allows us to "hide" the delegation object inside a block's closure which forms the body of the method we define on our class.
Note that because of this, Ruby's garbage collector is intelligent enough to not destroy the object we delegate to as long as it is part of any block's closure. That means, when using ClosureForwardable
, we strongly tie the forwarded object to the class. The delegation object will continue to be live as long as the class which delegates to it exists (thus typically forever).
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/meineerde/closure_forwardable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.