A bit of context
While working on a new Ruby on Rails pet project of my own, a very common decision-making situtation for projects like this one arose:
- How should one deal with presentation-related logic of model data in a way that differs from the its standard persisted format?
In this case, the data to be presented was basically an automatically calculated field (for the sake of this article I will call it calculated <Float>
) which depended on two other user-input fields which were also persisted into the DB: input_a <Float>
and input_b <Float>
.
Normalization
To go forward in common terms, let's assume for this article that:
- We are dealing with a model called
Generic
; - This model has a
GenericsController
controller class; - The associated action view template is
show.html.erb
;
The problem
Interface-wise, the expected way to render calculated
's value was in a 1/N ratio format, in which 1 was the common nominator while N was a variable denominator. As expected, this would require a bit of raw data presentation treatment logic put into calculated
's value.
If we had, let's say, calculated = 0.075
, then:
# object.calculated = 0.075 => object.presentable_calculated = 1/13
The straight-forward possible solutions
The following list (not an exhaustive one) presents some of the most common solving possibilities for the problem:
- Exposing a
@presentable_calculated
instance variable into the related action methods onGenericsController
, so that they are available to the related action view templates;
class GenericsController < ApplicationController
# ... Boilerplate code
def show
@generic = Generic.find(params[:generic_id])
@presentable_calculated = begin do
denominator = 1 / @generic.calculated
"1/#{denominator}"
end
end
end
- Implementing a
GenericsHelper
module to center the formatting logic:
module GenericsHelper
def presentable_calculated(calculated)
denominator = 1 / calculated
"1/#{denominator}"
end
end
- Implementing the
#presentable_calculated
method into theGeneric
model itself:
class Generic < ApplicationRecord
# ... Boilerplate code
def presentable_calculated
denominator = 1 / calculated
"1/#{denominator}"
end
end
While functional at first glance, each of these has its own downsides:
The Controller solution:
Exposing a new @presentable_calculated
instance variable into the controller means highly coupling view needs with controller action implementation.
A general action method (#show
in this example) would then have and grant access to a view-specific data. This approach possibly breaks a number of clean code/design patterns good practices, the clearest of them being the single responsibility principle.
The Helper solution:
While indeed helpful, Rails Helper
s tend to become cluttered over time if not used carefully for a simple reason: they start to feel like the go-to solution when a piece of code doesn't seem to fit into any other place in your codebase.
The anti-pattern can start to take its toll when we notice that, by default, Rails Controller
s and view templates all have access to all of the existing Helper
modules in your codebase.
The Model solution:
Close to what we discussed in The Helper Solution section, Rails models can also start to become cluttered with code that seems to belong but, in reality, don't.
In object oriented-programming, this is what we call The God class (god object) anti-pattern: one or more classes which hold too many responsibilities.
Take the current presentation context we're into, moving the presentation logic to a model class would mean centering view-specific needs into a class which might already know more than enough, mixing different domains in a single place. Yet another SOLID single responsibility violation (among other possible issues).
An alternate solution: The Decorator Pattern
This is where the Decorator Pattern (also known as Wrapper) comes in place.
The main idea behind the technique is:
- Wrap your original object around a stack of wrappers, each of which implement the needed API (either with the same or different signatures), thus extending your original object with extra behaviors.
In terms of our Ruby code, that would mean something like the following:
# app/models/generics.rb
class Generic < ApplicationRecord
# Inner implementations
def decorator
GenericsDecorator.new(self)
end
end
# app/decorators/generics_decorator.rb
class GenericsDecorator
attr_reader :object
def initialize(object)
@object = object
end
def presentable_calculated
denominator = 1 / calculated
"1/#{denominator}"
end
end
Then, in a hypothetical view:
<!--app/views/generics/show.html.erb-->
<div>
<p class="bleus_class">
<% = @generic.decorator.presentable_calculated %>
</p>
</div>
With this solution, we avoid the aforementioned pitfalls of different possibilities while respecting the open-closed principle: the Generic
class was extended, not changed, to handle the view specific needs. It's also kept separate from the responsibility of implementing the #presentable_calculated
logic, respecting the separate responsibilities principle.
As a result, GenericsController
remains as a sole request handler class and the view's presentation logic is also kept lean.
The already invented wheel: the Draper gem
The ruby gems community already has an existing gem aimed at solving this same problem: Draper.
Take your time and explore the documentation in order to learn more about the topic.
Top comments (0)