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.
- Unique page visitors
- Number of visits
- Number of goal conversions
- Total time spent
- Revenue
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.