ruby / delegate

This library provides three different ways to delegate method calls to an object.
BSD 2-Clause "Simplified" License
18 stars 13 forks source link

Define `DelegateClass` methods in separate module #14

Open jonathanhefner opened 1 year ago

jonathanhefner commented 1 year ago

Before this commit, modules included in a DelegateClass could not override delegate methods:

  Base = Class.new do
    def foo
      "base"
    end
  end

  Helper = Module.new do
    def foo
      "helper"
    end
  end

  WithHelper = DelegateClass(Base) { include Helper }

  WithHelper.new(Base.new).foo
  # => "base"

This commit defines delegate methods in a separate module, so other modules can come before it in the method lookup chain:

  WithHelper.new(Base.new).foo
  # => "helper"

Also, because of this change, methods in a DelegateClass block will properly override instead of redefine. Therefore, calling super is faster:

Benchmark script

  # frozen_string_literal: true
  require "benchmark/ips"
  $LOAD_PATH.prepend(".../delegate/lib")
  require "delegate"

  Base = Class.new do
    def foo
    end
  end

  Overridden = DelegateClass(Base) do
    def foo
      super
    end
  end

  overridden = Overridden.new(Base.new)

  Benchmark.ips do |x|
    x.report("super") { overridden.foo }
  end

Before

  Warming up --------------------------------------
                 super    75.044k i/100ms
  Calculating -------------------------------------
                 super    759.506k (± 0.8%) i/s -      3.827M in   5.039488s

After

  Warming up --------------------------------------
                 super   184.164k i/100ms
  Calculating -------------------------------------
                 super      1.835M (± 1.0%) i/s -      9.208M in   5.019711s

Fixes https://bugs.ruby-lang.org/issues/19079.

byroot commented 1 year ago

https://bugs.ruby-lang.org/issues/19074#note-3

nobu commented 1 year ago

Why not prepend Helper?

byroot commented 1 year ago

@nobu sure you can do that, but you'd have a similar-ish issue with just defining a method in the delegator:

Foo = Struct.new(:field)
FooDelegator = DelegateClass(Foo) do
  def field
    super.to_s
  end
end

In this example super doesn't call the method that DelegateClass generated, but fallback to Delegator#method_missing, it works but kinda defeat the purpose.

It's somewhat assumed that if you create a delegator, you will want to specialize a few methods.