Stop Monkey Patching Everything
Ruby is an awesome, dynamically typed language that has very strong OO features and excellent syntatic sugar. You can write very expressive software in few lines that looks good. It should sound like music to your ears if you’re coming from some kind of ugly templated behemoth. After all, Ruby’s motto is “a programmer’s best friend” – and it is – but only if you don’t suck.
A small case study
Let’s say we have a zoo. You hire a bootcamp graduate and they prepare for you the perfect class hierarchy to manage your steed. It looks something like this.
class Animal def initialize(name, sound) @name, @sound = name, sound end def some_important_hook # things end def do_trick raise NotImplementedError 'haha pls implement' end end
Impressed by your developer’s capacity to innovate, you let them go study the behavior of your animals so that they could write some software. They find out that it’s quite easy to turn both of these animals into glue. During code review, an engineering manager will berate them for producing this disgusting duplicate code.
class Horse < Animal def do_trick "http://elmers.com/" end end class Rabbit < Animal def do_trick "http://elmers.com/" end end
Your developer studies for a fortnight before arriving to this solution. For those not in the loop,
include will decorate your class’ instance methods with those found in the module. All sarcasm aside, this is the correct solution, and we should stop here.
module Gluable def do_trick "http://elmers.com/" end end class Horse < Animal include Gluable end class Rabbit < Animal include Gluable end
…instead, management now wants some synergy every time
#some_important_hook is run. Your developer thinks he is clever and produces this. Including a module into every subclass seems sooooooo 2006. Instead, we can type
acts_as_gluable in our subclass! How clever!
module Gluable def self.included(other) other.extend(ClassMethods) end def do_trick "http://elmers.com/" end def some_important_hook # overly complex and poorly tested business logic super end module ClassMethods def acts_as_gluable include Gluable end end end
How NOT clever
ActiveRecord::Base and prepare for the biggest clusterfuck you’ve ever seen. Everybody thinks it’s fashionable to make crude monkeypatches to the base AR class and ends up bringing in
Railtie dependencies when their library really focuses on adding two numbers or doing some geocoding. Here is an example of a geocoding library doing a lot of not-geocoding. Additionally, to facilitate their mess, they will override core ActiveRecord lifecycle hooks (e.g.
#before/after_*), or append their own methods in
ActiveRecord::FinderMethods that depend on other methods in that external module. Are you crying yet? You should be.
Why is it a problem?
The DSL methods that they include to decorate your model with domain specific functionality now exists in every model your project will ever have. Additionally, the support required to do this patching/integration usually ends up adding either a large part of
rails as a dependency. This is not even the worse part; sometimes these patches will break other libraries or even
It is 2017. Myself and many other developers prefer to work on microservice oriented applications. If sharing a database is something that your architecture requires (not all of us do the most clever, 3489248923423-Kafka-shard with 8394248e+01 ElasticSearch cluster services), then you don’t get to make portable models. Today while attempting to package all of our database models in an external gem, I had to read through the source of at least 5+ libraries to find out how to make their functions available without Rails. It isn’t necissarily difficult to do – but I noticed that most of the code was just to provide this convenient DSL sugar…and for no benefit at all.
I really don’t mean to sound so angry, but not everybody uses Rails for everything anymore. And that’s a good thing. Our ecosystem is maturing, finding alternatives, growing. Ruby’s metaprogramming facilities make it so easy for someone to write all of this seemingly “automagic” software, with include and extend hooks, great reflection support, and the whole 9 yards. It also makes it 100x easier for you to forget that you are committing abuses just becuase committing them is so much fun. If Ruby is to continue maturing, we need to exercise some self control as a community – or Ruby will become a thing of the past. And wouldn’t that be a damn shame.