Monday, September 08, 2008
Domain Specific Languages don't follow the Principle of Least Surprise
Ola Bini gets it right, as usual, in Evil Hook Methods?, but I think you can actually take the idea a bit further.
DataMapper allows you to gain it's methods simply by an include statement.
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.
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.
Unimpressed by lines 8 and 10, I changed the code to look like the following snippet.
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.
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.
DataMapper allows you to gain it's methods simply by an include statement.
include DataMapper::Resource
# ...
endOla 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
# ...
endHowever, 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"
Labels: DSL, principle of least surprise


