DEV Community

Nick Schwaderer
Nick Schwaderer

Posted on

Ruby Science Saturday: use Mixin Modules or Reopen Classes? 👩‍🔬🔬🧬🧪

Ruby legend Xavier Noria recently penned a tweet that got me thinking about how we organize and package up our native Ruby code:

Xavier Noria Tweet

How often do we just dump our code into mix-in modules (that never get reused) when our classes get too large? What if we continued to organize our code into separate files, but reopened classes?

This is something we can debate all day in the comments, but that sparked my Ruby Science question:

What if one of these approaches was faster?

Well, thanks to benchmark-ips, (which I have also bundled into my own performance logging gem) we don't have to wonder.

I fired up three example cases (Reopen, Include mixin module, Extend mixin module) and ran them against each other in a simple test case. Code below:

module MixinDependency
  def open_door
    puts 'Door is open!'
  end
end

class DoorOpenerMixin
  include MixinDependency

  def close_door
    puts 'Door is closed!'
  end
end

class DoorOpenerMixinExtend
  extend MixinDependency
end

class DoorOpenerReopen
  def close_door
    puts 'Door is closed!'
  end
end

class DoorOpenerReopen
  def open_door
    puts 'Door is open!'
  end
end

And we tested it with:

require 'benchmark/ips'

Benchmark.ips do |x|

  x.report('Include') { DoorOpenerMixin.new.open_door }
  x.report('Extend') { DoorOpenerMixinExtend.open_door }
  x.report('Reopen') { DoorOpenerReopen.new.open_door }

  x.compare!
end

Results? Nothing exciting.

Comparison:
              Extend:   214816.5 i/s
              Reopen:   197784.3 i/s - same-ish: difference falls within error
             Include:   182298.2 i/s - same-ish: difference falls within error

Even inconclusive findings are still findings, and I hope you enjoyed seeing how I picked through this hypothesis to give a clear answer. If you're unfamiliar with benchmark-ips, copy this code and try it out yourself! It really is one of my favorite tools in my toolbox.

One small but fun finding, I tried running this on different Ruby versions, you can see a clear speed bump since Ruby 2.1! Here's the results on the old version:

Comparison:
              Extend:   178840.8 i/s
             Include:   167210.7 i/s - same-ish: difference falls within error
              Reopen:   166005.2 i/s - same-ish: difference falls within error

If you have any 'experiments', feel free to share your results and thoughts below! 🥼 🤔

Top comments (2)

Collapse
 
ludamillion profile image
Luke Inglis

One of the big reasons I’ve preferred mixins over reopening classes is that it ‘reads’ a bit better. To me there is less cognitive load to thinking about collections of like behavior separated out into modules which are self-documenting rather than having the same class opened in multiple places which can ‘hide’ behavior.

I usually take the ‘middle way’ when trying to thin out my classes and extract common behavior into service objects.

Collapse
 
schwad profile image
Nick Schwaderer

I think you're right. There's a benefit to using well-trodden patterns to keep code readable for others as well.

Plus in my mind I do like one file being the 'home base' for a class, and then using mixins to group and store methods used by it elsewhere.

The class-reopening-approach definitely is used a lot more in gems though (in my experience!)