0

Một vài cách để DRY code trong Rails

Thuật ngữ DRY chắc không còn xa lạ lắm với các lập trinh viên, và có lẽ nó cũng là 1 nguyên tắc phát triển phần mềm mà các coder phải nên biết vì các lợi ích của nó mạng lại là không hề nhỏ.

DRY là gì ?

DRY thực chất là viết tắt của câu :

Don’t Repeat Yourself

Hiểu nôm na là chúng ta không nên lặp lại code
Lợi ích DRY mang lại :

  • Dễ bảo trì, mở rộng code : Khi chúng ta maintain code có lẽ ai cũng muốn chỉ cần tìm 1 ví trí nút thắt duy nhất để có thể giải quyết toàn bộ 1 issue thay vì chúng ta phải ctrl + shift + F đi mò từng file để sửa hoặc viết những đoạn code giống nhau không ạ 😅
  • Hạn chế bị bug: Nếu chúng ta copy 1 đoạn code từ chỗ này qua chỗ kia và đến 1 ngày chúng ta phải sửa hết đống code tha đi khắp nơi ấy mà chẳng may lại bị sót mất chỗ nào thì sao 😱?

Ví dụ :

Chúng ta đi vào 1 ví dụ quen thuộc trong rails:

class MicropostsController < ApplicationController
    def show
        @micropost = Micropost.find_by id: params[:id]
        ...
    end
  
    def edit
        @micropost = Micropost.find_by id: params[:id]
        ...
    end
  
    def update
        @micropost = Micropost.find_by id: params[:id]
        ...
    end

Chúng ta có thể thấy @micropost = Micropost.find_by id: params[:id] được lặp đi lặp lại ở các methods cảm giác "not cool at all" đúng không ạ.
Để giải quyết thì chúng ta đơn giản là dùng before_action :

class MicropostsController < ApplicationController
    before_action :load_micropost, only: [:show, :edit, :update]
    def show
        ...
    end
  
    def edit
        ...
    end
  
    def update
        ...
    end
    
    private
    def load_micropost
        @micropost = Micropost.find_by id: params[:id]
    end

Đó tuy năng suất và hiệu suất vào như thế nhưng giờ nhìn code thoáng hơn rồi. Note: before_action thực ra là 1 method được định nghĩa ở AbstractController::Callbacks::ClassMethods

Module Concern

Gom nhóm các phương thức có thể dùng chung trong nhiều class hoặc module vào chung 1 module và include vào các class hoặc module cần dùng.
Từ phiên bản Rails 4 chúng ta được cung cấp 2 folder:

  • app/controllers/concerns/
  • app/models/concerns/

Là nơi định nghĩa các phương thức được dùng chung trong nhiều controller hoặc nhiều model (Cái này thuộc về cách tổ chức file thôi chứ không nhất thiết tất cả các phương thức dùng chung phải bắt buộc định nghĩa ở đây ví dụ chúng ta hoàn toàn có thể include UsersHelper trong MicropostsController nhưng nghe vẻ hơi "dây mơ rễ má")
Mẫu module concern:

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end
  
  def example
  end
  ...
end
  • included : là 1 callback được gọi ngay khi module concern này được include vào class hoặc module khác
  • class_methods: nơi define các class method được include vào class/module khác
  • example: là các instance method được include vào class/module khác

Ví dụ:

app/models/concerns/common_methods.rb :

 module CommonMethods
  extend ActiveSupport::Concern

  included do
    scope :create_asc, -> { order created_at: :asc }
  end

  class_methods do
    def say_hi
      p "hello world"
    end
  end

  def a_instance_method
    p "i'm .... instance method (slap finger)"
  end

end

app/models/user.rb :

class User < ApplicationRecord
  include CommonMethods
end

app/models/micropost.rb :

class Micropost < ApplicationRecord
  include CommonMethods
end

Kết quả :




Phương thức send()

Đây là 1 trong những phương thức rất hay ho trong metaprogramming. Và mình nghĩ là metaprogramming được sinh ra là để giúp đỡ lặp lại code.
Phương thức send thực hiện 1 lời gọi hàm đền 1 phương thức với tham số truyền vào là 1 string, 1 block hoặc 1 symbol

Ví dụ :

class Person
  def initialize name
     @name = name
  end
  
 # def hello
 #   if @name == "dad"
 #     hello_dad
 #   elsif @name == "mom"
 #     hello_mom
 #   elsif @name == "sister"
 #     hello_sister
 #   else
 #     p "Hello"
 #   end
 # end
  
  def hello
    if !@name
      puts "Hello"
    else
      send "hello_#{@name}"
    end
  end
  
  private
  
    def hello_dad
      p "Hi Dad!"
    end
  
    def hello_mom
      p "Hi Mom!"
    end
  
    def hello_sister
      p "Hi Sister!"
    end
end

Person.new("dad").hello
# => "Hi Dad!"

define_method

Trong trường hợp chúng ta cần định nghĩa nhiều methods có độ tương tự cao ví dụ :

Ví dụ :

class Micropost < ApplicationRecord
  enum status: { pending: 0, approve: 1, cancel: 2}
  class << self
    # def microposts_pending
    #   pending.where(content: "pending")
    # end

    # def microposts_approve
    #   approve.where(content: "approve")
    # end

    # def microposts_cancel
    #   cancel.where(content: "cancel")
    # end

    Micropost.statuses.keys.each do |status|
      define_method "microposts_#{status}" do
        send(status).where(content: status)
      end
    end
  end
end

Tổng kết

Tài liệu tham khảo

https://api.rubyonrails.org/classes/ActiveSupport/Concern.html
https://viblo.asia/p/tim-hieu-ve-metaprogramming-trong-ruby-djeZ1G4Q5Wz


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í