[英]How to define a Ruby Struct which accepts its initialization arguments as a hash?
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). 我有一种情况,我想创建一个接受许多参数的类,并且在尽可能少的代码行中具有setter和getter(为了可维护性)。 I thought that using a Struct would be a good idea for this:
我认为使用Struct会是一个好主意:
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:
我更喜欢Ruby 2的关键字参数功能:
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?
有没有办法在像Struct这样的简短手中实现同样的目标?
In ruby 2.5 you can do the following: 在ruby 2.5中,您可以执行以下操作:
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: 引用:
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
If you don't care about performance, you can use an OpenStruct
. 如果您不关心性能,可以使用
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. 但同样,因为OpenStructs是使用
method_missing
和define_method
,所以它们非常慢。 I would go with BroiSatse's answer. 我会选择BroiSatse的答案。 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
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 . create_method
直接来自Module#define_method的文档。
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并通过添加工厂方法(此处称为build)以及必要时对struct的validate方法将代码量减少到最少
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. 其中validate的目的是使得检查字符串具有某些值,而不是仅仅依赖于所需的参数检查。
Now you only have to remember the order once, when you write the build method 现在,您只需要记住一次编写构建方法时的顺序
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: 有了更多可能的副作用但更好的语法你可以添加
alias_method :[], :from_hash
部分,这允许你:
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. 这也很好,因为它提醒(我)
Hash[]
构造函数,它从非哈希的东西中创建一个哈希。
In addition, you can rely on the constructor of Struct
itself. 此外,您可以依赖
Struct
本身的构造函数。
def initialize(*args)
super(*args)
# put your magic here!
end
This way you avoid the side-affects of named parameters etc. 这样可以避免命名参数等的副作用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.