If you're going to add a hook, make it a big one
Jay Fields responds to Ola Bini’s Evil Hook Methods? about the common ruby idiom that lets us write:
class Fruit
include DataMapper::Resource
property :id, Integer, :serial => true
property :name, String
property :notes, Text, :lazy => false
end
What Ola and Jay don’t like about that is the way that a single
include DataMapper::Resource
actually adds class methods to Fruit
because the implementation of DataMapper::Resource.included
looks
like:
Jay Fields responds to Ola Bini’s Evil Hook Methods? about the common ruby idiom that lets us write:
class Fruit
include DataMapper::Resource
property :id, Integer, :serial => true
property :name, String
property :notes, Text, :lazy => false
end
What Ola and Jay don’t like about that is the way that a single
include DataMapper::Resource
actually adds class methods to Fruit
because the implementation of DataMapper::Resource.included
looks
like:
module DataMapper::Resource
def included(module)
module.send :include, InstanceMethods
module.send :extend, ClassMethods
end
end
Which is a perfectly common idiom nowadays, but which breaks include
’s
contract in annoying ways. Jay proposes fixing this by adding a become
method to Object which would wrap the include
and extend
in such
away that they’d be called by the including class. Huzzah. And it makes
sense… sort of. But it really doesn’t go far enough.
Let’s take another look at the original code snippet shall we? The thing
that I notice is the wide scope of that ‘property’ method. It really
isn’t needed anywhere except for defining how a Fruit
is mapped onto
the database. What happens if we take a leaf out of Perl’s book:
class Fruit
use DataMapper::Resource {
property :id, Integer, :serial => true
property :name, String
property :notes, Text, :lazy => false
}
property :foo # => raises an exception
end
The block gives our extending module somewhere to play, it can introduce
a full on domain specific pidgin for the duration of the block with no
fear of polluting the including class with anything but the methods its
contracted to provide. So, how do we implement use
. Something like the
following should serve the purpose:
class Module
def self.use(mod, *args, &block)
mod.used_by(self, *args, &block)
end
def self.used_by(mod, *args, &block)
if instance_behaviours || class_behaviours
mod.become(self)
else
mod.send(:include, self)
end
end
def self.become(mod)
include mod.instance_behaviours) if mod.instance_behaviours
extend mod.class_behaviours if mod.class_behaviours
end
def self.instance_behaviours
nil
end
def self.class_behaviours
nil
end
end
The key idea here is that, in the default case, use
will ignore all
its arguments beyond the first and just include that module (a more
robust implementation would probably ensure that an exception was raised
if any extra arguments got passed). If the module author had written her
module to comply with Jay’s proposed become
, then we simply call
become
.
The interesting stuff happens when a module wants to do something a little more trick. So a version of DataMapper might do something like:
class DataMapper::Resource
def self.used_by(mod, &block)
mod.become build_behaviours(mod, &block)
end
end
And build_behaviours
would instance_eval
the block with an object
that would capture the properties and use them to build a set of class
and instance methods appropriate to the description.
Another module might simply take a hash to describe how things should be
parameterized. It all depends on the needs of the module being used. The
aim being to avoid polluting the caller’s namespace any more than
necessary. If I use a DataMapper type package, then all I want to end up
with in my client classes are appropriate instance accessor methods, I
don’t need spare class methods like property
or storage_names
that
are only of any use when I’m describing my class.
Updates
I edited one of the code snippets to remove a particularly heinous piece of brace matching. Thanks to Giles Bowkett for the catch. Also edited another snippet to make it into real ruby rather than some bastard combination of Ruby and Perl. Thanks to Yossef for that catch.
10 historic comments »
By Giles Bowkett Mon, 08 Sep 2008 03:23:44 GMT
I think starting a block with {
and ending it with end
is evil. Either
{}
, or do/end
. Anything else is the work of the devil.
Further, I demand that Matz alter the language to prevent people from using it in a way that I personally do not approve of.
I demand satisfaction!
By Piers Cawley Mon, 08 Sep 2008 04:12:40 GMT
Christ! If that works then consider me boggled.
I’ll fix the typo immediately sir!
By Jamie Macey Mon, 08 Sep 2008 09:20:53 GMT
This isn’t a workable solution, as it doesn’t provide a mechanism for the other important public class-methods like Model.all and Model.first.
By Piers Cawley Mon, 08 Sep 2008 11:19:42 GMT
@Jamie: How do you work that out?
The instance methods generated by build_behaviours
get put on the
instance_behaviours
attribute of whatever it returns, the public
class methods get put on the class_behaviours
attribute. Problem
solved.
used_by
gets license to do whatever the hell it likes to its user
after all. Yes, so can include/included
, but that’s not how its
contract is documented, so it’s problematic. A new interface for
extending modules and classes gets to write its own contract, the
trick is coming up with a minimal interface which allows for richer
possibilities than the existing interfaces.
Experience with Perl shows that having an extension method that you can pass arbitrary arguments allows a great deal of flexibility in this area.
By Yossef Tue, 09 Sep 2008 22:59:50 GMT
sub
included? Someone’s been thinking about Perl just a little too
much.
Also, there’s no reason to use send to extend an object. That’s a public method.
By Piers Cawley Wed, 10 Sep 2008 01:25:12 GMT
Argh! Thanks for spotting the sub. You should see what happens when I try to write Perl nowadays…
I haven’t changed the send :extend
part though, mostly because of
the company it’s keeping in its method.
def included(module)
module.send :include, instance_behaviours
module.extend, :class_behaviours
end
Just looks weird.
By Yossef Wed, 10 Sep 2008 22:01:09 GMT
I tried to write some Perl after spending at least a year away. It was
an awkward, bumbling experiment. I couldn’t even remember that I had
to use package
.
I guess what I meant to say was that there’s no reason other than
aesthetics to use send
to extend an object. It’s valid enough in
this context, but there are times I’ve seen it on its own, and that’s
just wrong. And note that I whole-heartedly agree with your comment on
Jay Fields’s post regarding extend
vs. extend_with
.
By Jamie Macey Thu, 11 Sep 2008 17:25:35 GMT
@Piers I must have missed that 4th code block that handled the include/extend (where the class methods obviously go) and was just looking at the 3rd one, with the block for local extension.
By Mr eel Fri, 12 Sep 2008 01:04:51 GMT
Personally, I think using include in this way has become a common and well-understood idiom. Doing backflips to avoid it isn’t really worthwhile.
By Piers Cawley Sun, 14 Sep 2008 16:18:00 GMT
@Mr eel: I tend to agree with you. If all we were buying was avoiding that, I wouldn’t be bothered. However, I think there’s value in something which makes it easy to pass an argument or block to the mixed in module as it’s mixed in.