Article

7 Deadly Sins of Ruby Metaprogramming

April 24, 2015  |  Pluralsight
Learn something new. Take control of your career.
Sign up

As a developer, you spend 90% of your time on code-related activities like reading and maintaining existing code. With such a large chunk of time spent on these tasks, it’s crucial to make sure everything you do (and code) is efficient. While metaprogramming with Ruby can be extremely powerful, using clever metaprogramming that makes it difficult to read or making bad tradeoffs will, ultimately, increase the cost of maintenance in the long term. So today I want to share the mistakes that can keep you from harnessing some of Ruby’s killer features in your projects.

Sin 1: Using method_missing as the very first option

Paolo Perrotta, the author of Metaprogramming Ruby book, says: “The method_missing() is a chainsaw: it’s powerful, but it’s also potentially dangerous.” The best way to use them? Sparingly and short. Let me show you an example using method_missing to implement Null Object pattern for an order, whose class will have a method to handle different currencies.

Now, in order to avoid writing more methods, we have two options — we can either use define_method or method_missing. Let’s now benchmark this using define_methodand method_missing.

This report shows that define_method is 10 times faster than using method_missing. We also don’t need that much flexibility to handle any currency. As long as we can list the currency that will be handled in our code, we can use define_method to reduce the method duplication and achieve better performing code.

Sin 2: Not overriding respond_to_missing?

You must override the respond_to_missing? method every time you override method_missing. If you’ve worked in a Rails project, you’ll be familiar with checking the current environment by using:

Rails.env.production? instead of doing Rails.env == ‘production’

So, let’s take a look at how this is implemented in ActiveSupport string_inquirer.rb in Rails 4.2:

The method_missing implementation checks to make sure the method ends with a question mark. If it does, it chops that question mark off from the method name and compares it to the current object (the value of self). And if they’re the same, it returns true otherwise false.

You can see the conditional used to trap certain calls is the same as in respond_to_missing? implementation, and that’s exactly how we want it. If you don’t override respond_to_missing?, the object will not respond to any of the dynamically generated methods. This will be a surprise when developers experiment with your library in the irb console, and a good library works as expected with very few surprises, if any.

Sin 3: Forgetting to handle unknown cases

In the previous example, you can see how Rails uses super to propagate a call that the current method does not know how to handle. In the StringInquirer class above, if the method does not end with a question mark, then it allows the call to propagate further up by calling super.

If you don’t fallback to super, then it might lead you to bugs that are really hard to track. Remember, method_missing is where the bugs go to hide. So don’t forget to fallback on BasicObject#method_missing when you don’t know how to handle a call.

Sin 4: Using define_method when it’s not needed

Here is an example from Restclient gem (version 2.0.0.alpha). In the bin/restclient, you will find:

The method_missing implementation checks to make sure the method ends with a question mark. If it does, it chops that question mark off from the method name and compares it to the current object (the value of self). And if they’re the same, it returns true otherwise false.

You can see the conditional used to trap certain calls is the same as in respond_to_missing? implementation, and that’s exactly how we want it. If you don’t override respond_to_missing?, the object will not respond to any of the dynamically generated methods. This will be a surprise when developers experiment with your library in the irb console, and a good library works as expected with very few surprises, if any.

Sin 3: Forgetting to handle unknown cases

In the previous example, you can see how Rails uses super to propagate a call that the current method does not know how to handle. In the StringInquirer class above, if the method does not end with a question mark, then it allows the call to propagate further up by calling super.

If you don’t fallback to super, then it might lead you to bugs that are really hard to track. Remember, method_missing is where the bugs go to hide. So don’t forget to fallback on BasicObject#method_missing when you don’t know how to handle a call.

Sin 4: Using define_method when it’s not needed

Here is an example from Restclient gem (version 2.0.0.alpha). In the bin/restclient, you will find:

Why is this a sin? Because you’re sacrificing readability and comprehension of code without gaining anything in return. The list of HTTP verbs is stable — they almost never change. But by using metaprogramming, you have increased the complexity by dynamically defining methods for the HTTP verbs. We don’t have an explosion of methods problem here, so there is no need for any metaprogramming.

Sin 5: Changing the semantics when opening classes

You should check to see if the method already exists before you open an existing class and add a method. If you don’t, you’ll change the semantics of an existing method by mistake. This will be a nasty surprise to the users of your library. So, prefer refinements over opening classes globally to reduce polluting the global namespace. A good example for this is the JSON gem. It opens the Ruby built-in classes like Range, Rational, Symbol, and so on to define the to_json method.

Sin 6:  Wrong dependency direction

In a layered architecture, the bottom-most layer could be depended upon by many other libraries, which sit on top it. So, it must be agnostic to any of the layers above to be reusable. Having the dependency direction pointing upward is wrong, and one of the horrible sins a programmer can commit. Even though it’s related to the introspective aspect of Ruby more than metaprogramming, I think the impact is huge and worth mentioning.

I’ve seen this mistake made on projects I’ve worked on with clients — libraries at the lowest layer should not use defined? some_constant to see the execution context in which it’s running to change behavior. Libraries at the lowest layer must be independent of their execution context. However, the library can provide API for customized use in a particular context. Another option is using configuration files in order to customize the behavior. The dependency should be in one direction, and should always point toward the stable abstractions.

Sin 7: Too many levels of nesting

Using metaprogramming in your code forces the client to use too many nested blocks, and unfortunately, you can see many open-source projects that use RSpec commit this sin. Here is an example from Spree gem that makes it difficult to understand the code. The following code is part of the backend/spec/controllers/spree/admin/payments_controller_spec.rb.

Why is this a sin? Because you’re sacrificing readability and comprehension of code without gaining anything in return. The list of HTTP verbs is stable — they almost never change. But by using metaprogramming, you have increased the complexity by dynamically defining methods for the HTTP verbs. We don’t have an explosion of methods problem here, so there is no need for any metaprogramming.

Sin 5: Changing the semantics when opening classes

You should check to see if the method already exists before you open an existing class and add a method. If you don’t, you’ll change the semantics of an existing method by mistake. This will be a nasty surprise to the users of your library. So, prefer refinements over opening classes globally to reduce polluting the global namespace. A good example for this is the JSON gem. It opens the Ruby built-in classes like Range, Rational, Symbol, and so on to define the to_json method.

Sin 6:  Wrong dependency direction

In a layered architecture, the bottom-most layer could be depended upon by many other libraries, which sit on top it. So, it must be agnostic to any of the layers above to be reusable. Having the dependency direction pointing upward is wrong, and one of the horrible sins a programmer can commit. Even though it’s related to the introspective aspect of Ruby more than metaprogramming, I think the impact is huge and worth mentioning.

I’ve seen this mistake made on projects I’ve worked on with clients — libraries at the lowest layer should not use defined? some_constant to see the execution context in which it’s running to change behavior. Libraries at the lowest layer must be independent of their execution context. However, the library can provide API for customized use in a particular context. Another option is using configuration files in order to customize the behavior. The dependency should be in one direction, and should always point toward the stable abstractions.

Sin 7: Too many levels of nesting

Using metaprogramming in your code forces the client to use too many nested blocks, and unfortunately, you can see many open-source projects that use RSpec commit this sin. Here is an example from Spree gem that makes it difficult to understand the code. The following code is part of the backend/spec/controllers/spree/admin/payments_controller_spec.rb.

This is a sin because it increases the context you need to reason about a particular piece of code. The simpler the API, the more elegant and easier it is to use. Good examples are validation methods of ActiveModel — here’s one from the Rails documentation for a person class:

And here’s another example — one-level nesting in a routes.rb file in Rails.

Metaprogramming is extremely valuable, and can make solving complex problems easier. But remember, it’s only worth using when there’s a tradeoff — like readability and comprehension in exchange for solving complex problems with less code. As long as you keep these tips in mind, you’ll find yourself becoming a better and more efficient developer in no time. Let me know your thoughts on metaprogramming, along with any advice you have, in the comments section below!

Learn something new. Take control of your career.
Sign up

Pluralsight

Pluralsight is the technology learning platform. We enable individuals and teams to grow their skills, accelerate their careers and... See more