简体   繁体   English

如何定义一个将其初始化参数作为哈希接受的Ruby Struct?

[英]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_missingdefine_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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM