When Yukihiro “Matz” Matsumoto created Ruby in the mid-1990s he did so with programmer productivity as a priority. The result was a powerful, flexible language with an elegant syntax that reads and writes like plain English. Ruby’s quick learning curve, flexibility, fast prototyping and powerful metaprogramming earned it a devoted following and cemented its status as one of the top programming languages for building the back-end of a modern web application.
But with great power comes great responsibility—Ruby’s deceptively simple exterior hides a lot of complexity under the hood, and if you’re not careful, that complexity can come back to bite you. In this article we’ll take a look at 5 common Ruby programming mistakes, and how you can avoid them.
-
Overusing method_missing
Nobody likes repetitive code, and metaprogramming with method_missing is one of those guilty pleasure catch-all’s we like to throw into our code to avoid errors at runtime and just make it work.
Need to allow one class to quickly use the methods available in another class? Use method_missing.
Got lots of methods but don’t want to name them all explicitly? Use method_missing.
Need to build lots of methods that are virtually identical except for a few slight differences? Use method_missing.
There’s just one problem with method_missing: it’s slow. Every time you call method_missing, the Ruby runtime must climb up your class chain hierarchy to find the method that actually does the heavy lifting that allows your code to work. Most benchmark tests show that method_missing is significantly slower at runtime than if you were to use plain old Ruby methods. A faster metaprogramming tool for the example cases above is define_method, which allows you to dynamically define methods that load when your classes are loaded.
So when should method_missing be used? When you’re dealing with dynamically named methods based on some pattern, and you can’t reasonably expect to be able to predict all the possible method name combinations. Ruby’s own ActiveRecord dynamic finders are probably the best example of this. For example, if you wanted to find a user by email from a large data set of users, and your user has an email attribute, you could use User.find_by_email(‘[email protected]’)
even though you never explicitly defined that method under the User or ActiveRecord::Base classes.
-
Relying too heavily on Gems
Every new Rails developer reaches a certain point in their careers where they fall prey to the siren song of the RubyGems repository. And how can you resist? Ruby’s extensive catalog of gems is supported by an active open-source community. Need help with authentication? Try Devise. What about authorization? 1CanCanCan’s got you covered. If there’s a task that you need help with, chances are high that there’s a gem for that.
Unfortunately, like all things in life it’s possible to have too much of a good thing. As powerful as gems are, there comes a point where you’ll begin to notice a tradeoff between programmer productivity and performance with every new gem. Too many gems can slow down performance, drain resources, and make tests take longer.
A bloated GemFile can become a nesting ground for future application-breaking bugs. Higher quality gems try to minimize dependencies as much as possible, but if you aren’t careful you could find yourself managing hundreds of gems. At that point, whatever productivity you gained from having to write less code is eaten up in managing dependencies, chasing hidden bugs, and keeping your application up to date.
What’s the solution? Moderation. Before you download a gem, ask yourself whether you can justify its use of resources and whether you need all the functionality that it has to offer. Being selective about your gems can help keep your application lean.
-
Application logic seeps into the view
Ruby on Rails follows the MVC (Model View Controller) pattern, where the model is responsible for data, the view is the visual representation of that data, and the controller forms the bridge between the two. The goal of this pattern is to decouple the major roles of an application in order to promote efficient code reuse and allow parallel development. Realizing that goal becomes difficult when you start to deviate from those roles.
Views should only be concerned with visual representation, however it can be tempting to sneak application logic into your ERB templates like so:
<h2> Congratulations <% if winning_player %> <%= winning_player.name %> <% else %> Contestant <% end %> </h2> |
Not only can the inefficiency of placing application logic into individual views lead to repeating code, managing a large jumble of Ruby and HTML code can make maintenance more difficult as your application scales. It is far better to encapsulate that logic into a reusable helper function, presenter, or decorator.
-
Taking “fat model skinny controller” too far
Controllers do a lot of things, from session handling to rendering and redirecting—but it basically boils down to a middleman between the model and view, receiving requests and providing the appropriate output. By default, the controller already has a lot on its plate. To prevent the temptation of stuffing model or view logic in the controller, the “fat model skinny controller” principle was born. Problem solved, right?
Not really. Now the problem of bloat has just been moved to the model. Every model should correspond to a data table in the database. The model’s corresponding data table should be its single responsibility. In a bid to keep views and controllers lean, it can be tempting to toss anything that doesn’t quite fit into the MVC paradigm into the model. It turns out that many of the maintenance issues that plague bloated views and controllers also extend to the model. The solution, is to use POROs (plain old Ruby objects) to encapsulate things that don’t quite fit in any of the MVC categories.
-
Leaving your code vulnerable to SQL injection
It’s one of the oldest tricks in the hacker playbook—SQL injection, which involves taking advantage of user input fields to gain direct access to a SQL database. In Rails, this vulnerability is found where user input is required (think: typing in a search field) to query data from a database. The traditional approach to handling a query for a username would look something like this:
user.find_by(name: params [:name]) |
This approach is vulnerable to SQL injection. Fortunately, in Rails we have ActiveRecord dynamic attribute-based finders, which work as parameterized queries that can properly handle the passed argument, avoiding SQL injection.
user.find_by_name(name) |
Besides using dynamic finders, it helps to only accept and construct values from external inputs like search queries and forms. Never construct SQL commands like INSERT and DELETE, and don’t send untrusted inputs to methods in the ActiveRecord library that accept raw SQL queries, table names, or columns.