How to decrease coupling in your controllers & views with decent_exposure for better maintainability
If you're a Rails developer, you're familiar with with the standard way of sharing data from the controller layer to the view layer. By convention, Rails copies all instance variables defined in the execution of a controller action to the view context. This standard usage will result in strong coupling between your controllers and views, as they are sharing state via instance variables.
To solve this problem, Stephen Caudill created the decent_exposure gem. Decent Exposure works by "exposing" methods over instance variables. This results in the ability to program views to an interface over a controller implementation.
Converting a standard controller to use decent_exposure
To demonstrate how to use decent_exposure, let's convert the follow controller:
class ComicsController < ApplicationController
def index
@comics = Comic.all
end
def show
@comic = Comic.find(params[:id])
end
def new
@comic = Comic.new
end
def edit
@comic = Comic.find(params[:id])
end
def create
@comic = Comic.new(params[:comic])
if @comic.save
redirect_to @comic, notice: 'Comic was successfully created.'
else
render action: "new"
end
end
def update
@comic = Comic.find(params[:id])
if @comic.update_attributes(params[:comic])
redirect_to @comic, notice: 'Comic was successfully updated.'
else
render action: "edit"
end
end
def destroy
@comic = Comic.find(params[:id])
@comic.destroy
redirect_to comics_url
end
end
To begin, we need to use the expose macro style method provided by decent_exposure to define the methods we want to make available to our controller. The expose macro will also declare the method as a helper method for your views to access:
class ComicsController < ApplicationController
respond_to :html
expose(:comics)
expose(:comic)
def create
comic.save
respond_with(comic)
end
def update
comic.save
respond_with(comic)
end
def destroy
comic.destroy
respond_with(comic)
end
end
At first glance, you will notice the removal of the index, show, new, and edit controller action definitions. This is a result of using another Rails convention. If a route is defined for the controller action, and a method is empty or not defined, Rails will by default render the standard template for the requested action. For example, if the index action was requested, Rails would render the view app/views/comics/index.html.erb by default. To clean up things further, we also take advantage of responders.
Also gone are the previously defined instance variables. The expose macro will initialize or query for resources based on some criteria. Here is a breakdown of how decent_exposure sets each resource in the above example:
- The collection of comics used by the
indexaction will be queried by default usingComic.scoped. - If no
:idparameter is available in the controllerparamshash,decent_exposurereturnsComic.new(params[:comic]). - If an
:idparameter is present,decent_exposurewill query for the resource by executingComic.find(params[:id]. If a plural exposed resource is available,decent_exposurewill scope the singular version to it. Thus,comicis set toComic.scoped.find(params[:id]).
Next, all calls to previously defined instance variables must be changed to their method counterpart. For example, all calls to @comics, must now send a message to comics instead.
<!-- app/views/comics/index.html.erb -->
<h1>Listing comics</h1>
<table>
<tr>
<th>Title</th>
<th>Volume</th>
<th></th>
<th></th>
<th></th>
</tr>
<% comics.each do |comic| %>
<tr>
<td><%= comic.title %></td>
<td><%= comic.volume %></td>
<td><%= link_to 'Show', comic %></td>
<td><%= link_to 'Edit', edit_comic_path(comic) %></td>
<td><%= link_to 'Destroy', comic, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New Comic', new_comic_path %>
Nested Resources
Another benefit to using the decent_exposure gem is its ability to work with nested resources out of the box. As an example, say comics were nested under a publisher, decent_exposure will be able to instantiates/queries for resources based on Rails conventions:
expose(:publisher)
expose(:comics, ancestor: :publisher)
expose(:comic)
In the above code, the exposed publisher method will be set by Publisher.find(params[:publisher_id]). Next, a nested relationship is defined via the exposed comics method. The comic exposed method will then use the pluaral exposed method to ininitialize/query resources. In the above example, quering for a Comic resource would call Publisher.find(params[:publisher_id]).comics.find(params[:id]).
Scopes
In instances where you would like to use a scope with a exposed method, pass a block to the expose macro:
expose(:comics) { Comic.recent }
Non-Conventional
Your application will likely contain instances where you do not want to query by an id. In those cases, decent_exposure provides some options to make your life a little easier.
finder_parameter
Maybe you don't query all the time using an :id parameter. In these cases, you can define what you want decent_exposure to use via the :finder_parameter option:
expose(:issue, finder_parameter: :slug)
model
If the name of your exposed method does not match to an existing model, use the :model option:
expose(:issue, model: :comic_issue)
params
Sometimes if you are using a custom form, you will pass different parameters to your controller. To handle this situation, use the :params option:
expose(:comic, params: :comic_params)
Playing friendly with Strong Parameters
By default, decent_exposure does not support the Strong Parameters gem. With strong parameters being the standard for sanitizing your parameters moving forward in Rails 4, it is recommended you use the decent_exposure strategy available on the project's wiki
To enable support, create a file strong_parameters_strategy.rb in your lib directory with the following code:
# https://github.com/voxdolo/decent_exposure/wiki/Strategies:-Use-with-strong_parameters
class StrongParametersStrategy < DecentExposure::ActiveRecordWithEagerAttributesStrategy
delegate :delete?, :to => :request
def attributes
(get? || delete?) ? super : controller.send(:"#{name.singularize}_params")
end
end
Next, create an initializer to ensure the strategy will be available to decent_exposure:
# config/initializers/decent_exposure.rb
require 'strong_parameters_strategy'
Finally, in any exposed methods where you want to utilize strong parameters, set StrongParametersStrategy to the :strategy option:
expose(:comic, strategy: StrongParametersStrategy)
When saving/updating a resource, decent_exposure will now look for the associated resource_name_params method within your controller. In the above example, we would define a private controller method named comic_params:
def comic_params
params.require(:comic).permit(:title, :volume)
end
Install
To use decent_exposure in your Rails project, add it to your Gemfile and run the bundle command:
gem 'decent_exposure'
This post is by Kevin Faustino. Kevin is the Chief Craftsman of Remarkable Labs and also the founder of the Toronto Ruby Brigade.

Comments
comments powered by Disqus