The Technology
Just as France’s Train à Grande Vitesse (TGV) (traveling at speeds of up to 320 km/h) dramatically reduces travel time for modern day rail passengers, Ruby on Rails (a.k.a. “Rails”) substantially reduces the time and effort required to build powerful web applications. Tim O’Reilly (founder of O’Reilly Media) refers to Rails as breakthrough technology and Gartner Research noted in a recent study that many high-profile companies are using Rails to build agile, scalable web applications.
The rate at which Rails has gained popularity is noteworthy, with estimates of over 200,000 web sites currently built with the technology. Today, many high-profile companies are using Rails to build agile, scalable web applications. Examples include Twitter, GitHub, Yammer, Scribd, Groupon, Shopify, and Basecamp, to name but a few.
Rails is a framework for web application development, written in Ruby, that also features its own routing system independent of the web server. The goal of Rails is to significantly simplify the development of web applications, requiring less code and time than would otherwise be required to accomplish the same tasks.
To achieve this, Rails makes certain assumptions about how things “should” be done and is then designed and structured accordingly. While imbibing this “Rails view of the world” can sometimes be a bit of a culture shock for developers strongly grounded in other languages and frameworks, over time most come to greatly appreciate the Rails approach and the productivity that it engenders.
The Challenge
From a recruiting standpoint, the explosive growth in Rails popularity is both the good and the bad news. While on the one hand it makes Rails developers easier to locate, it also makes finding the top-notch jewels among them that much more elusive.
Finding true high-quality Rails experts for full-time or part-time work requires a highly-effective recruiting process, as described in our post In Search of the Elite Few – Finding and Hiring the Best Developers in the Industry. Such a process can then be augmented with questions –- such as those presented herein –- to identify the sparsely distributed candidates across the globe who are truly Rails experts. The manifold benefits of finding them will likely be realized in the productivity and results that they will be able to achieve.
Yeah, I know Rails…
The extent to which Rails streamlines and simplifies the development of web applications can mislead neophyte developers into underestimating its capabilities and oversimplifying its conceptual underpinnings. While Rails is relatively easy to use, it is anything but simplistic.
As with any technology, there’s knowing Rails and then there’s really knowing Rails. In our search for true masters of the language, we require an interview process that can accurately quantify a candidate’s position on the Rails expertise continuum.
Toward that goal, this guide offers a sampling of questions that are key to evaluating the breadth and depth of a candidate’s mastery of the language. It is important to bear in mind, though, that these sample questions are intended merely as a guide. Not every “A” candidate worth hiring will be able to properly answer them all, nor does answering them all guarantee an “A” candidate. At the end of the day, hiring remains as much of an art as it does a science.
Frequent Rail Traveler?
It is not uncommon to encounter RoR developers whose grasp of the fundamentals and key paradigms of Rails are either weak or somewhat confused.
Questions that can help assess a developer’s grasp of the Rails foundation, including some of its more subtle nuances, are therefore an important component of the interview process.
Here are some examples:
Q: Explain the processing flow of a Rails request.
At the highest level, Rails requests are served through an application server, which is responsible for directing an incoming request into a Ruby process. Popular application servers that use the Rack web request interface include Phusion Passenger, Mongrel, Thin, and Unicorn.
Rack parses all request parameters (as well as posted data, CGI parameters, and other potentially useful bits of information) and transforms them into a big Hash (Ruby’s record / dictionary type). This is sometimes called the env
hash, as it contains data about the environment of the web request.
In addition to this request parsing, Rack is configurable, allowing for certain requests to be directed to specific Rack apps. If you want, for example, to redirect requests for anything in your admin section to another Rails app, you can do so at the Rack level. You can also declare middleware here, in addition to being able to declare it in Rails.
Those requests that are not directed elsewhere (by you in Rack) are directed to your Rails app where it begins interacting with the Rails ActionDispatcher, which examines the route. Rails apps can be spit into separate Rails Engines, and the router sends the request off to the right engine. (You can also redirect requests to other Rack compatible web frameworks here.)
Once in your app, Rails middleware – or your custom middleware – is executed. The router determines what Rails controller / action method should be called to process the request, instantiates the proper controller object, executes all the filters involved, and finally calls the appropriate the action method.
Further detail is available in the Rails documentation.
Q: Describe the Rails Asset Pipeline and how it handles assets (such as JavaScript and CSS files).
Rails 3.1 introduced the Asset Pipeline, a way to organize and process front-end assets. It provides an import/require mechanism (to load dependent files) that provides many features. While the Asset Pipeline does have its rough edges, it does solve and provide many of the modern best practices in serving these files under HTTP 1.1. Most significantly, the Asset Pipeline will:
- Collect, concatenate, and minify all assets of each type into one big file
- Version files using fingerprinting to bust old versions of the file in browser caches
The Asset Pipeline automatically brings with it Rails’ selection of Coffeescript as its JavaScript pre-processed / transpiled language of choice and SASS as its CSS transpiled language. However, being an extensible framework, it does allow for additional transpiled languages or additional file sources. For example, Rails Assets brings the power of Bower to your Rails apps, allowing you to manage third-party JavaScript and CSS assets very easily.
Q: What is Active Record and what is Arel? Describe the capabilities of each.
Active Record was described by Martin Fowler in his book Patterns of Enterprise Application Architecture as “an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data”.
ActiveRecord is both an Object Relational Mapping (ORM) design pattern, and Rails’ implementation of that design pattern. This means that fetching, querying, and storing your objects in the database is as much a part of the API of your objects as your custom business logic. A developer may see this as an undesired side effect, or as a welcome convention, depending on their preference and level of experience.
Arel provides a query API for ActiveRecord, allowing Ruby on Rails development experts to perform database queries without having to hand-write SQL. Arel creates lazily-executed SQL whereby Rails waits until the last possible second to send the SQL to the server for execution. This allows you to take an Arel query and add another SQL condition or sort to the query, right up to the point where Rails actually executes the query. Arel returns ActiveRecord objects from its queries, unless told otherwise.
Q: What is the Convention over Configuration pattern? Provide examples of how it is applied in Rails.
Convention over Configuration (CoC) is a software design pattern by which only the unconventional aspects of an application need to be specified by a developer. When the default convention matches the desired behavior, the default behavior is followed without any configuration being required. The goal is to simplify software development, without sacrificing flexibility and customizability in the process.
Here are some examples of how CoC principles are applied in Rails:
-
Model and database table naming. Rails automatically pluralizes class names to find the respective database tables. For a class Book, for example, it will expect a database table named books. For class names composed of multiple words, the model class name should employ CamelCase (e.g.,
BookClub
and book_clubs
).
-
Primary and foreign keys. By default, Rails uses an integer column named
id
as the table’s primary key. Foreign key names by default follow the pattern of appending _id
to the singularized tablename (e.g., item_id
for a foreign key into the items
table).
-
Reserved words for automatic functionality. There are also some optional column names which, if used, automatically add features and functionality to Rails database tables.
created_at
, for example, will automatically be set to the date and time when the record was created. Similarly, updated_at
will automatically be set to the date and time whenever the record was last updated.
-
Auto-loading of class definitions. Auto-loading is the “magic” by which classes appear to be accessible from anywhere, without the need to explicitly require them. Here’s how it works: When you reference a class in your code, Rails takes the class name (with namespace) as a string, calls underscore on it, and looks for a file with that name (in all directories specified in your
config.autoload_paths
). For example, if you reference a class named FileHandling::ZipHandler
, Rails will automatically search for file_handling/zip_handler.rb
in your config.autoload_paths
. This feature often results in novice Rails programmers thinking that they don’t need to explicitly require referenced classes and that Rails will just auto-magically find them anyway. They then become baffled when they don’t follow this convention and are suddenly being told by Rails that their classes can’t be found.
It is important to note that CoC specifies a default –- but not immutable –- convention. Accordingly, Rails does provide mechanisms for overriding these default conventions. As an example, the default database table naming scheme mentioned above can be overridden by specifying the ActiveRecord::Base.table_name
as shown here:
class Product < ActiveRecord::Base
self.table_name = "LEGACY_PRODUCT_TABLE"
end
Q: What is the “fat model, skinny controller” approach? Discuss some of its advantages and pitfalls, as well as some alternatives.
“Fat model skinny controller” is an MVC-based Rails design pattern.
MVC is itself a software design pattern that separates a system into three separate and distinct layers; namely, Model, View, and Controller. MVC strives to ensure a clean separation between each of its layers through clearly defined APIs. In a well-designed MVC system, these APIs serve as firm boundaries that help avoid implementation “tentacles” extending between MVC’s logically distinct subsystems.
The “Fat model skinny controller” design pattern advocates placing as much logic as possible in the Model for (a) maximum reuse and (b) code that is easier to test.
That said, a common pitfall for Rails developers is to end up with “overly bloated” models by adhering too blindly to the “fat model, skinny controller” paradigm. The infamous User model is a prime example of this. Since many Rails apps are about the user entering data into the system, or sharing information with their friends socially, the user model will often gain more and more methods, eventually reaching the point where the user.rb
model becomes bulky and unmanageable in size.
A few key alternatives worth considering include:
- Use of other objects: Extract functionality out of models into other objects (such as Decorators or Service objects)
-
Hexagonal architecture for Rails: Employ a hexagonal architecture that views the application as a hexagon, each side of which represents some sort of external interaction the application needs to have.
-
DCI (Data Context Interaction): Instead of focusing on individual objects, focus on the communication and interactions between data and its context.
Q: Describe the Rails testing philosophy.
Rails built testing support in from the beginning of the framework, and it became a part of the culture. As a result, there are a plethora of tools available for testing in the Rails environment.
By default, Rails 4.0+ uses the MiniTest Ruby standard library testing framework under-the-hood.
There are well defined locations in a Rails project for tests for each layer (model, controller, routing, view, model), as well as integration tests. Because of the MVC foundation of Rails, often these layers (with the exception of integration tests) can be tested without reliance on the other layers.
For example, we can create a database record, before the test runs, that contains the attributes we expect the test to return. Our test can focus on making sure our show post controller action retrieves the post we want it to by checking to see if it returns the object we created above as expected. If not, something went wrong or our code must have a bug. Here’s an example of such a test:
class PostsControllerTest < ActionController::TestCase
setup do
@post = posts(:one)
end
test "should show post" do
get :show, id: @post
assert_response :success
end
end
Integration tests (often called Feature tests) will usually drive the application as if a user is clicking buttons, using testing tools like Capybara (which can simulate user actions in a variety of manners, including driving embedded WebKit, or using Selenium).
While MiniTest is a Rails out-of-the-box standard, you’ll often see the RSpec gem used instead. This provides a Domain Specific Language for testing that may make it more natural to read than MiniTest.
Some Rails projects use the Cucumber testing framework to describe software behavior in plain English sentences. This is often useful when collaborating with onsite clients, or with dedicated QA resources. In the ideal world, these non-developers can write automated integration tests without having to see a line of Ruby code.
Down on the tracks
Someone who has worked extensively with Rails can be expected to possess a great deal of familiarity with its capabilities, constructs, and idiosyncrasies. These questions demonstrate ways of gauging the extent and depth of this expertise.
Q: Explain the use of yield
and content_for
in layouts. Provide examples.
yield
identifies where content from the view should be inserted. The simplest approach is to have a single yield
, into which the entire contents of the view currently being rendered is inserted, as follows:
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>
You can also create a layout with multiple yielding regions:
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
The main body of the view will always render into the unnamed yield
. To render content into a named yield
, use the content_for
method. content_for
allows for insertion of content into a named yield
block in a layout. This can be helpful with layouts that contain distinct regions, such as sidebars and footers, into which distinct blocks of content are to be inserted. It can also be useful for inserting tags that load page-specific JavaScript or CSS files into the header of an otherwise generic layout.
Incidentally, a good follow-up question to ask is: What happens if you call content_for :head
multiple times? The answer is that all of the values get concatenated.
Q: What are N+1 queries, and how can you avoid them?
Consider the following code, which finds 10 clients and prints their postal codes:
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
This code actually executes 11 queries; 1 (to find 10 clients) and then 10 more (one per each client to load its address). This is referred to as an “N+1 query” (where in the case of this example, N is 10).
Eager loading is the mechanism for loading the associated records of the objects returned by Model.find
using as few queries as possible.
Active Record’s eager loading capability makes it possible to significantly reduce the number of queries by letting you specify in advance all the associations that are going to be loaded. This is done by calling the includes
(or preload
) method on the Arel (ActiveRecord::Relation
) object being built. With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
We could therefore rewrite the above code to use the includes
method as follows:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
This revised version of this code will execute just 2 queries, thanks to eager loading, as opposed to 11 queries in the original version.
Q: What are “filters” in Rails? Describe the three types of filters, including how and why each might be used, and the order in which they are executed. Provide examples.
Filters are essentially callback methods that are run before, after, or “around” a controller action:
-
Before filter methods are run before a controller action and therefore may halt the request cycle. A common before filter is one which requires a user to be logged in for an action to be performed.
-
After filter methods are run after a controller action and therefore cannot stop the action from being performed but do have access to the response data that is about to be sent to the client.
-
Around filter methods are “wrapped around” a controller action. They can therefore control the execution of an action as well as execute code before and/or after the action is performed.
For example, in a website where changes have an approval workflow, an administrator could be able to preview them easily with an around filter as follows:
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
Note that an around filter also wraps rendering. In particular, in the example above, if the view reads from the database (e.g., via a scope), it will do so within the transaction and thus present the data to preview. You can also choose not to yield and build the response yourself, in which case the action will not be run.
The order of execution is a bit tricky and is important to understand clearly. Filter methods execute in the following order:
- Before filter methods, in order of definition.
- Around filter methods, in order of definition.
- After filter methods, in reverse order.
Also, because of the way Ruby instantiates classes, the filter methods of a parent class’ before will be run before those of its child classes.
Q: What is Rack middleware? How does it compare to controller filters/actions?
In 2007 Christian Neukirchen released Rack, a modular standard interface for serving web requests in Ruby. Rack is similar to other similar mechanisms in other languages, such as WSGI on the Python side, or Java Servlets, or Microsoft’s Internet Server Application Programming Interface (ISAPI).
Before requests are processed by your Rails action method, they go through various Rack middleware functions declared by Rails or by the developer. Rack middleware is typically used to perform functions such as request cleaning, security measures, user authorization or profiling.
You can see a list of available middleware components (both developer defined and those defined by Rails) by running rake middleware
on the command line.
A key distinction between Rack middleware and filters is that Rack middleware is called before Rails does its routing and dispatching, whereas filters are invoked after this routing has occurred (i.e., when Rails is about to call your controller action method). As such, its is advantageous to filter out requests to be ignored in middleware whenever possible, such as requests from common attack URLs (phpadmin.php
requests, for example, can be discarded in middleware, as they will never resolve in a Rails app and is probably just some attempt to hack the site.)
Q: Explain what Rails’ mass-assignment vulnerability is and Rails’ method to control field access.
When the user performs a post (such as, for example, creating a new User
) Rails needs to save all that new data into the database. This data is accessible from your Rails action via the params
Hash.
Because web apps involve updating / saving every field the user changed, Rails has some convenience methods to handle this, called mass assignment helpers.
For example, prior to Rails 4, creating a new User
object with parameters from a submitted form looked like:
User.create(params[:user])
params[:user]
will contain keys for the elements the user entered on the form. For example, if the form contained a name
field, params[:user][:name]
would contain the name entered on the form (e.g., “Jeff Smith”).
Convention vs. configuration strikes again here: name
is the name of both the input element in the form and the name of the column in the database.
In addition to the create
method, you can update a record the same way:
@user = User.find(params[:id])
@user.update_attributes(params[:user])
But what happens when a hacker goes in and edits your HTML form to add new fields? They may, for example, guess that you have an is_admin
field, and add it to the HTML form field themselves. Which now means that – even though you didn’t include it on the form that’s served to users – your hacker has gone in and made themselves an admin on your site!
This is referred to as mass assignment vulnerability; i.e., assigning all these fields with no filtering en masse, just trusting that the only field names and values will be those that were legitimately on the HTML form.
Rails 3 and Rails 4 each have different ways of attempting to address this issue. Rails 3 attempted to address it via attr_protected
/ attr_accessible
controls at the model level, while Rails 4 addresses it via strong parameters and a filtering mechanism at the controller level. Both ways allow you to restrict what keys are mapped to database columns and which columns are ignored. Using these mechanisms, in the prior is_admin
example, you can set the is_admin
field to only change when code explicitly modifies the field value, or only allow it to be changed in certain situations.
The Big Picture
An expert knowledge of Rails extends well beyond the technical minutia of the language. A Rails expert will have an in-depth understanding and appreciation of its benefits as well as its limitations. Accordingly, here are some sample questions that can help assess this dimension of a candidate’s expertise.
Q: Why do some people say “Rails can’t scale”?
Twitter was one of the first extremely high profile sites to use Rails. In roughly the 2006-2008 timeframe, the growth rate of Twitter made server errors (“fail whale”) appearances a very common occurrence for users, prompting users and tech pundits to lay blame at Rails’ feet. As is true with any software, the causes of scalability issues can be complex and multi-faceted. Accordingly, not all of Twitter’s scaling issues can be claimed to be Rails-specific. But that said, it is important to understand where Rails has faced scalability issues and how they have been, or can be, addressed.
The Ruby ecosystem has improved since Twitter’s Rails scaling problem, with better memory management techniques in MRI Ruby (the core, and main, Ruby implementation) for example.
Modern Rails applications typically mitigate scaling problems in one or more of the following ways:
- Implementing caching solutions (Rails 4 introduces good advances here)
- Leveraging (or implementing) server or platform solutions with automatic scaling built in
- Profiling costly operations and moving them out of Ruby or out of one monolithic Rails app
- Placing some operations in a background / worker queue to be completed at a later time (e.g., perform an export operation asynchronously, notifying the user by email with a download link when the export is completed)
While there has traditionally been a one-to-one mapping between websites and RoR applications (i.e., one website = one Rails app), there’s been an increasing movement towards more of a Service Oriented Architecture (SOA) approach whereby performance critical parts of the app are split off into new/separate apps which the main app usually talks to via web service calls. There are numerous advantages to this approach. Perhaps most noteworthy is the fact that these independent services can employ alternate technologies as appropriate; this might be a lightweight / more responsive solution in Ruby, or services written in Scala (as in Twitter’s case), or Node.js, Clojure, or Go.
But writing separate services isn’t the only way to speed up a Rails app. For example, Github has an interesting article on how it profiled Rails and ended up implementing a set of C apis for performing text escaping on the web.
Q: When is Rails a good choice for a project?
Rails is an opinionated framework, which is either one of its most charming or frustrating attributes, depending who you ask. Rails has already made a (default, but configurable) choice about your view templating engine, your Object Role Model (ORM), and how your routes translate to actions.
As a result of these choices, Rails is a great choice for a project where your application has total control over its own database, mostly returns HTML (or at least doesn’t solely return JSON), and for the most part displays data back to the users consistently with the way it is stored.
Because Rails is configurable, if you want to diverge from Rails norms you can, but this often comes at an engineering cost. Want to hook into an existing MS SQL database? You can do that, but you’ll hit some bumps along the way. Want to build a single page app with Rails, returning mostly JSON object? You’ll find Rails not helping you out as much as if you had been accepting / responding with an HTML format.
Q: What are some of the drawbacks of Rails?
Rails is generally meant for codebases of greater than a few hundred lines of code, and that primarily work with its own database objects. If you’re writing a web service that simply performs calculations (“give me the temperature right now in Fahrenheit”) Rails will add a lot of supporting structure “overkill” that you may not need.
Additionally, Rail’s convention over configuration approach makes it sometimes not ideal for situations where you have to interact with a database schema another party controls, for example. Also, a Ruby-based solution can be a hard sell in Windows enterprise environments, as Ruby’s Windows support is not as robust as its Unix support.
Like Python, the concurrency story in the default Ruby implementation (MRI; a.k.a. CRuby) is somewhat hobbled by a Global Interpreter Lock (GIL), which in broad strokes means only one thread can execute Ruby code at a time. (JRuby and Rubinius, other implementations of Ruby, have no GIL.
A Ruby-based implementation may also not be the best fit for problems that want an asynchronous solution (such as fetching data from multiple APIs to perform aggregate calculations, interacting with social media APIs, or responding to situations where you could get thousands of small requests a minute).
Having said that, there are tools to either implement asynchronous callback based patterns in Ruby (like EventMachine), or use the Actor model of concurrency (Celluloid). And of course there are a number of background worker mechanisms if your problem fits in that space.
And finally… Ruby Rookie or Gemologist?
Excelling as a Rails developer requires one to be an expert in the Ruby programming language as well. Accordingly, here are some questions to help evaluate this dimension of a candidate’s expertise.
Q: What are Ruby mixins, how do they work, and how would you use them? What are some advantages of using them and what are some potential problems? Give examples to support your answers.
A “mixin” is the term used in Ruby for a module included in another class. When a class includes a module, it thereby “mixes in” (i.e., incorporates) all of its methods and constants. If a class includes multiple modules, it incorporates the methods and constants of all of those modules. Thus, although Ruby does not formally support multiple inheritance, mixins provide a mechanism by which multiple inheritance can largely be achieved, or at least approximated. (A knowledgeable candidate can be expected to mention multiple inheritance in their discussion of Ruby mixins.)
Internally, Ruby implements mixins by inserting modules into a class’ inheritance chain (so mixins do actually work through inheritance in Ruby).
Consider this simple example:
module Student
def gpa
# ...
end
end
class DoctoralStudent
include Student
def thesis
# ...
end
end
phd = DoctoralStudent.new
In this example, the methods of the Student
class are incorporated into DoctoralStudent
class, so the phd
object supports the gpa
method.
It is important to note that, in Ruby, the require
statement is the logical equivalent of the include
statement in other languages. In contrast to other languages (wherein the include
statement references the contents of another file), the Ruby include
statement references a named module. Therefore:
- The module referenced by an
include
statement may either be in the same file (as the class that is including it) or in a different file. If in a different file, a require
statement must also be used to properly incorporate the contents of that file.
- A Ruby
include
makes a reference from the class to the included module. As a result, if the definition of a method in the included module is modified, even at runtime, all classes that include that module will exhibit the new behavior when that method is invoked.
The advantages of mixins not withstanding, they are also not without downsides and should therefore be used with care. Some potential pitfalls include:
-
Instance variable name collisions. Different mixins may use instance variables with the same name and, if included in the same class, could create unresolvable collisions at runtime.
-
Silent overriding of methods. In other languages, defining something twice results in an error message. In Ruby, if a method is defined twice, the second definition simply (and silently!) overwrites the first definition. Method name clashes across multiple mixins in Ruby are therefore not simple errors, but instead can introduce elusive and gnarly bugs.
-
Class bloat. The ease-of-use of mixins can also lead to their “abuse”. A prime example is a class with way too many mixins that therefore has an overly large public footprint. The rules of coupling and cohesion start to come into play, and you can end up with a system where changes to a module that’s frequently included can have disastrous effects. Traditional inheritance or composition is much less prone to this type of bloat. Quite often extracting parts of a class into modules that are mixed in is akin to cleaning your room by putting the mess into large bins. It looks clean until you start opening the bins.
Q: Compare and contrast Symbols and Strings in Ruby? Why use one vs. the other?
Symbols are singleton based on value, and immutable objects. Unlike strings, not all symbols may be garbage collected.
Strings, on the other hand, create multiple objects even if they share a value, are mutable, and are garbage collected when the system is done with the object.
Since symbols are singleton based on value (there is only one symbol object for a value, even if it appears multiple times in a program), this makes it trivial to compare whether two symbols are the same (Ruby basically just needs to compare their object_id values). Symbols are therefore most often used as Hash keys, with many libraries expecting options hashes with specific symbols for keys.
Strings can be made immutable (“frozen”) via the freeze method. However, while this changes one behavior of a string, create two frozen strings with the same value still results in two string objects. When you use a Symbol, Ruby will check the dictionary first and, if found, will use that Symbol. If the Symbol is not found in the dictionary, only then will the interpreter instantiate a new Symbol and put it in the heap.
As stated in The Ruby Programming Language O’Reilly book (by Matz and Flanigan):
A typical implementation of a Ruby interpreter maintains a symbol table in which it stores the names of all the classes, methods, and variables it knows about. This allows such an interpreter to avoid most string comparisons: it refers to method names (for example) by their position in this symbol table. This turns a relatively expensive string operation into a relatively cheap integer operation.
Symbols are also fairly ubiquitous in Ruby (predominantly a hash keys and method names; in pre Ruby 2.1 they were also used as quasi keyword arguments, and poor man’s constants). Because of their performance, memory and usage considerations, Symbols are most often used as Hash keys, with many libraries expecting option hashes with specific symbols for keys.
Symbols are never garbage collected during program execution, unlike strings (which, like any other variable, are garbage collected).
Because strings and symbols are different objects, here’s an example of something that often catches less experienced Ruby programmers unaware.
Consider the following hash, for example:
irb(main):001:0> a = {:key => 1}
irb(main):002:0> puts a['key']
=> nil.
You may be expecting to see 1
printed here, especially if a
was defined elsewhere in your program. But, as strings and symbols are different; i.e., key
(the symbol) and 'key'
(the string) are not equivalent. Accordingly, Ruby correctly returns nil
for a['key']
(even though this is annoying for the unsuspecting programmer wondering where her value is!)
Rails has a class, HashWithIndifferentAccess
, which acts like a Hash object, except it treats strings and symbols with the same values as equivalent when used as key names, thereby avoiding the above issue:
irb(main):001:0> a = HashWithIndifferentAccess.new({:key => 1})
irb(main):002:0> puts a['key']
=> 1
There is one important caveat. Consider this controller action:
def check_value
valid_values = [:bad, :ok, :excellent]
render json: valid_values.include?(params[:value].to_sym)
end
This innocent looking code is actually a denial-of-service (DOS) attack vulnerability. Since symbols can never be garbage collected (and since here we cast user input into a symbol), a user can keep feeding this endpoint with unique values and it will eventually eat up enough memory to crash the server, or at least bring it to a grinding halt.
Q: Describe multiple ways to define an instance method in Ruby.
Instance methods can of course be defined as part of a class definition. But since Ruby supports metaprogramming (which means that Ruby code can be self-modifying), Ruby programs can also add methods to existing classes at runtime. Accordingly, there are multiple techniques for defining methods in Ruby, as follows:
(1) Within a class definition, using def
(The simplest answer)
class MyObject
def my_method
puts "hi"
end
end
This is the standard way to define instance methods of a class.
(2) Within a class definition, without using def
class MyObject
define_method :my_method do
puts "hi"
end
end
Since define_method
is executed when Ruby instantiates the MyObject
class object, you can do any kind of dynamic code running here. For example, here’s some code that only creates our method if it’s being run on a Monday:
require 'date'
class MyObject
if Date.today.monday?
define_method :my_method do
puts "Someone has a case of the Mondays"
end
end
end
Starting the application on any other day of the week will make executing MyObject.new.my_method
raise an exception about no such method existing (even if it is executed on Monday). Which is true: It’ll only exist if the class body was evaluated on a Monday!
(It’s important to note that classes are objects too in Ruby, so our MyObject
class is an instantiation of a Class
object, just like in a = MyObject.new
, a
is an instance of MyObject
.)
(3) Extending an existing class definition
Ruby also allows class definitions to be extended. For example:
class MyObject
def say_hello
puts "hey there!"
end
end
class MyObject
def say_goodbye
puts "buh-bye"
end
end
The above code will result in the MyObject
class having both the say_hello
and the say_goodbye
methods defined.
Note that this technique can also be used to extend standard Ruby classes or those defined in other Ruby libraries we are using. For example, here we add a squawk
method to the standard Ruby string class:
class String
def squawk
puts "SQUAWK!!!"
end
end
"hello".squawk
(4) Using class_eval
class_eval
dynamically evaluates the specified string or block of code and can therefore be used to add methods to a class.
For example, we can define the class MyObject
:
class MyObject
...
end
… and then come back at a later time and run some code to dynamically add my_method
to the MyObject
class:
MyObject.class_eval do
def my_method
puts "hi"
end
end
(5) Using method missing
Ruby also provides a hook to check for undefined methods. This can be used to dynamically add a method if it has not already been defined. For example:
class MyObect
def method_missing(method_name_as_symbol, *params)
if method_name_as_symbol == :my_method
puts "hi"
end
end
end
Wrap Up
Ruby on Rails is a powerful framework for rapid web development. While all developers can benefit from its ease-of-use and flexibility, as with any technology, those who have truly mastered it and the development process will realize the greatest potential and productivity in its use.
While no brief guide such as this can entirely cover the breadth and depth of technical topics to cover in an interview to hire RoR developers, the questions provided herein offer an effective basis for identifying those who possess a sound and principled foundation in the Rails framework and its paradigms.