decent_exposure
helps you program to an interface, rather than an implementation in
your Rails controllers.
Sharing state via instance variables in controllers promotes close coupling with
views. decent_exposure
gives you a declarative manner of exposing an interface to the
state that controllers contain, thereby decreasing coupling and improving your
testability and overall design. I elaborate on this approach in A Diatribe on
Maintaining State.
Installation
gem install decent_exposure
Configure your Rails 2.X application to use it:
In config/environment.rb
:
config.gem 'decent_exposure'
When used in Rails 3:
In Gemfile
:
gem 'decent_exposure'
Examples
Railscast
Ryan Bates has a Railscasts episode covering decent_exposure
. If you're just
getting started or just enjoy screencasts (Ryan's are always great), you can
check it out here: Railscasts - Decent Exposure.
A full example
The wiki has a full example of converting a classic-style Rails controller.
In your controllers
When no block is given, expose
attempts to determine which resource you want
to acquire. When params
contains :category_id
or :id
, a call to:
expose(:category)
Would result in the following ActiveRecord#find
:
Category.find(params[:category_id]||params[:id])
As the example shows, the symbol passed is used to guess the class name of the
object (and potentially the params
key to find it with) you want an instance
of.
Should params
not contain an identifiable id
, a call to:
expose(:category)
Will instead attempt to build a new instance of the object like so:
Category.new(params[:category])
If you define a collection with a pluralized name of the singular resource,
decent_exposure
will attempt to use it to scope its calls from. Let's take the
following scenario:
class ProductsController < ApplicationController
expose(:category)
expose(:products) { category.products }
expose(:product)
end
The product
resource would scope from the products
collection via a
fully-expanded query equivalent to this:
Category.find(params[:category_id]).products.find(params[:id])
or (depending on the contents of the params
hash) this:
Category.find(params[:category_id]).products.new(params[:product])
In the straightforward case, the three exposed resources above provide for access to both the primary and ancestor resources in a way usable across all 7 actions in a typicall Rails-style RESTful controller.
A Note on Style
When the code has become complex enough to surpass a single line (and is not
appropriate to extract into a model method), use the do...end
style of block:
expose(:associated_products) do
product.associated.tap do |associated_products|
present(associated_products, :with => AssociatedProductPresenter)
end
end
In your views
Use the product of those assignments like you would an instance variable or any other method you might normally have access to:
= render bread_crumbs_for(category)
%h3#product_title= product.title
= render product
%h3 Associated Products
%ul
- associated_products.each do |associated_product|
%li= link_to(associated_product.title,product_path(associated_product))
Custom defaults
decent_exposure
provides opinionated default logic when expose
is invoked without
a block. It's possible, however, to override this with custom default logic by
passing a block accepting a single argument to the default_exposure
method
inside of a controller. The argument will be the string or symbol passed in to
the expose
call.
class MyController < ApplicationController
default_exposure do |name|
ObjectCache.load(name.to_s)
end
end
The given block will be invoked in the context of a controller instance. It is possible to provide a custom default for a descendant class without disturbing its ancestor classes in an inheritance heirachy.
Beware
This is a simple tool, which provides a solitary solution. It must be used in conjunction with solid design approaches ("Program to an interface, not an implementation.") and accepted best practices (e.g. Fat Model, Skinny Controller). In itself, it won't heal a bad design. It is meant only to be a tool to use in improving the overall design of a Ruby on Rails system and moreover to provide a standard implementation for an emerging best practice.
Development
Running specs
decent_exposure
has been developed with the philosophy that Ruby developers shouldn't
force their choice in RubyGems package managers on people consuming their code.
As a side effect of that, if you attempt to run the specs on this application,
you might get no such file to load
errors. The short answer is that you can
export RUBYOPT='rubygems'
and be on about your way (for the long answer, see
Ryan Tomayko's excellent treatise on the subject).
Contributors
Thanks to everyone that's helped out with decent_exposure
! You can see a full
list here: