Skip to content

Delegator and SimpleDelegator in Ruby's stdlib are useful, but they pull in most of Kernel. This is not appropriate for many uses; for instance, delegation to Rails models.

License

Notifications You must be signed in to change notification settings

stevenharman/dumb_delegator

Repository files navigation

DumbDelegator

Gem Version CI Maintainability Test Coverage

Ruby provides the delegate standard library. However, we found that it is not appropriate for cases that require nearly every call to be proxied.

For instance, Rails uses #class and #instance_of? to introspect on Model classes when generating forms and URL helpers. These methods are not forwarded when using Delegator or SimpleDelegator.

require "delegate"

class MyAwesomeClass
  # ...
end

o = MyAwesomeClass.new
d = SimpleDelegator.new(o)

d.class                #=> SimpleDelegator
d.is_a? MyAwesomeClass #=> false

DumbDelegator, on the other hand, forwards almost ALL THE THINGS:

require "dumb_delegator"

class MyAwesomeClass
  # ...
end

o = MyAwesomeClass.new
d = DumbDelegator.new(o)

d.class                #=> MyAwesomeClass
d.is_a? MyAwesomeClass #=> true

Installation

Add this line to your Gemfile:

gem "dumb_delegator"

And then install:

$ bundle

Or install it yourself:

$ gem install dumb_delegator

Versioning

This project adheres to Semantic Versioning.

Version 0.8.x

The 0.8.0 release was downloaded 1.2MM times before the 1.0.0 work began. Which is great! 🎉 But, we wanted to clean up some cruft, fix a few small things, and improve ergonomics. And we wanted to do all of that while, hopefully, not breaking existing usage.

To that end, 1.0.0 dropped support for all EoL'd Rubies and only officially supported Ruby 2.4 - 2.7 when it was released. However, most older Rubies, should still work. Maybe… Shmaybe? Except for Ruby 1.9, which probably does not work with DumbDelegator > 1.0.0. If you're on an EoL'd Ruby, please try the 0.8.x versions of this gem.

Usage

DumbDelegator's API and usage patters were inspired by Ruby stdlib's SimpleDelegator. So the usage and ergonomics are quite similar.

require "dumb_delegator"

class Coffee
  def cost
    2
  end

  def origin
    "Colombia"
  end
end

class Milk < DumbDelegator
  def cost
    super + 0.4
  end
end

class Sugar < DumbDelegator
  def cost
    super + 0.2
  end
end

coffee = Coffee.new

cup_o_coffee = Sugar.new(Milk.new(coffee))
cup_o_coffee.origin        #=> Colombia
cup_o_coffee.cost          #=> 2.6

# Introspection
cup_o_coffee.class         #=> Coffee
cup_o_coffee.__getobj__    #=> #<Coffee:0x00007fabed1d6910>
cup_o_coffee.inspect       #=> "#<Sugar:70188197507600 obj: #<Milk:70188197507620 obj: #<Coffee:0x00007fabed1d6910>>>"
cup_o_coffee.is_a?(Coffee) #=> true
cup_o_coffee.is_a?(Milk)   #=> true
cup_o_coffee.is_a?(Sugar)  #=> true

Rails Model Decorator

There are many decorator implementations in Ruby. One of the simplest is "SimpleDelegator + super + __getobj__," but it has the drawback of confusing Rails. It is necessary to redefine #class, at a minimum. If you're relying on Rails' URL Helpers with a delegated object, you also need to redefine #instance_of?. We've also observed the need to redefine other Rails-y methods to get various bits of 🧙 Rails Magic 🧙 to work as expected.

With DumbDelegator, there's not a need for redefining these things because nearly every possible method is delegated.

Optional case statement support

Instances of DumbDelegator will delegate #=== out of the box. Meaning an instance can be used in a case statement so long as the when clauses rely on instance comparison. For example, when using a case with a regular expression, range, etc...

It's also common to use Class/Module in the where clauses. In such usage, it's the Class/Module's ::=== method that gets called, rather than the #=== method on the DumbDelegator instance. That means we need to override each Class/Module's ::=== method, or even monkey-patch ::Module::===.

DumbDelegator ships with an optional extension to override a Class/Module's ::=== method. But you need to extend each Class/Module you use in a where clause.

def try_a_case(thing)
  case thing
  when MyAwesomeClass
    "thing is a MyAwesomeClass."
  when DumbDelegator
    "thing is a DumbDelegator."
  else
    "Bad. This is bad."
  end
end

target = MyAwesomeClass.new
dummy = DumbDelegator.new(target)

try_a_case(dummy) #=> thing is a DumbDelegator.

MyAwesomeClass.extend(DumbDelegator::TripleEqualExt)

try_a_case(dummy) #=> thing is a MyAwesomeClass.

Overriding Module::===

If necessary, you could also override the base Module::===, though that's pretty invasive.

🐲 There be dragons! 🐉

::Module.extend(DumbDelegator::TripleEqualExt)

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Contribution Ideas/Needs

  1. Ruby 1.8 support (use the blankslate gem?)

About

Delegator and SimpleDelegator in Ruby's stdlib are useful, but they pull in most of Kernel. This is not appropriate for many uses; for instance, delegation to Rails models.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages