In Rails, the default way of passing objects into views is to set instance variables in controllers. This is often confusing and breaks encapsulation (instance variables are meant to be private for the object). In addition, many such variables are set in the common ways, increasing boilerplate code (e.g. find record by id, find collection scoped to current user).
Decent_exposure is a small utility gem to improve exposing objects in controllers to views.
Removes the need for instance variables in controllers, improving encapsulation
Makes the variables used in views clearly visible at the top of the controller
Compatible with most common use-cases, e.g. strong_parameters, decorators
Exposes code smells, like controllers with multiple responsibilities (pun not intended)
Lazy-loading of exposed variables may cause reasoning about complex controller flow more difficult (e.g. when exactly is the model fetched from the database?)
Installing is as simple as adding gem ‘decent_exposure’ to the Gemfile and running bundle install
In its basic form, decent_exposure provides an expose macro to be used in rails controllers. Based on the name and request params, it will automatically fetch existing or instantiate new models. This is how a controller using decent_exposure looks like:
[code]
decent_exposure took care of:
Instantiating new post object in new and edit actions (they are not needed to be defined explicitly anymore)
Assigning attributes from params in create and update actions (we just have to call save)
Finding correct post by id (Post.find(params[:id])) for show action and listing posts for index action (again, no need to define these actions explicitly)
Adding “post” and “posts” methods available both in this controller and views
The variables defined by expose are memoized - meaning you can reference ‘post’ variable many times but it will be only assigned once.
How to refactor existing controller with decent_exposure:
Make sure there are tests in place to cover the controller’s functionality
Add expose macros at the top of the controller definition
Remove before_actions setting instance variables
Replace all references to instance variables by methods (without @) in controller and views
Modify controller tests using assigns:
assigns(:post) → controller.post
To scope finders to particular context, e.g. get current_user.posts.find() rather than Post.find(), use ancestor option:
expose(:post, ancestor: :current_user)
To expose objects with nonstandard fetching methods, or scoped collections, use the block passed to expose:
expose(:post) { Post.find_by_slug(params[:id]) }
expose(:posts) { Post.recent }
To expose paginated collections, use block with default argument:
expose(:posts) { |default| default.page(params[:page]).per(params[:per]) }
If you’re on Rails 4, then you’re probably using strong_parameters. It was added as a default way to prevent accidentally updating sensitive model attributes (i.e. admin field). It requires all the params to be whitelisted - read more about it here.
Decent exposure has built-in support for passing strong parameters: see example in the documentation.
Sometimes expose block contains a lot of code, like in this example:
[code]
In this situation, it is nice to extract the code to private method, keeping the header of the controller slim, like this:
[code]
The ideal number of exposures is 2 per controller (one for singular resource, e.g. post, one for plural collection e.g. posts). If the top of the controller class is crowded with expose statements, two code smells might occur:
Exposing variables not used in view
When the variable is not used in view, only in controller, extract private method instead:
[code]
This way details of the controller implementation don’t leak on the outside, and it is clearer which variables are used by views.
Controller with multiple responsibilities
Many view exposures in single controller is often an indication that the controller in question is too big or handles too many actions. Look for the opportunities to extract parts of functionality to separate controllers.
Decorators are a great pattern for managing presentational code of your models. For tutorial on implementing decorator classes, see draper gem.
You can utilize expose with block feature to decorate the object:
expose(:post) { |default| PostDecorator.new(default) }
Or, take a look at decent_decoration gem which streamlines the process.