07/09/2018, 08:45

Draper in Rails

Trong nhiều trường hợp, để hiển nội dung hay thông tin của một object ta cần phải xây dựng thêm các method trong Model hoặc trong Presenter nhằm làm giảm tối đa việt sử dụng logic ở ngoài view. Gem Draper là một gem rất mạnh giúp chung ta có thể thực hiện công việc đó một cách cực kỳ linh hoạt. ...

Trong nhiều trường hợp, để hiển nội dung hay thông tin của một object ta cần phải xây dựng thêm các method trong Model hoặc trong Presenter nhằm làm giảm tối đa việt sử dụng logic ở ngoài view. Gem Draper là một gem rất mạnh giúp chung ta có thể thực hiện công việc đó một cách cực kỳ linh hoạt. Ngày hôm nay chúng ta sẽ đi tìm hiểu về cách thức hoạt động của nó.

Trước tiên là phần cài đặt

Đơn giản là bạn chỉ cần add thêm dòng dưới đây vào Gemfile

gem "draper"

Sau đó run bundle install để cài đặt

Object Decorator

Chúng ta có một model User với các thuộc tính là first_name và last_name

class User < ActiveRecord::Base
  belongs_to :company
  validates_presence_of :first_name, :last_name
end

Ở ngoài view chúng ta muốn show ra tên đầy đủ của một user chúng ta có thể viết một hàm đơn giản trong model, tuy nhiên có thể thấy hàm này chỉ sử dụng cho mục đích hiển thị và không thường xuyên được sử dụng. Vì vậy đặt những hàm như thế trong model không phải là một cách làm đúng đắn. Drapter sẽ giúp chúng ta giải quyết vấn đề đó bằng việc tạo ra một object có tất cả những method cần thiết dựa trên một object ban đầu. Trong thư mục app/decorators chúng ta thêm một class UserDecorator có nội dung như sau:

class UserDecorator < Draper::Decorator
  delegate_all
    
  def full_name
    "#{first_name} #{last_name}"
  end
end

Bên trên chúng ta gọi ra hai hàm first_name và last_name. Hai hàm này chưa được khai báo trong class UserDecorator vậy chúng ta lấy nó ở đâu. Đó là do method delegate_all của classDraper::Decorator

def self.delegate_all
  include Draper::AutomaticDelegation
end

Method này include module Draper::AutomaticDelegation. Đi vào trong module này chúng ta thấy method_missing được overide như sau:

def method_missing(method, args, &block)
  return super unless delegatable?(method)
  object.send(method, args, &block)
end

Khi gọi đến hai hàm first_name và last_name, đầu tiên chúng sẽ được tìm trong các method của parent decorator class. Trong trường hợp này là không tìm thấy, method_missing sẽ được gọi và ở đây first_name và last_name sẽ được gọi từ object, trong trường hợp này object là một user. Sở dĩ làm được điều này vì Draper có một cơ chế map tên của một decorator class với một model class tương ứng.

Khi cài đặt Draper, mặc định module Draper::Decoratablesẽ được include vào trong ActiveRecord::Base. Để tiện theo dõi mình xin copy những đoạn code quan trọng của module này vào đây:

module Draper
  module Decoratable
    extend ActiveSupport::Concern
    include Draper::Decoratable::Equality

    def decorate(options = {})
      decorator_class.decorate(self, options)
    end

    def decorator_class
      self.class.decorator_class
    end

    module ClassMethods
      def decorate(options = {})
        decorator_class.decorate_collection(all, options.reverse_merge(with: nil))
      end

      def decorator_class(called_on = self)
        prefix = respond_to?(:model_name) ? model_name : name
        decorator_name = "#{prefix}Decorator"
        decorator_name_constant = decorator_name.safe_constantize
        return decorator_name_constant unless decorator_name_constant.nil?

        if superclass.respond_to?(:decorator_class)
          superclass.decorator_class(called_on)
        else
          raise Draper::UninferrableDecoratorError.new(called_on)
        end
      end
    end
  end
end

Chúng ta thấy có một class method tên là decorator_class. Method này trả về một decorator class có tên tương ứng với model hiện tại decorator_name = "#{prefix}Decorator". Tiếp theo là môt class method khác có tên là decorate. Method này có nhiệm vụ là decorate cho tất cả các object của model. Hàm mà chúng ta hay gọi nhất cũng có tên là decorate tuy nhiên đây không phải là một class method mà là một instance method.

def decorate(options = {})
  decorator_class.decorate(self, options)
end

Method này khởi tạo và trả về một decorator object tương ứng với object hiện tại. Câu lệnh decorator_class.decorate(self, options) tương đương với decorator_class.new(self, options). Vậy tóm lại là khi ta thực hiện đoạn code sau, những cách gọi dưới đây là tương đương nhau:

user = User.new first_name: "Lam", last_name: "Do"

user.decorate.full_name
user.decorator_class.decorate(user).full_name
user.decorator_class.new(user).full_name
UserDecorator.new(user).full_name

Collection Decorator

Ta có một model nữa có quan hệ với model User như sau

class Company < ActiveRecord::Base
 has_many :user
end

Khi chạy đoạn code dưới đây thì điều gì sẽ xảy ra

company = Company.first
company.users.decorate

Khi này company.users là một collection. Draper sẽ tìm một collection decorator class tương ứng. Ví dụ User sẽ map với UsersDecorator nếu không có class này thì mặc định class được dùng sẽ là Draper::CollectionDecorate.

Như vậy khi gọi company.users.decorate sẽ tương đương với UsersDecorate.new(company.users) nếu như class UsersDecorator đã được định nghĩa, trong trường hợp này là không như vậy đoạn code trên sẽ tương đương với Draper::CollectionDecorator.new(company.users) Việc decorate một collection đơn giản là việc decorate từng element có trong collection đó

Kết luận

Mình vừa trình bày xong cách thức hoạt động cơ bản của Drapter. Hi vọng nó sẽ giúp mọi người hiểu rõ hơn về Drapter để có thể sử dụng nó một cách hiệu quả nhất

0