DataMapper allows you to gain it's methods simply by an include statement.
include DataMapper::Resource
# ...
end
Ola points out that the include method should not add class methods. At least, that's not what it was designed to do. Include should (by way of append_features) add the constants, methods, and module variables of this module to the class or module that called include.
The problem for me is: Should DataMapper add it's methods to your class when you use Ruby methods as they were originally intended, or when you use DataMapper's Domain Specific Language (DSL).
If DataMapper is a framework and should be used traditionally then you should add it's methods in the following way.
include DataMapper::Resource
extend DataMapper::Resource::ClassMethods
# ...
end
However, you can't blame DataMapper for following the pattern that's been around since long before DataMapper. At this point I would consider the trick to definitely be an idiom even if it is an anti-pattern. The reality is that include has been stolen by those that prefer the simplest possible Domain Specific Language for adding their behavior.
Martin Fowler describes how a framework can have a traditional API as well as a thin veneer that allows you to use the framework in a more fluent way.
Unfortunately, in the Ruby world we've designed our veneer in a way that doesn't allow for traditional usage.
The other day I noticed something that I thought was equally interesting in the Java world. I was working on a test that used JMock and IntelliJ formatted my code as shown below.
1
2 Mockery mockery = new Mockery();
3
4 {
5 final Subscriber subscriber = context.mock(Subscriber.class);
6
7 mockery.checking(new Expectations() {
8 {
9 one (subscriber).receive(message);
10 }
11 });
12
13 // ...
14 }
15 } {
Unimpressed by lines 8 and 10, I changed the code to look like the following snippet.
Mockery mockery = new Mockery();
{
final Subscriber subscriber = context.mock(Subscriber.class);
mockery.checking(new Expectations() {{
one (subscriber).receive(message);
}});
// ...
}
} {
Mike Ward said I shouldn't do that because the IntelliJ formatting properly shows an initializer for an anonymous class. Which is absolutely correct, but I don't want an anonymous class with an initializer, I want to use JMock's DSL for defining expectations. And, while the second version might not highlight how those expectations are set, that's not what I care about.
When I write the code I want to create expectations in the easiest way possible, and when I read the code I want the fact that they are expectations to be obvious. I don't think removing lines 8 and 10 reduces readability, in fact it may improve it. Truthfully, I don't care what tricks JMock uses to define it's DSL (okay, within reason), I only care that the result is the most readable option possible.
Back to DataMapper, I believe there's a superior option that allows them to have both a clean DSL and a traditional API. The following code would allow you to add methods as Ola desires (traditionally) and it would allow you to get everything with one method invocation for those that prefer DSL syntax.
include DataMapper::Resource
extend DataMapper::Resource::ClassMethods
end
end
include DataMapper::Resource
extend DataMapper::Resource::ClassMethods
# ...
end
data_mapper_resource
# ...
end
The obvious drawback is if everyone starts adding methods to Object we may start to see method collision madness. Of course, if the method names are given decent names it shouldn't be an issue. It's not likely that someone else is going to want to define a method named data_mapper_resource.
Don't worry. For those of you who prefer complexity "just in case", I have a solution for you also.
; end
DataMapper::Resource::InstanceMethods
end
DataMapper::Resource::ClassMethods
end
end
"instance method"
end
end
"class method"
end
end
include mod.instance_behaviors
extend mod.class_behaviors
end
end
include DataMapper::Resource::InstanceMethods
extend DataMapper::Resource::ClassMethods
# ...
end
become DataMapper::Resource
end
Entry.class_method # => "class method"
Entry.new.instance_method # => "instance method"
Category.class_method # => "class method"
Category.new.instance_method # => "instance method"
I'm afraid the margin of this blog post (or at least, it's utility for writing code snippets) is to narrow for the comment I want to write, so I've blogged about it instead.
ReplyDeleteA quick comment about your mention of "append_features": Append features is a deprecated precursor to the included method which does not require that you invoke super like append_features did.
ReplyDeleteI agree with Rick's comment in Ola Bini post.
ReplyDelete"Why not put the include line in the #do_funky_madness_on (alias become) method?"
module Speakable
def self.become(klass)
klass.send :include, InstanceMethods
klass.extend ClassMethods
end
end
class Person
Speakable.become(self)
end
If the issue is clarity of "what is happening here?" perhaps you could alias #include to be #include_and_extend which you would use instead to tell the reader that including this module is going to also extend the class?
ReplyDelete@charly: One obvious problem with that is that you're telling Speakable to become a Person, which seems arse about face somehow.
ReplyDeleteThe obvious method to use there is extend, but that's already taken for a method that would probably be better named extend_with or similar.
Ho hum.
Why add these methods to Object instead of Class?
ReplyDelete@ymendel, yes, that's probably better.
ReplyDeleteCheers, Jay
Do you expect any module passed to Object#become to implement the instance_behaviour and class_behaviour methods in your solution?
ReplyDeleteclass Object
def become(mod)
include mod.instance_behaviors
extend mod.class_behaviors
end
end
@linh, yes.
ReplyDeleteBy the way, it's "its" when talking about DataMapper's methods.
ReplyDeleteAt any rate, nice article! The `become' method is interesting, though I'm still working the internal debate on if I agree with it or not.
I agree in that modifying the expected behavior of "include" is bad practice, but IMO the notion that your "data_mapper_resource" gives the user different choices seems a bit backwards in terms of improving readability... In the case of Datamapper, the library is meant for a specific purpose. The more options you provide to do exactly the same would only add to confusion. If I'm new to DM and see both approaches in different codebases I will be rather confused and expect different behaviours.
ReplyDelete