+1

Khắc phục DRY code trong Rails

Khi bạn làm việc với Rails có rất nhiều đoạn code xử lý giống nhau trong các controllers hay models khiến việc DRY code khá nhiều và đọc thấy rất khó chịu. Bạn có bao giờ để ý đến thư mục concerns không. Folder này nằm trong app/controllers và app/models nó sẽ là cứu cánh để xử lý DRY code Trong bài viết này mình sẽ khắc phục bằng ActiveSupport::Concern (concerns)

Build một app demo

Trong app này mình sẽ sử dụng bootstrap và devise. Thực hiện các bước sau nhé. rails new demo thêm hai gem và chạy câu lệnh bundle

gem "devise"
gem "bootstrap-sass"

Tiếp theo cài đặt Devise rails g devise:install giờ hãy create một model Admin với devise rails g devise Admin

Giờ tạo một partion app/views/layouts/_navigation.html.erb với nội dung

#app/views/layouts/_navigation.html.erb

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <% if admin_signed_in? %>
        <%= link_to "Twik", admin_products_path, class: "navbar-brand" %>
      <% else %>
        <%= link_to "Twik", root_path, class: "navbar-brand" %>
      <% end %>
    </div>
    <div class="collapse navbar-collapse" id="navbar-collapse">
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to 'Home', root_path %></li>
        <% if admin_signed_in? %>
          <li><%= link_to "My Account", edit_admin_registration_path  %></li>
          <li><%= link_to "Logout", destroy_admin_session_path,  method: :delete %></li>
        <% else %>
          <li><%= link_to "Login", new_admin_session_path  %></li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

Trong file application.html.erb bạn phải render menu chúng ta vừa tạo bằng cách thêm đoạn code này vào <%= render "layouts/navigation" %>

products Controllers and Concerns

Chúng ta muôn viết ít code nhất có thể và chắc chắn một điều là chúng không bị lăp lại. Concern sẽ gom những đoạn code thực thi chức năng giống nhau trong mỗi controllers lại. Oke giờ đi vào cụ thể.

  • tạo một product modle: rails g model product description:text
  • tiếp theo là controller cho nó rails g controller productsController.
  • create một file twiable.rb trong app/controllers/concerns với nọi dung
#app/controllers/concerns/productable.rb

 module productable
   extend ActiveSupport::Concern

   included do
     before_action :set_product, only: [:show, :edit, :destroy, :update]
   end

   def index
     @products = product.all
   end

   def new
     @product = product.new
   end

   def show
   end

   def create
     @product = product.new product_params
     if @product.save
       flash[:notice] = "Successfully created product."
       redirect_to @product
     else
       flash[:alert] = "Error creating product."
       render :new
     end
   end

   private

   def product_params
     params.require(:product).permit :description
   end

   def set_product
     @product = product.find params[:id]
   end
 end

Đoạn code trên extend ActiveSupport::Concern có nghĩa là chúng ta đang tạo một concern. Trong include block sẽ được thực thi mỗi khi module này được include vào trong mỗi controller cần dung. Đây nó như là các function bên thứ 3. Giờ mình sẽ tiến hành include Twiable vào trong productsController.

class productsController < ApplicationController
  include productable
end

Chúng ta cũng cần mộ namespace cho admin cho product. Để chỉ có admin mới có quyền them sửa xóa product rails g controller admin/product.

class Admin::productsController < ApplicationController
  include productable

  def edit
  end

  def update
    if @product.update_attributes product_params
      flash[:notice] = "Successfully updated product."
      redirect_to admin_product_path
    else
      flash[:alert] = "Error creating product."
      render :edit
    end
  end

  def destroy
    if @product.destroy
      flash[:notice] = "Successfully deleted product."
      redirect_to products_path
    else
      flash[:alert] = "Error deleting product."
    end
  end
end

đấy chúng ta đã không con lặp code nữa rồi. giờ hãy tạo view để thấy nó làm việc

mkdir -p app/views/admin/products
touch app/views/admin/products/index.html.erb
touch app/views/admin/products/new.html.erb
touch app/views/admin/products/show.html.erb
touch app/views/admin/products/edit.html.erb
touch app/views/products/new.html.erb
touch app/views/products/show.html.erb
touch app/views/products/index.html.erb

paste lần lượt các đoạn dưới đây vào: Admin product Edit Page

#app/views/admin/products/edit.html.erb
<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @product, :url => {:controller => "products", :action => "update" } do |f| %>
        <div class="form-group">
          <%= f.label :description %>
          <%= f.text_field :description, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Update", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin product Index Page

#app/views/admin/products/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing products</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New description", new_admin_product_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @products.each do |product| %>
              <tr>
                <td class="col-sm-8 col-xs-8"><%= product.description %></td>
                <td class="col-sm-4 col-xs-4"><%= link_to 'Show', admin_product_path(product), class: "btn btn-primary" %>
                    <%= link_to 'Edit', edit_admin_product_path(product), class: "btn btn-default" %>
                    <%= link_to "Delete", admin_product_path(product), class: "btn btn-danger", data: {:confirm => "Are you sure?"}, method: :delete %>
                </td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

Admin New product Page

#app/views/admin/products/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @product do |f| %>
        <div class="form-group">
          <%= f.label :description %>
          <%= f.text_field :description, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin product Show Page

#app/views/admin/products/show.html.erb

<div>
  <h2><%= @product.description %></h2>
</div>

product Index Page

#app/views/products/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing products</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New description", new_product_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @products.each do |product| %>
              <tr>
                <td><%= product.description %></td>
                <td><%= link_to 'Show', product, class: "btn btn-primary" %></td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

New product Page

#app/views/products/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @product do |f| %>
        <div class="form-group">
          <%= f.label :description %>
          <%= f.text_field :description, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

product Show Page

#app/views/products/show.html.erb

<div>
  <h2><%= @product.description %></h2>
</div>

Giờ bạn chạy rails s lên và thấy mọi thứ làm việc thật sự tốt. Code trong controller điều hướng của bạn thật sự gọn gang nhờ concern.

Concerns in Models

Phần trước ta đã thấy thực hiện viết code trong controller. Giờ là đến Models nó cũng tương tự vậy thôi. Nếu bạn thực sự hiểu cách implement trong controller thì bạn sẽ biết cách thực hiện nó trong model vậy. Hãy nhóm những đoạn thực thi các function dung chung và viết chúng vào block include. Cuối cũng nhưng model nào sử dụng thì include modul concern này vào 😄

Kết luận

Nhờ ActiveSupport::Concern mà bạn có thể refactor lại code của mình tránh được DRY code, trông có vẻ sang sủa và tối ưu dễ đọc. Hy vọng sẽ giúp ích được bạn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí