# Passing many params
I recently came across some old code that I had written that honestly made me shudder.
This is the actual code down to the formatting with only the variable names changed.
module TopModule
class NestedClass
attr_reader :param_a, :param_b, :param_c, :param_d, \
:param_e, :param_f, :param_g, :param_h, :param_i
def initialize(param_a, param_b, param_c,
param_d, param_e, param_f,
param_g, param_h, param_i)
@param_a = param_a
@param_b = param_b
@param_c = param_c
@param_d = param_d
@param_e = param_e
@param_f = param_f
@param_g = param_g
@param_h = param_h
@param_i = param_i
end
end
end
Here is how I would write this code now:
module TopModule
class NestedClass
ALLOWED_KEYS = %i[
param_a
param_b
param_c
param_d
param_f
param_g
param_h
param_i
]
attr_reader(*ALLOWED_KEYS)
def initialize(**params)
params.slice(*ALLOWED_KEYS).each do |k, v|
instance_variable_set("@#{k}", v)
end
end
end
end
With just a little bit of metaprogramming the code is clearer with significanlty less duplication. I agree that Ruby metaprogramming can be a double edged sword but in this I believe it is justifed.
# attr_reader
attr_reader
can take mulitiple arugments. Using the splat_operator *
I easily deconstructed the ALLOWED_KEYS
array and passed each param as a separate arugment to the attr_reader function.
# intialize
This initalize is slightly different then the original. The original expected seperated arugments:
TopModule::NestedClass.new(value_1, value_2 ...)
The main disadvantage of this style is that order matters. When the arguments are few this is not a problem. However, when there is a greater number of arugments it is very easy to misorder the values.
In the newer version the values can be passed as a hash or via keyword arguments:
TopModule::NestedClass.new(param_a: value_1, param_b: value_2 ...)
.
The double splat **
operator is used to destructure a hash. Using Hash#slice (opens new window) method I ensure that only the correct keys are used.
# Metaprogramming
The last bit is to actually set the passed in values as instance variables. This is achieved using instance_variable_set (opens new window) method. This allows us to use variable as the instance varaible name. For example if I want to set @param_a
I could:
@param_a = value_1
or I can:
instance_variable_set(@:param_a, value_1)
The benefit of the later is that a variable can be used for example:
instance_variable_name = 'testing'
instance_variable_set("@#{instance_variable_name.to_sym}", value_1)
Hope you found this useful. Thanks.