I'm wondering what's the canonical way in Ruby to create custom setter and getter methods. Normally, I'd do this via attr_accessor
but I'm in the context of creating a DSL. In a DSL, setters are called like this (using the =
sign will create local variables):
work do
duration 15
priority 5
end
Therefore, they must be implemented like this:
def duration(dur)
@duration = dur
end
However this makes implementing a getter a bit tricky: creating a method with the same name but with no arguments will just overwrite the setter.
So I wrote custom methods that do both the setting and the getting:
def duration(dur=nil)
return @duration = dur if dur
return @duration if @duration
raise AttributeNotDefinedException, "Attribute '#{__method__}' hasn't been set"
end
Is this a good way to go about it? Here's a gist with test cases:
Ruby Custom Getters & Setters
Thanks!
The trickier case is if you want to set duration to nil. I can think of two ways of doing this
def duration(*args)
@duration = args.first unless args.empty?
@duration
end
Allow people to pass any number of args and decide what to do based on the number. You could also raise an exception if more than one argument is passed.
Another way is
def duration(value = (getter=true;nil))
@duration = value unless getter
@duration
end
This exploits default arguments a little: they can be pretty much any expression.
When called with no arguments getter
is set to true, but when an argument is supplied (even if it is nil) the default value is not evaluated. Because of how local variable scope works getter
ends up nil.
Possibly a little too clever, but the method body itself is cleaner.
For something like this, I prefer to separate the underlying class from the DSL. That is, make a Work class that has the usual accessors, duration
and duration=
. And to use that class via a DSL, wrap the work instance with something that can invoke the accessors situationally, like this:
class AccessorMultiplexer
def initialize(target)
@target = target
end
def method_missing(method, *args)
method = "#{method}=" unless args.empty?
@target.send method, *args
end
end
Wherever you want to use your Work class via a DSL, you'd wrap it with AccessorMultiplexer.new(work)
.
If you're opposed to metaprogramming in the wrapper, you could always make a specific WorkDSL wrapper that does the same without using method_missing
. But it would maintain the separation and keep your Work class from being polluted by the quirks of the DSL syntax. You may want to use the Work class somewhere else in your code where the DSL would be in the way. In rake or a script or -- who knows.
(Adapted from my answer on codereview.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With