How to decrease coupling in your controllers & views with decent_exposure for better maintainability

Posted on

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 index action will be queried by default using Comic.scoped.
  • If no :id parameter is available in the controller params hash, decent_exposure returns Comic.new(params[:comic]).
  • If an :id parameter is present, decent_exposure will query for the resource by executing Comic.find(params[:id]. If a plural exposed resource is available, decent_exposure will scope the singular version to it. Thus, comic is set to Comic.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'
002

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