Hello guys!
I’d like to propose new feature for ActiveSupport Module’s delegate method. I’ve actually implemented it already and i want your opinion on this matter. Recently during my work hours, i was writing enumerable proxy-object to encapsulate some business-logic and i wanted it to mimic Array functionally as much as possible, though behave in slightly different way.
I wanted to do it with composition, because i wholeheartedly agree with Steve Klabnik’s article on ‘subclassing vs composition’ here:
http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
Delegate method quickly came to my mind, though i had one concern: delegated methods on proxy object must return proxy object itself to enable chaining of filters and such.
I’ll demonstrate it with example, let’s look on standard delegation:
class ProxyContainer < Struct.new(:array)
delegate :select!, to: :array
end
proxy = ProxyContainer.new(%w{a b c})
=> #<struct ProxyContainer array=[“a”, “b”, “c”]>
proxy.select! { |el| el != ‘a’ }
=> [“b”, “c”]
If we want to apply some business-logic filter and such, residing in proxy object, we must assign intermediate value to some variable, we cannot chain methods.
On the opposite side look at this delegation example with return_value_of in use:
class ProxyContainer < Struct.new(:brands)
delegate :select!, to: :brands, return_value_of: :self
def reject_unprofitable!
some business-logic here
end
def profitable
some business-logic here too
@brands # profitable brands
end
def business_criteria
criteria logic
end
end
Now we can do stuff like this:
proxy = ProxyContainer.new([#brand objects#])
=> #<struct ProxyContainer brands=[#brand objects#]>
proxy.select!(&:criteria).reject_unprofitable!
=> #<struct ProxyContainer brands=[#profitable brands#]>
If we want to use standard behaviour:
proxy.select!(&:criteria).profitable
=> [#profitable brands#]
It’s kind of switching default focus in a favor of proxy, making instance variable returns explicit in user-made logic.
Look at how simple it looks and feels. It doesn’t break backward compatibility and it will be easy to refactor, if it’ll come to this(maybe it is, by the looks of github issues).
I’ve added test and implementations and squashed to this commit:
https://github.com/dredozubov/rails/commit/81940715a2137ae765419d338d5c1704b1e54d8b
I can open parallel PR on github if needed.
I’ll be glad to update docs(or help to update them - my english isn’t perfect), if you will approve this one.