Reputation: 238667
I have a situation where I would like to create a class which accepts many arguments and has setters and getters in the fewest lines of code possible (for maintainability). I thought that using a Struct would be a good idea for this:
Customer = Struct.new(:id, :username, :first_name, :last_name, :address1, ...etc...)
Customer.new(123, 'joe', 'Joe', ...etc...)
However, I don't like having to know the exact order of the attributes. I prefer Ruby 2's keyword arguments feature:
class Customer
attr_accessor :id, :username, :first_name, ...etc...
def initialize(id:, username:, first_name:, last_name:, address1:, ...etc...)
@id = id
@username = username
@first_name = first_name
...etc...
end
end
Customer.new(id: 123, username: 'joe', first_name: 'Joe', ...etc...)
However, writing this all out requires a lot more code and is more tedious. Is there a way to achieve the same thing in a short-hand like the Struct?
Upvotes: 16
Views: 12857
Reputation: 776
In addition, you can rely on the constructor of Struct
itself.
def initialize(*args)
super(*args)
# put your magic here!
end
This way you avoid the side-affects of named parameters etc.
Upvotes: 3
Reputation: 7301
Variation on the theme, but a bit more refined, works in any ruby
class Struct
module HashConstructable
def from_hash hash
rv = new
hash.each do |k, v|
rv.send("#{k}=", v)
end
rv
end
# alias_method :[], :from_hash
end
end
and then
class MyStruct < Struct.new(:foo, :boo)
extend Struct::HashConstructable
end
and you have best of both worlds this way - no funny name clashes and side-effects and it's clear want you want to do when you do it:
MyStruct.from_hash(foo: 'foo')
does exactly what you think it does. With a bit more possible side-effects but nicer syntax you can add the alias_method :[], :from_hash
part, this allows you:
MyStruct[foo: 'foo']
this is also nice because it reminds (me) of the Hash[]
constructor which creates a hash out of something that isn't hash.
Upvotes: 1
Reputation: 10620
In ruby 2.5 you can do the following:
Customer = Struct.new(
:id,
:username,
:first_name,
keyword_init: true
)
Customer.new(username: "al1ce", first_name: "alice", id: 123)
=> #<struct Customer id=123, username="al1ce", first_name="alice">
references:
Upvotes: 34
Reputation: 91
You can use Struct and reduce the amount of code to a minimum by adding a factory method (called build here) and if necessary a validate method to your struct
Struct.new("Example",:a,:b) do
def build(a:, b:nil)
s = Struct::Example.new(a,b)
s.validate
return s
end
def validate
unless a == 'stuff' || a == 'nonsense'
raise ValidationError, "broken"
end
end
end
m = Struct.Example.build(a: 'stuff')
where validate is intended to so something like check strings have certain values, rather than just relying on the required parameters check.
Now you only have to remember the order once, when you write the build method
Upvotes: 1
Reputation: 4982
If you don't care about performance, you can use an OpenStruct
.
require 'ostruct'
user = OpenStruct.new(id: 1, username: 'joe', first_name: 'Joe', ...)
user.first_name
=> "Joe"
See this for more details.
It's entirely possible to make it a class and define methods on it:
class Customer < Openstruct
def hello
"world"
end
end
joe = Customer.new(id: 1, username: 'joe', first_name: 'Joe', ...)
joe.hello
=> "world"
But again, because OpenStructs are implemented using method_missing
and define_method
, they are pretty slow. I would go with BroiSatse's answer. If you care about required parameters, you should so something along the lines of
def initialize(params = {})
if missing_required_param?(params)
raise ArgumentError.new("Missing required parameter")
end
params.each do |k,v|
send("#{k}=", v)
end
end
def missing_required_params?(params)
...
end
Upvotes: 7
Reputation: 44675
Cant you just do:
def initialize(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
UPDATE:
To specify default values you can do:
def initialize(hash)
default_values = {
first_name: ''
}
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
If you want to specify that given attribute is required, but has no default value you can do:
def initialize(hash)
requried_keys = [:id, :username]
default_values = {
first_name: ''
}
raise 'Required param missing' unless (required_keys - hash.keys).empty?
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
Upvotes: 15
Reputation: 110675
This is one approach you could use.
class A
def initialize(h)
h.each do |k, v|
instance_variable_set("@#{k}", v)
create_method("#{k}=") { |v|instance_variable_set("@#{k}", v ) }
create_method("#{k}") { instance_variable_get("@#{k}") }
end
end
end
def create_method(name, &block)
self.class.send(:define_method, name, &block)
end
a = A.new(apple: 1, orange: 2)
a.apple #=> 1
a.apple = 3
a.apple #=> 3
a.orange #=> 2
create_method
is straight from the documentation for Module#define_method.
Upvotes: 0