DRYing rails controllers using meta programming

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.