10/11/2018, 00:02

Alias method và Custom callback trong Rails

Ruby on Rails hỗ trợ rất nhiều callbacks xung quanh lifecycle của một object hay một action nào đó. Tuy nhiên, những callbacks này là không đủ để có thể giải quyết những vấn đề đặc biệt. Thay vào đó, với ActiveSupport, Rails đã cung cấp cho chúng ta đầy đủ công cụ để có thể tự tạo ra một callback ...

Ruby on Rails hỗ trợ rất nhiều callbacks xung quanh lifecycle của một object hay một action nào đó. Tuy nhiên, những callbacks này là không đủ để có thể giải quyết những vấn đề đặc biệt. Thay vào đó, với ActiveSupport, Rails đã cung cấp cho chúng ta đầy đủ công cụ để có thể tự tạo ra một callback cho riêng mình. Các bước để tạo ra một callback trong Rails bao gồm:

  • Define target: Sử dụng kết hợp hai method define_callbacks và run_callbacks để xác định đối tượng và phạm vi chạy callback.
  • Callback methods: Giống nhưng những method thông thường khác.
  • Set callback: Sử dụng method set_callback để xác định thời điểm sẽ chạy các callback methods đã được khai báo ở trên.

Trước khi đi vào vấn đề chính chúng ta sẽ đi tìm hiểu về một method rất thú vị mà Ruby cung cấp. Nếu nắm vững và sử dụng hợp lý nó sẽ làm cho code trở nên linh hoạt hơn đặc biệt là trong việc override và sử dụng callback.

Chúng ta có một class Dog kế thừa từ class Animal như sau:

class Animal
  def run
    puts "Animal running..."
  end
end

class Dog < Animal
  def run
    # will be override here
  end
end

Vì một lý do nào đó chúng ta phải override lại method run của Animal tuy nhiên vẫn muốn giữ lại nó ở đâu đó ở trong Dog. Giải pháp là sử dụng alias_method:

class Dog < Animal
  def run
    puts "Dog running..."
  end
  
  alias_method :run_as_animal, :run
end

Chúng ta hãy cùng xem điều gì đã xảy ra:

Một object trong ruby hoàn toàn không chứa method. Tất cả những method mà object sử dụng đều được lưu ở table_methods nằm trong class object tạo ra nó hoặc bên trong SingletonClass của nó. Một method name bản chất là một symbol trỏ tới một block code.

Method alias_method(:new_name, :old_name) sẽ tạo ra một bản copy của block mà :old_name đang trỏ tới. Tiếp đó :new_name sẽ được trở tới block mới này.

Cụ thể trong trường hợp trên :run_as_animal sẽ trỏ tới một bản copy của :run (kế thừa từ Animal). Method run trong Dog đã được override và tại thời điểm này :run sẽ trỏ đến bản copy kia.

Ở một diễn biến tương tự khác, chúng ta có UserController như sau:

class UserController < ApplicationController
  alias_method :render_without_feature, :render
  alias_method :render, :render_with_feature
end

Ngắn gọn hơn nữa, ActiveSupport cung cấp một method có tên là alias_method_chain. Toàn bộ quá trình chúng ta vừa thấy ở trên sẽ diễn ra chỉ sau một dòng code:

alias_method_chain :render, :feature

Chúng ta chỉ việc define method render_with_feature và symbol :render sẽ tự động trỏ tới nó. Tuy nhiên, từ các phiên bản Rails 5 (sử dụng Ruby 2.0), method này không còn được sử dụng nữa.

Trước phiên bản Ruby 2.0, mỗi một module khi được include vào một class, code của nó chỉ có thể được đưa vào phía trên code của class đó. Nghĩa là các method trong module có thể bị override bởi class hiện tại. Từ phiên bản Ruby 2.0, với prepend, chúng ta có thể sử dụng module để override các method của chính class include nó. Những vấn đề này chúng ta sẽ tìm hiểu kỹ hơn trong một bài viết khác.

Sử dụng callback giống như một con dao hai lưỡi, nó có thể khiến cho code của bạn phức tạp và khó debug hơn tuy nhiên khi sử dụng hợp lý thì nó lại giúp cho code DRY và ngắn gọn hơn rất nhiều. Có nhiều trường hợp trong controller cần sử dụng đến một callback. Ví dụ như khi bạn sử dụng ajax để create hay delete một record trên trang index. Điều cần làm sau đó là phải xử lý phân trang lại vì số lượng record hiển thị trên một trang cần phải cố định. Hay việc phải xác định layout trước khi render, để đơn giản chúng ta sẽ sử dụng luôn ví dụ này làm minh họa cho việc custom callback trong controller. Chúng ta có UsersController như sau:

class UsersController < ApplicationController
  def new
    ...
    set_layout
  end
  
  def create
    ...
    set_layout
  end
  
  private
  def set_layout
    ...
  end
end

Đó là khi không sử dụng callback. Cũng không vấn đề gì cả nếu như việc set layout động chỉ xảy ra ở UsersController. Trong trường hợp project có nhiều controller khác phải thực hiện việc này thì một callback có thể sẽ là lựa chọn tốt hơn cả. Vậy thời điểm chạy callback này là khi nào? Đó là trước khi controller thực hiện render view. Chúng ta sử dụng một module để giải quyết vấn đề này.

# app/controllers/concerns/render_callbacks.rb

module RenderCallbacks
  include ActiveSupport::Callbacks

  included do
    define_callbacks :render
  end

  def render *options, &block
    run_callbacks :render do
      super
    end
  end
  
  ...
end

Như đã nói ở phần đầu

  • Chúng ta đã sử dụng define_callbacks để định nghĩa target sẽ đăng ký callback, ở đây là :render .
  • Tiếp theo là định nghĩa method :render và chỉ rõ phạm vi sẽ được chạy callback bằng việc wrap chúng vào trong block run_callbacks.
  • Việc set callback sẽ được thực hiện thông qua một class method có tên là before_render.

Có một vấn đề là một class khi include một module thì không thể kế thừa những class method đã được định nghĩa trong module đó. Nhưng rất may mắn là ActiveSupport::Concern đã có giải pháp cho điều đó:

module RenderCallbacks
  extend ActiveSupport::Concern
  include ActiveSupport::Callbacks
  ...
  
  module ClassMethods
    def append_before_render_action *callbacks, &block
      _insert_callbacks callbacks, block do |callback, options|
        set_callback :render, :before, callback, options
      end
    end

    def prepend_before_render_action *callbacks, &block
      _insert_callbacks callbacks, block do |callback, options|
        set_callback :render, :before, callback, options.merge(prepend: true)
      end
    end

    def skip_before_render_action *callbacks, &block
      _insert_callbacks callbacks, block do |callback, options|
        skip_callback :render, :before, callback, options
      end
    end

    alias_method :before_render, :append_before_render_action
    alias_method :prepend_before_render, :prepend_before_render_action
    alias_method :skip_before_render, :skip_before_render_action
  end
end

Tất cả những method được định nghĩa trong module ClassMethods sẽ trở thành class method của những class include module này. Method _insert_callbacks sẽ tạo ra một default options được sử dụng làm tham số cho method set_callback hay skip_callback. Chi tết về việc sử dụng các method này các bạn có thể xem tại đây. Bây giờ chúng ta sẽ viết lại UsersController như sau:

class UsersController < ApplicationController
  include RenderCallbacks
  
  before_render :set_layout

  def new
    ...
  end
  
  def create
    ...
  end
  
  private
  def set_layout
    ...
  end
end

Mọi thứ hoạt động tốt và code trông đã rất ngắn gọn và sạch sẽ.

Với model thì việc tạo mới một callback sẽ đơn giản hơn. Mặc định Rails đã hộ trợ rất nhiều những callback xung quanh lifecycle của một object như: create, update, destroy. Tương ứng với đó là các thời điểm chạy callback: before, after, arround. Lấy luôn ví dụ về trang Viblo, khi một user chuyển bài viết từ Private draft sang trạng thái Public thì những user đang theo dõi user này sẽ nhận được thông báo về bài viết mới. Để tạo một callback trong model chúng ta phải extend module ActiveModel::Callbacks mà Rails cung cấp:

class Post
  extend ActiveModel::Callbacks
  
  define_model_callbacks :publish, only: :before
  
  before_publish :notify_subscribed_users
  
  def publish
    run_callbacks :publish do
      puts "Make this post public"
    end
  end
  
  private
  def notify_subscribed_users
    ...
  end
end

Về cơ bản việc tạo callback ở trong một model cũng tương tự như với một controller ngoại trừ việc ActiveModel::Callbacks cung cấp một số method để việc tạo callback trở nên dễ dàng hơn.

  • Cũng giống như define_callbacks nhưng define_model_callbacks còn thực hiện luôn chức năng của hàm set_callback khi xác định thời điểm chạy callback bằng một tham số options. Trong trường hợp này only: :before
  • Khai báo method :publish và phạm vi đoạn code sẽ được xác định để chạy callback
  • Khai báo method sẽ được callback. Trong trường hợp này notify_subscribed_users sẽ được chạy ngay bên dưới dòng code puts "Make this post public"

Như vậy chúng ta đã tìm hiểu xong về một method rất thú vị và mạnh mẽ của Ruby là alias_method cũng như tìm hiểu về cách để tạo ra một callback trong Rails. Việc tự tạo cho mình một callback sẽ giúp ta hiểu rõ hơn về cách thức hoạt động của chúng cũng như để việc sử dụng những callback mà Rails cung cấp hiệu quả hơn.

0