Simplify Your Rails Controllers With a Vengeance

Preview:

DESCRIPTION

A step-by-step process for simplifying controller code in Ruby on Rails applications, with code examples. First presented at Philly.rb September 2014.

Citation preview

SimplifyYour Rails Controllers

VengeanceWith a

Brian Auton

brianauton@gmail.comtwitter.com/brianautongithub.com/brianauton

I know what's wrongwith your Rails app...

The controllers(they're too complex)

1. Less responsibilities

2. Shorter methods

3. Less duplication

1. Less responsibilities

2. Shorter methods

3. Less duplication

Controllers are just code.

1. Less responsibilities

User's Intent <=> Business Logic

1. Less responsibilities

User's Intent <=> Business Logic

How? REST

routes.rbresources :clients do get :download_pdf, on: :memberend

clients_controller.rbdef download_pdf client = Client.find params[:id] send_data client.to_pdf, type: :pdfend

def show @client = Client.find params[:id]end

routes.rbresources :clients

clients_controller.rbdef show @client = Client.find params[:id] respond_to do |format| format.html {} format.pdf do send_data @client.to_pdf, type: :pdf end endend

routes.rbresources :orders do post :submit, on: :memberend

orders_controller.rbdef submit order = Order.find params[:id] PaymentGateway.process order flash[:notice] = “Payment successful” order.update_attribute :status, :completerescue PaymentGateway::Error => e order.update_attribute :status, :failed redirect_to order, alert: “Error: #{e}”end

routes.rbresources :ordersresources :payment_attempts, only: :create

payment_attempts_controller.rbdef create attempt = PaymentAttempt.create payment_attempt_params PaymentGateway.process attempt.order flash[:notice] = “Payment successful” attempt.update_attribute :status, :completerescue PaymentGateway::Error => e attempt.update_attribute :error, e.message redirect_to attempt.order, alert: “Error: #{e.message}”end

routes.rbresources :regions do put :sort, on: :collectionend

regions_controller.rbdef sort params[:region].each do |id, position| Region.find(id).update_attribute :position, position endend

routes.rbresources :regionsresources :region_collections, only: :update

region_collections_controller.rbdef update region_collection_params.each do |attributes| Region.find(attributes[:id)].update_attributes attributes endend

private

def region_collection_params params.require(:region_collection).permit [:id, :position]end

2. Shorter methods

2. Shorter methods

How? Delegate to models

payment_attempts_controller.rbdef create attempt = PaymentAttempt.create payment_attempt_params PaymentGateway.process attempt.order flash[:notice] = “Payment successful” attempt.update_attribute :status, :completerescue PaymentGateway::Error => e attempt.update_attribute :error, e.message redirect_to attempt.order, alert: “Error: #{e.message}”end

payment_attempt.rbclass PaymentAttempt < ActiveRecord::Base before_save do PaymentGateway.process order rescue PaymentGateway::Error => e update_attribute :error, e.message end

def successful? error.present? endend

payment_attempts_controller.rbdef create @attempt = PaymentAttempt.create payment_attempt_params if @attempt.successful? flash[:notice] = “Payment successful.” else flash[:alert] = “Error: #{@attempt.error}” redirect_to @attempt.order endend

users_controller.rbdef update @user = User.find params[:id] @user.update_attributes user_params @user.address.update_attributes address_params ...end

def user_params params.require(:user).permit :name, :emailend

def address_params params.require(:address).permit :city, :state, :zipend

users_controller.rbdef update @user = User.find params[:id] @user.update_attributes user_params ...end

def user_params params.require(:user).permit :name, :email, { address_attributes: [:city, :state, :zip] }end

user.rbhas_one :addressaccepts_nested_attributes_for :address

3. Reduce Duplication

3. Reduce Duplication

How? It's just code.

widgets_controller.rbdef new @widget = Widget.newend

def show @widget = Widget.find params[:id]end

def update @widget = Widget.find params[:id] ...end

widgets_controller.rbbefore_action :build_widget, only: [:new]before_action :find_widget, only: [:show, :update]

private

def build_widget @widget = Widget.newend

def find_widget @widget = Widget.find params[:id]end

application_controller.rbprotected

def build_member set_member_instance_variable collection.newend

def find_member set_member_instance_variable collection.find(params[:id])end

def set_member_instance_variable(value) variable_name = “@#{controller_name.singularize}” instance_variable_set variable_name, (@member = value)end

def collection controller_name.classify.constantizeend

assets_controller.rbdef update if @asset.update_attributes asset_params redirect_to @asset, notice: “Asset updated” else flash[:alert] = @asset.errors.full_messages.join(', ') render :edit endend

surveys_controller.rbdef update if @survey.update_attributes survey_params redirect_to @survey, notice: “Survey updated” else flash[:alert] = @survey.errors.full_messages.join(', ') render :edit endend

assets_controller.rbdef update @asset.update_attributes asset_params respond_to_update @assetend

surveys_controller.rbdef update @survey.update_attributes survey_params respond_to_update @surveyend

application_controller.rbdef respond_to_update(model) if model.valid? type = model.class.name.humanize redirect_to model, notice: “#{type} updated” else flash[:alert] = model.errors.full_messages.join(', ') render :edit endend

shared_rest_actions.rbmodule SharedRestActions def self.included(base) base.before_action :build_member, only: [:new, :create] base.before_action :find_collection, only: :index base.before_action :find_member, except: [:index, :new, :create] end

def index end

def update member_params = send “#{controller_name.singularize}_params” @member.update_attribute member_params respond_to_update @member end

...end

assets_controller.rbclass AssetsController < ApplicationController include SharedRestActions before_action :paginate, only: :index before_action :sort_by_date, only: :indexend

surveys_controller.rbclass SurveysController < ApplicationController include SharedRestActions before_action :build_member, only: :indexend

1. Less responsibilities (REST)

2. Shorter methods (Models)

3. Less duplication

Recap:

Go try it out!

brianauton@gmail.comtwitter.com/brianautongithub.com/brianauton