Avoid repeating when defining a class in Ruby?
-
14-12-2019 - |
Question
I find my self repeating things a lot when I create classes in ruby, often I will end up with something similar to the following:
class Foo
attr_reader :bar_0,
:bar_1,
.
.
.
:bar_n
def initialize( bar_0 = something,
bar_1 = something,
.
.
.
bar_n = something)
@bar_0 = bar_0
@bar_1 = bar_1
.
.
.
@bar_n = bar_n
end
end
Does ruby employ a shortcut for more efficiently implementing something like this?
Solution
Judging from the way the question is phrased, you should probably rethink the design of your classes. However, Ruby provides an interesting way to quickly create classes with attr_accessor
s (not readers). Here's a simple example:
>> class Person < Struct.new(:name, :age) ; end
=> nil
>> p = Person.new
=> #<struct Person name=nil, age=nil>
>> p.age = 23
=> 23
>> p.class
=> Person
>> p.methods.grep(/age/)
=> [:age, :age=]
Of course this is a normal class and you can add all the methods you want (and use getters and setters instead of instance variables, e.g. var
for the getter and self.var = foo
for the setter).
If you really don't want the writers, make them private or undef
them.
>> attrs = [:name, :age]
=> [:name, :age]
>> class Person < Struct.new *attrs ; end
=> nil
>> Person.instance_eval { private *attrs.map{|attr| "#{attr}=" }}
=> Person
>> p = Person.new
=> #<struct Person name=nil, age=nil>
>> p.methods.grep(/age/)
=> [:age]
All of the above doesn't help with the tons of assignments in initialize
of course, but then one wonders if you really want to many constructor arguments or if maybe you just have one hash argument and merge that into a default hash.
OTHER TIPS
Ruby is dynamic and offers a lot in terms of introspection, so you can make use of metaprogramming (or writing code that essentially writes code). In your example there are several things you can do to clean up the verbosity:
class Foo
# Rather than writing bar_1, bar_2, bar_3, ...
attr_accessor ((0..9).to_a + ('a'..'n').to_a).map { |x| :"foo_#{x}" }
# Using mass assignment...
def initialize(attributes = {})
attributes.each do |attribute, value|
respond_to?(:"#{attribute}=") && send(:"#{attribute}=", value)
end
end
end
Since mass assignment is a popular and reusable behavior, it makes sense to extract it into a separate module and make it a mixin:
module MassAssignment
def initialize(attributes = {})
mass_assign(attributes)
end
def mass_assign(attributes)
attributes.each do |attribute, value|
respond_to?(:"#{attribute}=") && send(:"#{attribute}=", value)
end
end
end
class Foo
include MassAssignment
end
You can use introduce parameter object refactoring method to simplify constructor call.