Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a Better OpenStruct (RubyConf 2016)

Ariel Caplan
November 10, 2016

Building a Better OpenStruct (RubyConf 2016)

OpenStruct, part of Ruby's standard library, is prized for its beautiful API. It provides dynamic data objects with automatically generated getters and setters. Unfortunately, OpenStruct also carries a hefty performance penalty.

Luckily, Rubyists have recently improved OpenStruct performance and provided some alternatives. We'll study their approaches, learning to take advantage of the tools in our ecosystem while advancing the state our community.

Sometimes, we can have our cake and eat it too. But it takes creativity, hard work, and willingness to question why things are the way they are.

Ariel Caplan

November 10, 2016
Tweet

More Decks by Ariel Caplan

Other Decks in Technology

Transcript

  1. OPENSTRUCT IS RUBY’S JAVASCRIPT OBJECT require 'ostruct' open_struct = OpenStruct.new(foo:

    :bar) open_struct.baz = 4 open_struct[:something] = 'whatever'
  2. OPENSTRUCT IS RUBY’S JAVASCRIPT OBJECT require 'ostruct' open_struct = OpenStruct.new(foo:

    :bar) open_struct.baz = 4 open_struct[:something] = 'whatever' open_struct[:foo] => :bar open_struct["baz"] => 4 open_struct[:quux] => nil open_struct.foo => :bar open_struct.baz => 4 open_struct.quux => nil
  3. WHY USE OPENSTRUCT? 3 common use cases for OpenStruct (Erik

    Michaels-Ober's list): • Consume an API • Configuration object • Simple test double
  4. CONSUME AN API hash = JSON.parse(APICall.execute) results = OpenStruct.new(hash) class

    APICall < OpenStruct def self.execute response = Net::HTTP.get(URI('domain.com/endpoint')) new(JSON.parse(response)) end end results = APICall.execute SIMPLE STRATEGY: PASS RESPONSE INTO AN OPENSTRUCT ADVANCED STRATEGY: SUBCLASS OPENSTRUCT
  5. CONSUME AN API { "thing": [1, 2, 3], "other_thing": "hello"

    } If the API response is… results.thing => [1, 2, 3] You can write this instead! Instead of… results['thing'] => [1, 2, 3]
  6. CONSUME AN API You can also define your own methods!

    class User < OpenStruct def name "#{first_name} #{last_name}" end end api_call = JSON.parse(API.get('/users/1')) # => {"first_name"=>"Ariel","last_name"=>"Caplan"} User.new(api_call).name # => "Ariel Caplan"
  7. CONFIGURATION OBJECT class MyGem def self.configure yield configuration end def

    self.configuration @configuration ||= OpenStruct.new end end MyGem.configure do |configuration| configuration.setting = true end
  8. TEST DOUBLE class Order def initialize(payment_gateway, products) @payment_gateway = payment_gateway

    @products = products end def pay payment_gateway.total = total_cost payment_gateway.charge end def total_cost # sum up the products end end
  9. TEST DOUBLE class Order def initialize(payment_gateway, products) @payment_gateway = payment_gateway

    @products = products end def pay payment_gateway.total = total_cost payment_gateway.charge end def total_cost # sum up the products end end Expensive to test!
  10. TEST DOUBLE # Stub out methods payment_gateway = OpenStruct.new(charge: :PAID)

    # Run the test payment_result = Order.new(payment_gateway, products).pay # Assert a value is returned assert_equal payment_result, :PAID # Assert a value is set on the OpenStruct assert_equal payment_gateway.total, total_cost(products)
  11. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class.
  12. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. foo = Object.new def foo.bar "hello from bar" end foo.bar => "hello from bar" foo.singleton_methods => [:bar]
  13. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. OpenStruct.new(foo: :bar)
  14. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar)
  15. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar) open_struct.baz = 4
  16. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  17. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  18. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  19. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.baz open_struct.send(:baz=, 4)
  20. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end
  21. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end This is the singleton class version of: attr_reader :foo attr_writer :foo but it uses the internal @table, not instance variables!
  22. • No 2 OpenStructs share a set of methods •

    The methods must be defined each time! • Under the hood, OpenStruct defines attribute setter/getter methods on the object’s singleton class. HOW DOES IT WORK?
  23. • Q: How slow is OpenStruct? • A: OPENSTRUCT IS

    SLOW !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! • Translation: 10-40x slower than an explicitly defined class • Q: Why is OpenStruct slow? • Defining methods is slow • method_missing is slow • In < 2.1, reset the global method cache, no longer a problem
  24. METHOD LOOKUP IN RUBY cat = Cat.new cat.to_s DOG OBJECT

    ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL
  25. METHOD LOOKUP IN RUBY cat = Cat.new cat.to_s DOG OBJECT

    ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL #TO_S Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  26. class Cat def to_s "I'm a cat!" end end METHOD

    LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  27. class Cat def to_s "I'm a cat!" end end METHOD

    LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  28. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  29. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  30. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  31. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE Looking up methods can be really expensive!
  32. METHOD LOOKUP IN RUBY Cat.ancestors => [Cat (call 'Cat.connection' to

    establish a connection), Cat::GeneratedAssociationMethods, #<#<Class:0x007fd72c534de0>: 0x007fd72c534e58>, ApplicationRecord(abstract), ApplicationRecord::GeneratedAssoc
 iationMethods, #<#<Class:0x007fd72c6e5a18>: 0x007fd72c6e5ae0>, ActiveRecord::Base, GlobalID::Identification, ActiveRecord::Suppressor, ActiveRecord::SecureToken, ActiveRecord::Store, ActiveRecord::Serialization, ActiveModel::Serializers::JSON, ActiveModel::Serialization, ActiveRecord::Reflection, ActiveRecord::TouchLater, ActiveRecord::NoTouching, ActiveRecord::Transactions, ActiveRecord::Aggregations, ActiveRecord::NestedAttributes, ActiveRecord::AutosaveAssociation, ActiveModel::SecurePassword, ActiveRecord::Associations, ActiveRecord::Timestamp, ActiveModel::Validations::Callbacks, ActiveRecord::Callbacks, ActiveRecord::AttributeMethods::Serializ ation, ActiveRecord::AttributeMethods::Dirty, ActiveModel::Dirty, ActiveRecord::AttributeMethods::TimeZone Conversion, ActiveRecord::AttributeMethods::Primary
 Key, ActiveRecord::AttributeMethods::Query, ActiveRecord::AttributeMethods::Before
 TypeCast, ActiveRecord::AttributeMethods::Write, ActiveRecord::AttributeMethods::Read, ActiveRecord::Base::GeneratedAssociation Methods, #<#<Class:0x007fd72d296ee8>: 0x007fd72d296f60>, ActiveRecord::AttributeMethods, ActiveModel::AttributeMethods, ActiveRecord::Locking::Pessimistic, ActiveRecord::Locking::Optimistic, ActiveRecord::AttributeDecorators, ActiveRecord::Attributes, ActiveRecord::CounterCache, ActiveRecord::Validations, ActiveModel::Validations::HelperMethods, ActiveSupport::Callbacks, ActiveModel::Validations, ActiveRecord::Integration, ActiveModel::Conversion, ActiveRecord::AttributeAssignment, ActiveModel::AttributeAssignment, ActiveModel::ForbiddenAttributesPro
 tection, ActiveRecord::Sanitization, ActiveRecord::Scoping::Named, ActiveRecord::Scoping::Default, ActiveRecord::Scoping, ActiveRecord::Inheritance, ActiveRecord::ModelSchema, ActiveRecord::ReadonlyAttributes, ActiveRecord::Persistence, ActiveRecord::Core, ActiveSupport::ToJsonWithActiveSup
 portEncoder, Object, ActiveSupport::Dependencies::Load
 able, PP::ObjectMixin, JSON::Ext::Generator::Generator
 Methods::Object, ActiveSupport::Tryable, Kernel, BasicObject]
  33. METHOD LOOKUP IN RUBY • James Golick found that 10%

    of Rails CPU time was rebuilding the method cache… • So he implemented a better solution: A hierarchical method cache! • It was included in Ruby 2.1
  34. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  35. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  36. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  37. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE
  38. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE
  39. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE "
  40. HI EVERYONE! MY NAME IS ARIEL CAPLAN. YOU CAN FIND

    ME ONLINE AT @AMCAPLAN AND AMCAPLAN.NINJA
  41. WE USE A SYSTEM OF MICROSERVICES AND EXTERNAL APIS COMMUNICATING

    VIA JSON PARSED INTO RUBY HASHES FED INTO OPENSTRUCTS
  42. In our app: gem 'stackprof' StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump') do

    # The block we wanted to test end Then, in the terminal: $ stackprof tmp/stackprof-cpu-myapp.dump --text
  43. ================================== Mode: cpu(1000) Samples: 2020 (3.72% miss rate) GC: 413

    (20.45%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 264 (13.1%) 264 (13.1%) OpenStruct#new_ostruct_member 158 (7.8%) 158 (7.8%) Nokogiri::XML::Document#decorate 62 (3.1%) 62 (3.1%) Nokogiri::XML::Node#content= 52 (2.6%) 51 (2.5%) Nokogiri::XML::Node#write_to 48 (2.4%) 48 (2.4%) Nokogiri::XML::Node#[]= 62 (3.1%) 48 (2.4%) RSolr::Xml::Document#add_field 47 (2.3%) 47 (2.3%) Time.zone_offset 45 (2.2%) 45 (2.2%) block in OpenStruct#new_ostruct_member 43 (2.1%) 43 (2.1%) block in Sunspot::Setup#get_inheritable_hash 41 (2.0%) 41 (2.0%) OpenStruct#method_missing
  44. ================================== Mode: cpu(1000) Samples: 2020 (3.72% miss rate) GC: 413

    (20.45%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 264 (13.1%) 264 (13.1%) OpenStruct#new_ostruct_member 158 (7.8%) 158 (7.8%) Nokogiri::XML::Document#decorate 62 (3.1%) 62 (3.1%) Nokogiri::XML::Node#content= 52 (2.6%) 51 (2.5%) Nokogiri::XML::Node#write_to 48 (2.4%) 48 (2.4%) Nokogiri::XML::Node#[]= 62 (3.1%) 48 (2.4%) RSolr::Xml::Document#add_field 47 (2.3%) 47 (2.3%) Time.zone_offset 45 (2.2%) 45 (2.2%) block in OpenStruct#new_ostruct_member 43 (2.1%) 43 (2.1%) block in Sunspot::Setup#get_inheritable_hash 41 (2.0%) 41 (2.0%) OpenStruct#method_missing
  45. def update(args) args.each { |k, v| assign(k, v) } end

    — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end OpenFastStruct.new(baz: 4)
  46. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end OpenFastStruct.new(baz: 4)
  47. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end OpenFastStruct.new(baz: 4)
  48. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  49. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz = 4 open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  50. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz = 4 open_fast_struct.baz # baz does not exist open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  51. SAMPLE USAGE PersistentOpenStruct class Animal < PersistentOpenStruct def speak "The

    #{type} makes a #{sound} sound!" end end dog = Animal.new(type: 'dog', sound: 'woof') # => #<Animal type="dog", sound="woof"> dog.speak # => "The dog makes a woof sound!" dog.ears = 'droopy' dog[:nose] = ['cold', 'wet'] dog['tail'] = 'waggable' dog # => #<Animal type="dog", sound="woof", ears="droopy", nose=["cold", "wet"], tail="waggable"> Animal.instance_methods(false) # => [:speak, :type=, :type, :sound=, :sound, :ears=, :ears,
 :nose=, :nose, :tail=, :tail]
  52. HOW IT WORKS PersistentOpenStruct def new_ostruct_member(name) name = name.to_sym unless

    respond_to?(name) self.class.send(:define_method, name) { @table[name] } self.class.send(:define_method, "#{name}=") { |x| @table[name] = x } end name end def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end OpenStruct PersistentOpenStruct def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) self.class.send(:define_method, name) { @table[name] } self.class.send(:define_method, "#{name}=") { |x| @table[name] = x } end name end
  53. Comparison: RegularClass: 3673779.6 i/s PersistentOpenStruct: 764359.4 i/s - 4.81x slower

    OpenFastStruct: 546808.8 i/s - 6.72x slower OpenStruct: 155130.1 i/s - 23.68x slower
  54. THE INFORMATION YOU CRAVE • Let n = number of

    methods to define • Let o = objects to create • Using PersistentOpenStruct: O(n) • Using OpenStruct: O(no) USING THE NOTATION YOU LOVE!
  55. MATH PROVIDES A USEFUL FRAMEWORK FOR THINKING ABOUT PROBLEMS. AS

    ENGINEERS, WE NEED ANSWERS GROUNDED IN REALITY.
  56. $ gem install benchmark-ips Then, in a Ruby program: require

    ‘benchmark/ips' Benchmark.ips do |x| x.report('old code') do #run old code end x.report('new code') do # run new code end x.compare! end
  57. Warming up -------------------------------------- old code 48.250k i/100ms new code 37.676k

    i/100ms Calculating ------------------------------------- old code 561.458k (± 8.0%) i/s - 2.799M in 5.019311s new code 437.953k (± 4.8%) i/s - 2.185M in 5.001528s Comparison: old code: 561458.3 i/s new code: 437953.4 i/s - 1.28x slower
  58. SAMPLE USAGE DynamicClass Animal = DynamicClass.new do def speak "The

    #{type} makes a #{sound} sound!" end end dog = Animal.new(type: 'dog', sound: 'woof') # => #<Animal:0x007fdb2b818ba8 @type="dog", @sound="woof"> dog.speak # => "The dog makes a woof sound!" dog.ears = 'droopy' dog[:nose] = ['cold', 'wet'] dog['tail'] = 'waggable' dog # => #<Animal:0x007fc26b1841d0 @type="dog", @sound="woof", @ears="droopy", @nose=["cold", "wet"], @tail=“waggable"> Animal.instance_methods(false) # => [:to_h, :speak, :type=, :type, :sound=, :sound, :ears=, :ears,
 :nose=, :nose, :tail=, :tail]
  59. HOW IT WORKS DynamicClass class << self def attributes @attributes

    ||= Set.new end def add_methods!(key) class_exec do attr_writer key unless method_defined?("#{key}=") attr_reader key unless method_defined?("#{key}") attributes << key end end end
  60. HOW IT WORKS DynamicClass def initialize(attributes = {}) attributes.each_pair do

    |key, value| __send__(:"#{key}=", value) end end Animal.new(baz: 4)
  61. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end Animal.new(baz: 4)
  62. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 Animal.new(baz: 4)
  63. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Animal.new(baz: 4)
  64. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def []=(key, value) key = key.to_sym instance_variable_set(:"@#{key}", value) unless self.class.attributes.include?(key) self.class.add_methods!(key) end end def [](key) instance_variable_get(:"@#{key}") end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Animal.new(baz: 4)
  65. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def []=(key, value) key = key.to_sym instance_variable_set(:"@#{key}", value) unless self.class.attributes.include?(key) self.class.add_methods!(key) end end def [](key) instance_variable_get(:"@#{key}") end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Instead of an internal hash @table, we use instance variables just as you would in a standard class! Animal.new(baz: 4)
  66. IS IT FAST? Comparison: RegularClass: 3757567.8 i/s DynamicClass: 1250634.2 i/s

    - 3.00x slower PersistentOpenStruct: 766792.7 i/s - 4.90x slower OpenFastStruct: 525565.1 i/s - 7.15x slower OpenStruct: 147361.4 i/s - 25.50x slower
  67. 1 PERSON CAN INVENT A GREAT TOOL. IT TAKES A

    COMMUNITY TO ADAPT IT FOR THE COMMUNITY.