In Progress
Unit 1, Lesson 21
In Progress

Struct from Hash

Video transcript & code

In Episode #20, we learned about Struct. And in Episode #25, we learned about OpenStruct. One of the differences you might have noted between Struct and OpenStruct is that Structs are always instantiated with positional arguments, whereas OpenStructs are instantiated with a hash of keys and values. Considering all the extra features Structs have over OpenStructs, it seems almost like an oversight that we can't initialize a Struct from a hash. Let's see if we can fix that in our own Structs.

We'll start with a Struct definition for beer stats. We'll write a new class-level method called .from_hash. This method first creates a new, empty instance of the struct. Then it goes through the keys and values of the given hash, setting each one on the new instance. To do this it takes advantage of the hash-style subscript operator provided by Struct

Now we can instantiate a new Beer instance using a hash of attributes.

Beer = Struct.new(:brewery, :name, :abv, :ibu) do
  def self.from_hash(attributes)
    instance = self.new
    attributes.each do |key, value|
      instance[key] = value
    end
    instance
  end
end

hopback = Beer.from_hash(
  brewery: "Tröegs", 
  name: "Hopback Amber Ale",
  abv: 6.0,
  ibu: 55)
hopback                         
# => #<struct Beer brewery="Tröegs", name="Hopback Amber Ale", abv=6.0, ibu=55>

It seems only logical that if we can get a new Beer instance from a Hash, we ought to be able to convert one back to a hash of attributes as well. So we define an #attributes method as well. Here, the introspection features of Struct come in handy. We can use the #members method to get a list of the names of all the attributes, and set a value in the result hash for each one.

Beer = Struct.new(:brewery, :name, :abv, :ibu) do
  def self.from_hash(attributes)
    instance = self.new
    attributes.each do |key, value|
      instance[key] = value
    end
    instance
  end

  def attributes
    result = {}
    members.each do |name|
      result[name] = self[name]
    end
    result
  end
end

hopback = Beer.from_hash(
  brewery: "Tröegs", 
  name: "Hopback Amber Ale",
  abv: 6.0,
  ibu: 55)
hopback.attributes
# => {:brewery=>"Tröegs", :name=>"Hopback Amber Ale", :abv=>6.0, :ibu=>55}

One of the questions I received in response to the Struct episode was whether making use of the introspection and enumerability features of a Struct would couple code too closely to the implementation of the object, and make it harder to migrate to a non-struct class later on. I think this is an excellent question. As I've shown here, the introspective capabilities of Struct are quite useful internally, as part of the implementation details of methods. However, I would generally refrain from using Struct-specific methods like the #members method from outside of the class. If you only use these facilities internally, it's easy to graduate your structs to an ActiveRecord model or some other kind of non-struct later on.

That's it for today. Happy hacking!

Responses