amcaplan/dynamic_class

View on GitHub
README.md

Summary

Maintainability
Test Coverage
[![Gem Version](https://badge.fury.io/rb/dynamic_class.svg)](https://badge.fury.io/rb/dynamic_class)
[![Build Status](https://travis-ci.org/amcaplan/dynamic_class.svg?branch=master)](https://travis-ci.org/amcaplan/dynamic_class)
[![Code Climate](https://codeclimate.com/github/amcaplan/dynamic_class.png)](https://codeclimate.com/github/amcaplan/dynamic_class)

# 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:

``` ruby
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:

```ruby
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](http://contributor-covenant.org) 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](http://opensource.org/licenses/MIT).