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.)
That seems fine to me, although it seems strange that you raise an error if the value hasn't been set. This is what I would normally do:
def duration(dur=nil)
#duration = dur if dur
#duration
end
However this is a simplistic approach because it means you can't set #duration back to nil using just this method
I usually do this
def duration dur='UNDEFINED'
#duration = dur if dur != 'UNDEFINED'
#duration
end
You can replace UNDEFINED with your favorite thing
Related
In POODR 2nd edition, page 125, Sandy Metz writes
There are two new messages, default_chain and default_tire_size, sent
on lines 6 and 7 below. ... Wrapping the defaults in methods is good
practice in general
The code she's referring to is below. Note how the default value for chain and tire_size is set.
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(**opts)
#size = opts[:size]
#chain = opts[:chain] || default_chain
#tire_size = opts[:tire_size] || default_tire_size
end
def default_chain # <- common default
"11-speed"
end
def default_tire_size # <- common default
"2.1"
end
end
Why is this approach better than the familiar method of simply setting the default value inside initialize(), i.e.
def initialize(chain: "11-speed", tire_size:"2.1", **opts):
This book is about OOP design, so I'm guessing the answer has something to do with good OOP practice but I'm not sure what.
Methods are easier to work with. You can stub them in tests or change the implementation (load values from a config file or something like that), all without touching code that uses them.
But the bigger difference between your code and what Sandi suggests is the handling of falsy values.
In your code, it's possible to pass nils explicitly instead of the default value.
Bicycle.new(chain: nil, tire_size: "2.1", ...)
It might not be a hardcoded nil in your code, but come from somewhere else. Regardless, Bicycle will accept it and then maybe crash at runtime when you try to use the value.
Whereas code from the book does not accept nil values, no matter if it's a default nil or explicitly sent. If chain is falsy, "11-speed" will be used.
Using a method modularizes the code so that subclasses can override the behavior without overriding the initializer:
class MountainBike < Bicycle
def default_tire_size
'18'
end
end
If you used positional arguments initialize(chain="11-speed", tire_size="2.1", **opts) this actually is a major downgrade as you now have to remember the order of the arguments. Positional arguments should only be used when there is an obvious order to the arguments.
You can actually set the defaults for both positional and keyword arguments in the arguments list from a method like this:
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(tire_size: default_tire_size, chain: default_tire_size, **opts)
end
# ...
end
However its not done very often since it tends to make the method signature very cluttered.
I am trying to write this inside my class:
class << self
def steps
#steps.call
end
def transitions
#transitions.call
end
def steps(&steps)
#steps = steps
end
def transitions(&transitions)
#transitions = transitions
end
end
That won't work since in Ruby, I can't do this kind of method overloading. Is there a way around this?
You can kind of do this with method aliasing and mixins, but the way you handle methods with different signatures in Ruby is with optional arguments:
def steps(&block)
block.present? ? #steps = block : #steps.call
end
This sort of delegation is a code smell, though. It usually means there's something awkward about the interface you've designed. In this case, something like this is probably better:
def steps
#steps.call
end
def steps=(&block)
#steps = block
end
This makes it clear to other objects in the system how to use this interface since it follows convention. It also allows for other cases, like passing a block into the steps method for some other use:
def steps(&block)
#steps.call(&block)
end
Ruby does not support method overloading (see "Why doesn't ruby support method overloading?" for the reason). You can, however, do something like:
def run(args*)
puts args
end
args will then be an array of the arguments passed in.
You can also pass in a hash of options to handle arguments, or you can pass in nil when you don't want to supply arguments and handle nil in your method body.
I am working on a project that requires very specific methods to be called on an ActiveRecord::Relation object. These methods cannot extend ActiveRecord::Relation because the Class has it's own initialize method to determine if the object should be collected. I have tried a dozen ways to handle this but because of method chaining in AR I have been unable to accomplish this. Currently I have monkey patched ActiveRecord::Relation with a method that converts it like so:
module ActiveRecord
class Relation
def to_claim_set
exec_queries unless loaded?
ClaimSet.new(#records)
end
end
end
Firstly I am sure this is an improper way to handle it. Secondly this causes me to have to call #to_claim_set constantly throughout the application.
I am hoping someone can assist on making this the default return after all method chaining is complete.
What I am hoping for is something like
Claim.policy_number('913006')
#=> ClaimSetObjectHere
But I need it to support chaining like AR does so that things like
Claim.policy_number('913006').by_program('Base')
#=> ClaimSetObjectHere
I also tried to patch the #where method inside Claim which works great unless I use a scope or I chain methods in which case it complains that ClaimSet does not define default_scoped?.
Any insight would be greatly appreciated. As for "Why would you want to do this" like I said I am constantly calling this method throughout the application and I need the methods defined in ClaimSet for this to function properly.
Note: This is being used outside of rails
Okay so what I ended up doing was imposing a wrapper for ActiveRecord::Relation like so:(removed specific business logic for brevity)
class ClaimSet
def initialize(object)
process_target(object)
# ...
end
# ...
def respond_to_missing?(method_name,include_private=false)
#target.respond_to?(method_name)
end
def method_missing(method_name, *args, &block)
if #target.respond_to?(method_name)
ClaimSet.new(#target.send(method_name,*args,&block))
else
super
end
end
private
def process_target(object)
#target = object if object.is_a?(ActiveRecord::Relation)
#target = object.target if object.is_a?(ClaimSet)
end
end
Then in the Claim class.
class Claim < ActiveRecord::Base
class << self
def where(*args)
ClaimSet.new(super(*args))
end
def localized_scope(name,proc)
scope_proc = lambda do |*args|
ClaimSet.new(proc.call(*args))
end
singleton_class.send(:define_method,name,scope_proc)
end
end
end
Then I define all my scopes as localized_scope e.g.
localized_scope :policy_number, ->(policy_number){where(policy_number: policy_number)}
Now it always returns a ClaimSet in place of an ActiveRecord::Relation for #where and #localized_scope and supports method chaining through #method_missing. It also removes the monkey patch on ActiveRecord::Relation.
If you have any other suggestions please let me know as I would be glad to entertain other ideas but this works for the time being.
Can someone explain the difference between initializing "self" and having #variables when defining classes?
Here's an example
class Child < Parent
def initialize(self, stuff):
self.stuff = stuff
super()
end
end
So in this case, wouldn't I be able to replace self.stuff with #stuff? What's the difference? Also, the super() just means whatever is in the Parent initialize method the Child should just inherit it right?
In general, no, self.stuff = stuff and #stuff = stuff are different. The former makes a method call to stuff= on the object, whereas the latter directly sets an instance variable. The former invokes a method which may be public (unless specifically declared private in the class), whereas the latter is always setting a private instance variable.
Usually, they look the same because it is common to define attr_accessor :stuff on classes. attr_accessor is roughly equivalent to the following:
def stuff
#stuff
end
def stuff=(s)
#stuff = s
end
So in that case, they are functionally identical. However, it is possible to define the public interface to allow for different results and side-effects, which would make those two "assignments" clearly different:
def stuff
#stuff_called += 1 # Keeps track of how often this is called, a side effect
return #stuff
end
def stuff=(s)
if s.nil? # Validation, or other side effect. This is not triggered when setting the instance variable directly
raise "Argument should not be nil"
end
#stuff = s
end
You actually can't use self.stuff= unless you specifically create an attr_writer for modifying that value.
In fact, these are equivalent:
class Child
attr_writer :stuff
end
class Child
def stuff=(val)
#stuff = val
end
end
It is more common to use an attr_writer if that is the functionality you want, rather than the explicit method. But you will often use an explicit method if you want to perform extra error checking or change the way the assignment works.
To your question of when to use #stuff = and when to use self.stuff =, I would use the former if you only have simple assignments and if your class is simple, and would move towards the latter if your requirements might become more complicated. There are many other reasons too, but it's more a matter of style than anything else.
Are there any plans to implement ruby behavior similar to the CoffeeScript feature of specifying an instance variable name in a method argument list?
Like
class User
def initialize(#name, age)
# #name is set implicitly, but #age isn't.
# the local variable "age" will be set, just like it currently works.
end
end
I'm aware of this question: in Ruby can I automatically populate instance variables somehow in the initialize method? , but all the solutions (including my own) don't seem to fit the ruby simplicity philosophy.
And, would there be any downsides for having this behavior?
UPDATE
One of the reasons for this is the DRY (don't repeat yourself) philosophy of the ruby community. I often find myself needing to repeat the name of an argument variable because I want it to be assigned to the instance variable of the same name.
def initialize(name)
# not DRY
#name = name
end
One downside I can think of is that it may look as though a method is doing nothing if it has no body. If you're scanning quickly, this may look like a no-op. But I think given time, we can adapt.
Another downside: if you're setting other instance variables in the body, and you try to be readable by putting all the assignments at the beginning, it can take more cognitive "power" to see that there assignments also happening in the argument list. But I don't think this is any harder than, say, seeing a constant or method call and having to jump to its definition.
# notice: instance var assignments are happening in 2 places!
def initialize(#name)
#errors = []
end
After some pondering, I wondered if it's possible to actually get the argument names from a ruby method. If so, I could use a special argument prefix like "iv_" to indicate which args should be set as instance variables.
And it is possible: How to get argument names using reflection.
Yes! So I can maybe write a module to handle this for me. Then I got stuck because if I call the module's helper method, it doesn't know the values of the arguments because they're local to the caller. Ah, but ruby has Binding objects.
Here's the module (ruby 1.9 only):
module InstanceVarsFromArgsSlurper
# arg_prefix must be a valid local variable name, and I strongly suggest
# ending it with an underscore for readability of the slurped args.
def self.enable_for(mod, arg_prefix)
raise ArgumentError, "invalid prefix name" if arg_prefix =~ /[^a-z0-9_]/i
mod.send(:include, self)
mod.instance_variable_set(:#instance_vars_from_args_slurper_prefix, arg_prefix.to_s)
end
def slurp_args(binding)
defined_prefix = self.class.instance_variable_get(:#instance_vars_from_args_slurper_prefix)
method_name = caller[0][/`.*?'/][1..-2]
param_names = method(method_name).parameters.map{|p| p.last.to_s }
param_names.each do |pname|
# starts with and longer than prefix
if pname.start_with?(defined_prefix) and (pname <=> defined_prefix) == 1
ivar_name = pname[defined_prefix.size .. -1]
eval "##{ivar_name} = #{pname}", binding
end
end
nil
end
end
And here's the usage:
class User
InstanceVarsFromArgsSlurper.enable_for(self, 'iv_')
def initialize(iv_name, age)
slurp_args(binding) # this line does all the heavy lifting
p [:iv_name, iv_name]
p [:age, age]
p [:#name, #name]
p [:#age, #age]
end
end
user = User.new("Methuselah", 969)
p user
Output:
[:iv_name, "Methuselah"]
[:age, 969]
[:#name, "Methuselah"]
[:#age, nil]
#<User:0x00000101089448 #name="Methuselah">
It doesn't let you have an empty method body, but it is DRY. I'm sure it can be enhanced further by merely specifying which methods should have this behavior (implemented via alias_method), rather than calling slurp_args in each method - the specification would have to be after all the methods are defined though.
Note that the module and helper method name could probably be improved. I just used the first thing that came to mind.
Well, actually...
class User
define_method(:initialize) { |#name| }
end
User.new(:name).instance_variable_get :#name
# => :name
Works in 1.8.7, but not in 1.9.3. Now, just where did I learn about this...
I think you answered your own question, it does not fit the ruby simplicity philosophy. It would add additional complexity for how parameters are handled in methods and moves the logic for managing variables up into the method parameters. I can see the argument that it makes the code less readable a toss up, but it does strike me as not very verbose.
Some scenarios the # param would have to contend with:
def initialize( first, last, #scope, #opts = {} )
def search( #query, condition )
def ratchet( #*arg )
Should all of these scenarios be valid? Just the initialize? The #*arg seems particularly dicey in my mind. All these rules and exclusions make the Ruby language more complicated. For the benefit of auto instance variables, I do not think it would be worth it.