Ruby Science

Use Convention Over Configuration

Ruby’s meta-programming allows us to avoid boilerplate code and duplication by relying on conventions for class names, file names and directory structure. Although depending on class names can be constricting in some situations, careful use of conventions will make your applications less tedious and more bug-proof.

Uses

  • Eliminates case statements by finding classes by name.
  • Eliminates shotgun surgery by removing the need to register or configure new strategies and services.
  • Eliminates duplicated code by removing manual associations from identifiers to class names.
  • Prevents future duplication, making it easier to avoid duplication.

Example

This controller accepts an id parameter identifying which summarizer strategy to use and renders a summary of the survey based on the chosen strategy:

# app/controllers/summaries_controller.rb
class SummariesController < ApplicationController
  def show
    @survey = Survey.find(params[])
    @summaries = @survey.summarize(summarizer)
  end

  private

  def summarizer
    case params[]
    when 'breakdown'
      Breakdown.new
    when 'most_recent'
      MostRecent.new
    when 'your_answers'
      UserAnswer.new(current_user)
    else
      raise "Unknown summary type: #{params[]}"
    end
  end
end

The controller is manually mapping a given strategy name to an object that can perform the strategy with the given name. In most cases, a strategy name directly maps to a class of the same name.

We can use the constantize method from Rails to retrieve a class by name:

params[].classify.constantize

This will find the MostRecent class from the string "most_recent", and so on. This means we can rely on a convention for our summarizer strategies: Each named strategy will map to a class implementing that strategy. The controller can use the class as an abstract factory and obtain a summarizer.

However, we can’t immediately start using constantize in our example, because there’s one outlier case: The UserAnswer class is referenced using "your_answers" instead of "user_answer", and UserAnswer takes different parameters than the other two strategies.

Before refactoring the code to rely on our new convention, let’s refactor to obey it. All our names should map directly to class names and each class should accept the same parameters:

# app/controllers/summaries_controller.rb
def summarizer
  case params[]
  when 'breakdown'
    Breakdown.new(current_user)
  when 'most_recent'
    MostRecent.new(current_user)
  when 'user_answer'
    UserAnswer.new(current_user)
  else
    raise "Unknown summary type: #{params[]}"
  end
end

Now that we know we can instantiate any of the summarizer classes the same way, let’s extract a method for determining the summarizer class:

# app/controllers/summaries_controller.rb
def summarizer
  summarizer_class.new(current_user)
end

def summarizer_class
  case params[]
  when 'breakdown'
    Breakdown
  when 'most_recent'
    MostRecent
  when 'user_answer'
    UserAnswer
  else
    raise "Unknown summary type: #{params[]}"
  end
end

The extracted method performs exactly the same logic as constantize, so let’s use it:

# app/controllers/summaries_controller.rb
def summarizer
  summarizer_class.new(current_user)
end

def summarizer_class
  params[].classify.constantize
end

Now we’ll never need to change our controller when adding a new strategy; we just add a new class following the naming convention.

Scoping constantize

Our controller currently takes a string directly from user input (params) and instantiates a class with that name.

There are two issues with this approach that should be fixed:

  • There’s no list of available strategies, so a developer would need to perform a complicated search to find the relevant classes.
  • Without a whitelist, users can make the application instantiate any class they want, by hacking parameters. This can result in security vulnerabilities.

We can solve both easily by altering our convention slightly: Scope all the strategy classes within a module.

We change our strategy factory method:

# app/controllers/summaries_controller.rb
def summarizer
  summarizer_class.new(current_user)
end

def summarizer_class
  params[].classify.constantize
end

To:

# app/controllers/summaries_controller.rb
def summarizer_class
  "Summarizer::#{params[].classify}".constantize
end

With this convention in place, you can find all strategies by just looking in the Summarizer module. In a Rails application, this will be in a summarizer directory by convention.

Users also won’t be able to instantiate anything they want by abusing our constantize, because only classes in the Summarizer module are available.

Drawbacks

Weak Conventions

Conventions are most valuable when they’re completely consistent.

The convention is slightly forced in this case because UserAnswer needs different parameters than the other two strategies. This means that we now need to add no-op initializer methods to the other two classes:

# app/models/summarizer/breakdown.rb
class Summarizer::Breakdown
  def initialize(options)
  end

  def summarize(question)
    question.breakdown
  end
end

This isn’t a deal-breaker, but it makes the other classes a little noisier and adds the risk that a developer will waste time trying to remove the unused parameter.

Every compromise made weakens the convention, and having a weak convention is worse than having no convention. If you have to change the convention for every class you add that follows it, try something else.

Class-Oriented Programming

Another drawback to this solution is that it’s entirely class-based, which means you can’t assemble strategies at run-time. This means that reuse requires inheritance.

Also, while this class-based approach is convenient when developing an application, it’s more likely to cause frustration when writing a library. Forcing developers to pass a class name instead of an object limits the amount of runtime information strategies can use. In our example, only a user was required. When you control both sides of the API, it’s fine to assume that this is safe. When writing a library that will interface with other developers’ applications, it’s better not to rely on class names.

Ruby Science

The canonical reference for writing fantastic Rails applications from authors who have created hundreds.

Work with us to make a new Rails app, or to maintain, improve, or scale your existing app.