Create classes that define themselves... eventually.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
lib
spec
.gitignore
.rspec
.travis.yml
CODE_OF_CONDUCT.md
Gemfile
LICENSE.txt
README.md
Rakefile
dynamic_class.gemspec

README.md

Gem Version Build Status Code Climate

DynamicClass

Many developers use OpenStruct as a convenient way of consuming APIs through a nifty data object. But the performance penalty is pretty awful.

DynamicClass offers a better solution, optimizing for the case where you need to create objects with the same set of properties every time, but you can't define the needed keys until runtime. DynamicClass works by defining instance methods on the class every time it encounters a new propery.

Let's see it in action:

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">
dog.type
# => "dog"
dog.tail
# => "waggable"

cat = Animal.new
# => #<Animal:0x007fdb2b83b180>
cat.to_h
# => {:type=>nil, :sound=>nil, :ears=>nil, :nose=>nil, :tail=>nil}
# The class has been changed by the dog!

Because methods are defined on the class (unlike OpenStruct which defines methods on the object's singleton class), there is no need to define a method more than once. This means that, past the first time a property is added, the cost of setting a property drops.

The results are pretty astounding. Here are the results of the benchmark in bin/benchmark.rb (including a few other OpenStruct-like solutions for comparison), run on Ruby 2.3.1; the final benchmark is most representative of the average case:

Initialization benchmark

Warming up --------------------------------------
          OpenStruct    84.801k i/100ms
PersistentOpenStruct    74.901k i/100ms
      OpenFastStruct    81.303k i/100ms
        DynamicClass    97.024k i/100ms
        RegularClass   211.767k i/100ms
Calculating -------------------------------------
          OpenStruct      1.104M (± 5.8%) i/s -      5.512M in   5.011886s
PersistentOpenStruct    941.181k (± 5.2%) i/s -      4.719M in   5.027485s
      OpenFastStruct      1.020M (± 6.0%) i/s -      5.122M in   5.040500s
        DynamicClass      1.309M (± 4.0%) i/s -      6.598M in   5.049905s
        RegularClass      4.170M (± 4.0%) i/s -     20.965M in   5.036315s

Comparison:
        RegularClass:  4169602.6 i/s
        DynamicClass:  1308644.3 i/s - 3.19x slower
          OpenStruct:  1103594.3 i/s - 3.78x slower
      OpenFastStruct:  1019939.5 i/s - 4.09x slower
PersistentOpenStruct:   941180.6 i/s - 4.43x slower



Assignment Benchmark

Warming up --------------------------------------
          OpenStruct   216.147k i/100ms
PersistentOpenStruct   210.657k i/100ms
      OpenFastStruct   101.072k i/100ms
        DynamicClass   311.870k i/100ms
        RegularClass   312.066k i/100ms
Calculating -------------------------------------
          OpenStruct      4.505M (± 5.0%) i/s -     22.479M in   5.003206s
PersistentOpenStruct      4.515M (± 5.0%) i/s -     22.540M in   5.005895s
      OpenFastStruct      1.383M (± 3.5%) i/s -      6.974M in   5.048792s
        DynamicClass     11.138M (± 5.0%) i/s -     55.825M in   5.026293s
        RegularClass     11.069M (± 5.8%) i/s -     55.236M in   5.009156s

Comparison:
        DynamicClass: 11137717.4 i/s
        RegularClass: 11068826.7 i/s - same-ish: difference falls within error
PersistentOpenStruct:  4514966.3 i/s - 2.47x slower
          OpenStruct:  4505071.4 i/s - 2.47x slower
      OpenFastStruct:  1383122.4 i/s - 8.05x slower



Access Benchmark

Warming up --------------------------------------
          OpenStruct   259.543k i/100ms
PersistentOpenStruct   255.894k i/100ms
      OpenFastStruct   225.799k i/100ms
        DynamicClass   313.455k i/100ms
        RegularClass   313.982k i/100ms
Calculating -------------------------------------
          OpenStruct      6.744M (± 5.0%) i/s -     33.741M in   5.016060s
PersistentOpenStruct      6.863M (± 5.2%) i/s -     34.290M in   5.011129s
      OpenFastStruct      4.717M (± 4.5%) i/s -     23.709M in   5.036478s
        DynamicClass     11.467M (± 5.9%) i/s -     57.362M in   5.021761s
        RegularClass     11.395M (± 6.6%) i/s -     56.831M in   5.011823s

Comparison:
        DynamicClass: 11467320.5 i/s
        RegularClass: 11395421.4 i/s - same-ish: difference falls within error
PersistentOpenStruct:  6862609.3 i/s - 1.67x slower
          OpenStruct:  6744325.9 i/s - 1.70x slower
      OpenFastStruct:  4717334.0 i/s - 2.43x slower



All-Together Benchmark

Warming up --------------------------------------
          OpenStruct    13.929k i/100ms
PersistentOpenStruct    64.546k i/100ms
      OpenFastStruct    45.014k i/100ms
        DynamicClass    96.783k i/100ms
        RegularClass   197.149k i/100ms
Calculating -------------------------------------
          OpenStruct    147.361k (± 4.8%) i/s -    738.237k in   5.021813s
PersistentOpenStruct    766.793k (± 5.8%) i/s -      3.873M in   5.069128s
      OpenFastStruct    525.565k (± 4.1%) i/s -      2.656M in   5.062072s
        DynamicClass      1.251M (± 4.0%) i/s -      6.291M in   5.038697s
        RegularClass      3.758M (± 4.1%) i/s -     18.926M in   5.046044s

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

DynamicClass is still behind plain old Ruby classes, but it's the best out of the pack when it comes to OpenStruct and friends.

WARNING!

This class should only be used to consume trusted APIs, or for similar purposes. It should never be used to take in user input. This will open you up to a memory leak DoS attack, since every new key becomes a new method defined on the class, and is never erased.

Installation

Add this line to your application's Gemfile:

gem 'dynamic_class'

And then execute:

$ bundle

Or install it yourself as:

$ gem install dynamic_class

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can run the benchmark using rake benchmark. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Contributing

Bug reports and pull requests are welcome. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

For functionality changes or bug fixes, please include tests. For performance enhancements, please run the benchmarks and include results in your pull request.

License

The gem is available as open source under the terms of the MIT License.