Konrad Reiche About Photos Talks

Singleton-Active Record Charade

Ruby on Rails applications are modelled around active record yet what if your application is based on a domain which needs to be provided in a programmatic way? In other words, your application, controller and views stand on a fixed set of Ruby classes yielding the context. In a manual fashion one would implement this by creating controller and views for each specific class. Assuming that the classes of this domain share the same interface this approach would introduce a lot of redundancy. With meta-programming this amount can be cut down.

For this example we look at a Rails application implementing an analytics platform which will providing useful information using the following metrics.

Each metric has to provide compute method which defines how the quantitative value is calculated. Naturally, these metrics want to be translated to Ruby classes. Another requirement is to display and visualize each metric result on a different page. The more metrics are added the more sophisticated the platform would become, hence it would be reasonable to have a simple interface for adding new metrics without writing the same boilerplate code and over again. A simple layout would look like this:

class Metrics::Metric
end

class Metrics::UniquePageVisitors < Metrics::Metric
end

class Metrics::NumberOfVisits < Metrics::Metric
end

class Metrics::NumberOfGoalConversions < Metrics::Metric
end

class Metrics::TotalTimeSpent < Metrics::Metric
end

class Metrics::Revenue < Metrics::Metric
end

The whole point of this pattern is whenever a new metric class is added it should automatically appear within the application and become accessible through views. As a first pass all available metrics should be listed on an index page, favorably in an active record fashion using .all:

<ul>
  <% Metrics::Metric.all.each  do |metric| %>
    <li><%= metric.to_s.titlecase %></li>
  <% end  %>
</ul>

How can this be implemented without extending ActiveRecord::Base? After all our classes are not persisted in a database. For starters let us add our own Metric.all method which should return all metric classes, here kept in an array.

class Metrics::Metric

  def self.all
    @@metrics
  end

end

How can this array be prepoulated? Certainly, by iterating over files in a subdirectory, for instance in app/models/metrics and then includimg them. While this is feasible there is an easier and more flexible way by utilizing Ruby’s inherited method:

inherited(subclass) Callback invoked whenever a subclass of the current class is created.

Not only is this more elegant but it actually allows to add new classes dynamically to the stack even at runtime:

class Metrics::Metric
  include Singleton

  def self.inherited(subclass)
    super
    @@metrics ||= []
    @@metrics << subclass.instance
  end

  def id
    self.class.to_s.demodulize.underscore.dasherize
  end

  def to_s
    id.titleize
  end

end

Extending this to the view each metric could implement a specific view with possibly a default view which can be reused for very simple metrics. Each specific view would be stored inside a partial. Thinking in hierarchies concrete metrics could subclass other concrete metrics for sharing specific behaviors. For instance revenue could be based on the number of goal conversions:

class Metrics::Revenue < Metrics::NumberOfGoalConversions
end

Hence, there might be a scenario in which we want to fallback to the nearest ancestor implementing the most concrete view for this metric. The code for this could look like the following:

class MetricsController < ApplicationController

  helper_method :select_partial

  def select_partial
    partials = "metrics/partials"
    directory = "app/views/" + partials

    ancestors = @metric.class.ancestors
    ancestors = ancestors.select { |cls| cls < Metrics::Metric }

    ancestors.map { |cls| cls.to_s.demodulize.underscore }.each do |candidate|
      file = "#{directory}/_#{candidate}.html.erb"
      return "#{partials}/#{candidate}" if File.exists?(file)
    end

    "metrics/partials/generic"
  end
end

For this to work Metric needs to extend ActiveModel::Naming.

Default implementations of model.model_name, which are used by ActionPack (for instance, when you using render :partial => model)

This pattern should have given a small overview of how to integrate models into the realm of active models without the need to use active record directly. Rails has come a long way to refactor essential functionality out of active record into dedicated APIs allowing us to implement powerful constructs winged by Ruby’s meta-programming capabilities.