Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby get and set in one method

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!

like image 961
fullstackplus Avatar asked Dec 20 '22 19:12

fullstackplus


2 Answers

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.

like image 113
Frederick Cheung Avatar answered Dec 23 '22 10:12

Frederick Cheung


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.)

like image 22
Rob Davis Avatar answered Dec 23 '22 10:12

Rob Davis