Don’t Repeat Yourself (DRY) is a software development principle, the main aim of which is to reduce repetition of code. There is a misconception among many developers that they define the DRY term wrongly. They believe that reduced code is known as DRY code but hold on. DRY doesn't tell you to reduce the code, it says to reduce the multiple appearance of same code.
Before going to deep into this article I assume that you are aware about the advantages of DRY and importance of following this principle. I am also assuming you have basic knowledge about metaprogramming and the power of metaprogramming.
Now suppose we have an application where we have multiple controllers which are doing same funcationality like simple C-R-U-D (Create-Read-Update-Delete) operations. Then what is the best way to do things faster - Scaffolding right? Yes!
Scaffolding is the good way to create such controllers to develop quickly. For example look at the following example:
rails generate scaffold book name:string description:text image_url:string price:integer isbn:string
When you run this command you will get a controller named as books_controller
. This will look like following:
class BooksController < ApplicationController
before_action :set_book, only: [:show, :edit, :update, :destroy]
# GET /books
# GET /books.json
def index
@books = Book.all
end
# GET /books/1
# GET /books/1.json
def show
end
# GET /books/new
def new
@book = Book.new
end
# GET /books/1/edit
def edit
end
# POST /books
# POST /books.json
def create
@book = Book.new(book_params)
respond_to do |format|
if @book.save
format.html { redirect_to @book, notice: 'Book was successfully created.' }
format.json { render :show, status: :created, location: @book }
else
format.html { render :new }
format.json { render json: @book.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /books/1
# PATCH/PUT /books/1.json
def update
respond_to do |format|
if @book.update(book_params)
format.html { redirect_to @book, notice: 'Book was successfully updated.' }
format.json { render :show, status: :ok, location: @book }
else
format.html { render :edit }
format.json { render json: @book.errors, status: :unprocessable_entity }
end
end
end
# DELETE /books/1
# DELETE /books/1.json
def destroy
@book.destroy
respond_to do |format|
format.html { redirect_to books_url, notice: 'Book was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_book
@book = Book.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def book_params
params.require(:book).permit(:name, :description, :image_url, :price, :isbn)
end
end
Now suppose you need another controllers by which you need to perform only basic C-R-U-D operations then you need to run the same scaffold generator(after changing your model and attributes name) and you get the similar resource controller. Though this is fast to build the functionalities but as a professional Ruby on Rails developer (If you really love the magic of Ruby) this is not a good approach because in all such controllers you are using same code(except your resource name).
Now this is better to write DRY controllers using some metaprogramming techniques. Believe me I am not using any advanced meta programming example here, instead I am just using only few basic methods to keep this as simple as possible.
Enough talking lets use a practical example. We will do this by creating a concern and then including this concern into all our common controllers. Go to app/controllers/concerns folder into your project.
Now create a file and name as common_crud.rb
and put the following code inside this:
module Concerns::CommonCrud
extend ActiveSupport::Concern
included do
before_action :set_resource, only: %w[show edit update destroy]
def index
results = resource_class.all
instance_variable_set("@#{plural_resource_name}", results)
respond_to do |format|
format.html
end
end
def new
set_resource(resource_class.new)
end
def edit; end
def show; end
def create
set_resource(resource_class.new(resource_params))
if get_resource.save
redirect_to send("#{plural_resource_name}_path"), flash: { success: "#{resource_name.titleize} has been created successfully." }
else
render :new
end
end
def update
if get_resource.update(resource_params)
redirect_to send("#{plural_resource_name}_path"), flash: { success: "#{resource_name.titleize} has been updated successfully." }
else
render :edit
end
end
def destroy
if get_resource && get_resource.destroy
redirect_to send("#{plural_resource_name}_path"), flash: { success: "#{resource_name.titleize} has been deleted successfully." }
else
redirect_to send("#{plural_resource_name}_path"), flash: { error: "No #{resource_name} found!" }
end
end
private
def resource_class
@resource_class ||= resource_name.classify.constantize
end
def resource_name
@resource_name ||= self.controller_name.singularize
end
def plural_resource_name
resource_name.pluralize
end
def resource_params
self.send("#{resource_name}_params")
end
def set_resource(resource=nil)
resource ||= resource_class.find(params[:id])
instance_variable_set("@#{resource_name}", resource)
end
def get_resource
instance_variable_get("@#{resource_name}")
end
end
end
Now its time to include this module into your books controller and after doing this the app/controllers/books_controller.rb
should look like like this:
class BooksController < ApplicationController
include Concerns::CommonCrud
private
def book_params
params.require(:book).permit!
end
end
Here we go! But hold on! These steps will only work fine for all rails versoions prior to rails 6. We have tried this article with rails 4.x, 5.x and works fine. But if you use this blog as it is for rails 6 too, you will get some name error, like this:
NameError (uninitialized constant BooksController::Concerns)
This is because in rails 6 we can not use concerns as namespace. So for rails 6 you need to remove namespace from our common_crud.rb
concern. So just need to do following things:
module Concerns::CommonCrud
# with
module CommonCrud
# and inside books_controller.rb replace
include Concerns::CommonCrud
# with
include CommonCrud
And you are good to go. Cheers!!!
You can do the same for all your controllers and whenever you need to customize an action you can simply override that action. You can observe the difference between both ways: default scaffold controllers and implementing controllers using DRY standards.
I hope you like this article.