Exploiting abstraction while writing rails controllers
Abstraction is simple and beautiful while writing abstract code is an art.
While writing web apps apart from writing business logic most of the time we end up writing CRUD operations (Create, Read, Update Delete). There are a bunch of methods available to generate boilerplate code i.e. scaffolding, custom generators, etc. which have their own dos and don’ts.
I personally have been exploiting abstraction to write CRUD operations. Consider an example of developing a school management system where we have controllers like Student, Course, Section, Teacher, Exam, and many others. A typical student controller CRUD would be
class StudentsController < ApplicationController
before_action :set_student, only: %i[ show edit update destroy ]
def index
@students = Student.all
end
def show
end
def new
@student = Student.new
end
def edit
end
def create
@student = Student.new(student_params)
respond_to do |format|
if @student.save
format.html { redirect_to @student, notice: "Student was successfully created." }
format.json { render :show, status: :created, location: @student }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @student.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @student.update(student_params)
format.html { redirect_to @student, notice: "Student was successfully updated." }
format.json { render :show, status: :ok, location: @student }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @student.errors, status: :unprocessable_entity }
end
end
end
def destroy
@student.destroy
respond_to do |format|
format.html { redirect_to students_url, notice: "Student was successfully destroyed." }
format.json { head :no_content }
end
end
private
def set_student
@student = Student.find(params[:id])
end
def student_params
params.require(:student).permit(:first_name, :last_name, :roll_no, :date_of_birth)
end
end
Sections controller CRUD would look like
class SectionsController < ApplicationController
before_action :set_section, only: %i[ show edit update destroy ]
def index
@sections = Section.all
end
def show
end
def new
@section = Section.new
end
def edit
end
def create
@section = Section.new(section_params)
respond_to do |format|
if @section.save
format.html { redirect_to @section, notice: "Section was successfully created." }
format.json { render :show, status: :created, location: @section }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @section.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @section.update(section_params)
format.html { redirect_to @section, notice: "Section was successfully updated." }
format.json { render :show, status: :ok, location: @section }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @section.errors, status: :unprocessable_entity }
end
end
end
def destroy
@section.destroy
respond_to do |format|
format.html { redirect_to sections_url, notice: "Section was successfully destroyed." }
format.json { head :no_content }
end
end
private
def set_section
@section = Section.find(params[:id])
end
def section_params
params.require(:section).permit(:name)
end
end
And Course CRUD would be
class CoursesController < ApplicationController
before_action :set_course, only: %i[ show edit update destroy ]
def index
@courses = Course.all
end
def show
end
def new
@course = Course.new
end
def edit
end
def create
@course = Course.new(course_params)
respond_to do |format|
if @course.save
format.html { redirect_to @course, notice: "Course was successfully created." }
format.json { render :show, status: :created, location: @course }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @course.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @course.update(course_params)
format.html { redirect_to @course, notice: "Course was successfully updated." }
format.json { render :show, status: :ok, location: @course }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @course.errors, status: :unprocessable_entity }
end
end
end
def destroy
@course.destroy
respond_to do |format|
format.html { redirect_to courses_url, notice: "Course was successfully destroyed." }
format.json { head :no_content }
end
end
private
def set_course
@course = Course.find(params[:id])
end
def course_params
params.require(:course).permit(:title, :credit_hours, :code, :year)
end
end
If you notice most of the time we are doing same kind of operation i.e. in the case of the show we first set the resource which could be Student, Course, Section, etc. In general, we are dealing with a resource or resources everywhere in the controllers.
In the Ruby on Rails world, we often talk about resources, we model every real-world object into Rails model as we did with the school management system Student, Course, Teacher, Exam. If we look at our above code we just specified resource name in reach controller for example in StudentsController we used Student, @student, @students, set_student, or student_params and the same is the case with CoursesController we have used Course, @course, @courses, set_course, or course_params, etc. In short, we are repeating the resource name in each file. We can come up with a generic controller let say BaseController where we perform all operations on a resource rather than student, course, section, etc. Then, we just inherit StudentsController, CoursesController, TeachersController, ExamsController from a single source of truth BaseController.
Let’s start with the show function and try to write BaseController.
class BaseController < ApplicationController
before_action :set_resource, only: %i[ show ]
def show
end
def set_resource
resource ||= resource_class.find(params[:id])
instance_variable_set("@#{resource_name}", resource)
end
def resource_class
@resource_class ||= resource_name.classify.constantize
end
def resource_name
@resource_name ||= controller_name.singularize
end
end
Writing create function is a bit more interesting
def create
@resource = resource_class.new(resource_params)
respond_to do |format|
if @resource.save
format.html { redirect_to @resource, notice: "#{resource_name} was successfully created." }
format.json { render :show, status: :created, location: @resource }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @resource.errors, status: :unprocessable_entity }
end
end
end
update and destroy functions are somewhat similar
def update
@resource = resource_class.find(params[:id])
respond_to do |format|
if @resource.update(resource_params)
format.html { redirect_to @resource, notice: "#{resource_name} was successfully updated." }
format.json { render :show, status: :ok, location: @resource }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @resource.errors, status: :unprocessable_entity }
end
end
end
def destroy
@resource = resource_class.find(params[:id])
@resource.destroy
respond_to do |format|
format.html { redirect_to "#{controller_name}_url", notice: "#{resource_name} was successfully destroyed." }
format.json { head :no_content }
end
end
In all of the above rewrites, we just simply used resources as a generic variable attribute. destroying an object is the same no matter its Student, Course, Section, or Exam so resource.destroy works the same for each resource.
Our final refactoring will look like this
Since controllers are inherited from BaseController we can always override any method in the respective controller. For example, instead of directly creating a Student object we have a service written which takes student params and creates Student object after applying a bunch of business logic and pre-checks. For such scenarios, we can override create the function in StudentsController.
We can use the same pattern while writing our APIs and all other scenarios where we have a similar pattern repetition.
Thank You so much for the read.