In Progress
Unit 1, Lesson 21
In Progress

OpenStruct

Video transcript & code

In Episode 20 we took an in-depth look at the Struct class. Today I thought we'd explore the related OpenStruct library.

Since I'm making this episode as the outskirts of hurricane Sandy are beginning to hit our area, it seems appropriate to use a weather example. Supposing we want to write a little script to output the current weather conditions at the command line. We decide to use the Weather Underground API, since it provides data in convenient JSON format.

Our script has two methods: one, #get_observation, will query the Weather Underground's "current conditions" endpoint and parse the relevant part of the results into a Hash. The other, #print_observation, uses the observation data to output text.

require 'open-uri'
require 'json'

def get_observation
  key  = ENV['WUNDERGROUND_KEY']
  url  = "http://api.wunderground.com/api/#{key}/conditions/q/PA/York.json"
  data = open(url).read
  JSON.parse(data)['current_observation']
end

def print_observation(observation)
  puts "Temperature: #{observation['temp_f']}F"
  pressure_trend = observation['pressure_trend'] == "-" ? "falling" : "rising"
  puts "Pressure: #{observation['pressure_mb']}mb and #{pressure_trend}"
  puts "Winds: #{observation['wind_string']}"
end

print_observation(get_observation)

That #print_observation method is a little smelly. It's tightly coupled to the Weather Underground data format. And I find the hash subscripts inside string interpolation hard to read. It would be nice to use message sends in this method instead of digging into the hash.

def print_observation(observation)
  puts "Temperature: #{observation.temp_f}F"
  pressure_trend = observation.pressure_trend == "-" ? "falling" : "rising"
  puts "Pressure: #{observation.pressure_mb}mb and #{pressure_trend}"
  puts "Winds: #{observation.wind_string}"
end

Now, to make this work we could create a class to represent weather observations. As a shortcut, we could use Struct. But there's an even shorter shortcut: OpenStruct.

Here's how OpenStruct works. We require the 'ostruct' library, then initialize an OpenStruct object, passing in an optional hash of keys and values. What we get in return is an object that has a reader method for each key in the hash.

require 'ostruct'
observation = {
  'temp_f'         => 49.0,
  'pressure_trend' => '-',
  'pressure_mb'    => 981,
  'wind_string'    => "From the North at 3.0 MPH Gusting to 7.0 MPH"
}
os = OpenStruct.new(observation)
os.temp_f                       # => 49.0
os.wind_string                  # => "From the North at 3.0 MPH Gusting to 7.0 MPH"

It's not limited to the keys passed in during initialization, either. New methods can be created on the fly as well.

require 'ostruct'
observation = {
  'temp_f'         => 49.0,
  'pressure_trend' => '-',
  'pressure_mb'    => 981,
  'wind_string'    => "From the North at 3.0 MPH Gusting to 7.0 MPH"
}
os = OpenStruct.new(observation)

os.my_own_forecast = "Cloudy with a chance of Godzilla"
os.my_own_forecast # => "Cloudy with a chance of Godzilla"

Back in our weather script, we require the 'ostruct' library, and change our #get_observation method to construct an OpenStruct using the observation data as an initialization hash.

Not only does this read more cleanly, we are in a better position to swap out the observation object for some other class later on.

OpenStruct is a useful cousin of Struct, but it's a bare-bones cousin. Many of the features of Struct, which we explored in Episode 20, are not present on OpenStruct. For instance, there's no listing or iterating through keys and values, the way you can with Struct.

require 'ostruct'
s  = Struct.new(:foo, :bar).new(42, 32)
os = OpenStruct.new(foo: 42, bar: 23)

s.members                       # => [:foo, :bar]
os.members                      # => nil
s.map {|value| value * 2}       # => [84, 64]
os.map {|value| value * 2}      # => nil

There may also be performance concerns with OpenStruct compared to Struct, since it defines new methods on the fly. As always, profile your program to find out where the bottlenecks are.

I find OpenStruct useful in a few scenarios:

  1. In quick one-off scripts like our weather report example;
  2. For prototyping, when first fleshing out an object model; and
  3. As quick-and-dirty stub objects in tests, when a full mocking framework is unavailable or inconvenient for some reason.

That's it for today. Happy hacking!