12/08/2018, 16:33

Tại sao chúng ta nên sử dụng View Object?

Khi view của bạn chứ nhiều logic phức tạp hơn, bạn có thể sẽ phải gặp đủ loại vấn đề. Thứ nhất là vấn dề test, thứ hai là code của bạn sẽ khó đọc và khó thay đổi hơn. Hãy xem xét ví dụ dưới đây: < table > < thead > < tr > < th > Ordered ...

Khi view của bạn chứ nhiều logic phức tạp hơn, bạn có thể sẽ phải gặp đủ loại vấn đề. Thứ nhất là vấn dề test, thứ hai là code của bạn sẽ khó đọc và khó thay đổi hơn. Hãy xem xét ví dụ dưới đây:

<table>
  <thead>
    <tr>
      <th>Ordered date</th>
      <th>Items to order</th>
      <th>Vendor</th>
      <th>Term</th>
      <th>Discount</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <% @orders.each do |order| %>
      <tr>
        <td><%= order.date.nil? ? "Not ordered" : order.date.strftime("%m/%d/%Y - %H:%M") %></td>
        <td><%= order.items.map(&:amount_ordered).sum %></td>
        <td><%= order.is_custom ? "Custom" : order.vendor.brand.name  %></td>
        <td><%= order.term %></td>
        <td><%= "#{(order.discount * 100).round(2)} %" %></td>
        <td>
          <%= link_to 'Show', order_path(order), class: 'btn btn-primary btn-xs' %>
          <% if has_order_permission? %>
            <%= link_to 'Destroy', order_path(order), class: 'btn btn-danger btn-xs', 
       method: :delete %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

Nó không đẹp, phải không? Có một số vấn đề chúng ta có thể giải quyết.

Thoạt nhìn có quá nhiều câu lệnh cần hiển thị giá trị model hoặc default value. Tuy nhiên, view không phải là nơi thích hợp cho bất kỳ tính toán nào. Điều tiếp theo là định dạng dữ liệu, nó cần được hoàn thành trước và sau đó chỉ cần hiển thị trên view.

Sau đây là những sai lầm phổ biến nên tránh để giữ cho view gọn gàng hơn:

  • Đưa tính toán vào view. Điều này làm cho code không thể tái sử dụng và khó debug. Nó cũng có thể gây ra vấn đề hiệu suất
  • Lấy dữ liệu từ database
  • Sử dụng dữ liệu lồng nhau phức tạp. Nên tránh việc sử dụng nhiều hơn một dấu chấm trong biểu thức (Ví dụ: ko nên sử dụng order.vendor.brand.name)
  • Sử dụng nhiều câu lệnh điều kiện để xử lý logic hiển thị dự liệu

Không nên tách ra các mixin từ các model cồng kềnh

Chúng ta có thể đóng gói tất cả các logic liên quan đến view trong các model, tuy nhiên lý tưởng, các model trong Rails không nên bao gồm bất kỳ đoạn code nào để trình bày hay hiển thị các thuộc tính của nó.

Có một cách hay là tách logic mà chỉ được sử dụng trong view đến một class khác sẽ tốt hơn là làm chúng rối tung lên trong các model ActiveRecord. Chúng ta vẫn có thể định nghĩa các module logic bên trong và đưa chúng vào model. Nó trông có vẻ gọn gàng hơn, nhưng chúng ta vẫn có một con quái vật khổng lồ với đầy đủ các phụ thuộc, rất khó để xác định.

Rials helper

Rails đã được xây dựng cơ chế để xử lý logic liên quan đến view là View Helpers (app / helpers / * _ helper.rb). Bạn có thể dễ dàng đặt các method vào global namespace và sử dụng chúng bên trên view. Tuyệt vời, phải không? Tuy nhiên, cách này không có tính hướng đối tượng. Di chuyển logic vào helper có thể sử dụng được khi ứng dụng của bạn không phải là quá phức tạp.

Có vài vấn đề với người trợ giúp:

  • Helper được include vào tất cả view, vì vậy cần chú ý conflict khi đặt tên và các hành vi không mong muốn
  • Helper là một module vì vậy nó không thể access vào một đối tượng. Muốn truy cập vào một đối tượng thì chúng ta phải truyền nó sang method
  • Chức năng trong helper không thể được kế thừa

Khi logic liên quan đến view không chính xác thuộc về model cũng không phải là một helper, chúng ta nên sử dụng View Object. Chúng cung cấp cho bạn một giải pháp hướng đối tượng để đại diện việc hiển thị logic của model bằng cách thêm một lớp trừu tượng khác. Cách tiếp cận này mang lại cho chúng ta nhiều đoạn code gọn gàng hơn và có thể bảo trì dễ hơn, bởi vì logic đã được đóng gói.

Bạn có thể gặp các tên khác nhau đề cập đến View Object như Presenter hay Decorator. Thực ra mà nói thì chúng hơi khác nhau. Nhưng vì trách nhiệm của chúng khá giống nhau nên tên thường được thay thế cho nhau, nhưng nói chung chúng ta có thể gọi chúng là View Object.

Hãy xem cách chúng ta có thể implement View Object trong các ứng dụng.

Decorator

Decorator cho phép bạn tách riêng logic cho việc hiển thị và thêm hành vi bổ sung vào một đối tượng riêng lẻ. Nó rất tiện dụng, bởi vì chúng ta có thể thêm bất kỳ hành vi nào chúng ta muốn cho một model instance, trước khi chuyển nó đến một template từ controller. Chúng ta có thể quyết định trong trường hợp nào mà cá thể sẽ sử dụng hành vi bổ sung. Draper là một gem phổ biến cung cấp decorator pattern được xác định rõ ràng. Hãy tìm hiểu những tính năng mà nó cung cấp. Thêm Draper vào Gemfile

gem ‘draper’

cài đặt và tạo ra đối tượng decorator cho model tương ứng

$ bundle install
$ rails generate decorator Order

Nó sẽ tự động tạo thư mục app/decorators và bạn có thể tìm thấy tập tin OrderDecorator mới được tạo ra. Đầu tiên, chúng ta sẽ loại bỏ câu lệnh điều kiện trên view:

class OrderDecorator < Draper::Decorator
  delegate_all

  def formatted_ordered_date
    object.date.nil? ? "Not ordered" : object.date.strftime("%m/%d/%Y - %H:%M")
  end

  def vendor_name
    object.is_custom ? "Custom" : object.vendor.brand.name
  end
  
  def ordered_items_amount
    object.items.map(&:amount_ordered).sum
  end
 
  def discount_percent
    "#{(object.discount * 100).round(2)} %"
  end
end

Tất cả các phương thức của model nguồn có sẵn trên decorator bởi cuộc gọi delegate_all. Sau đó nhưng logic mà chúng ta cần trên view liên quan đến đối tượng sẽ định nghĩa ở decorator. Ngoài ra gem Draper còn cung cấp một cách truy cập đến helper dễ dàng bằng cách sử dụng h như sau:

h.content_tag(:strong, "Hello there!")

Chúng ta có thể include module Draper :: LazyHelpers bên trong decorator cho phép bạn truy cập trực tiếp vào helper. Sau đó chúng ta có thể thêm link đến template mà không cần phải thêm h trước biểu thức link_to. Thêm nữa chúng ta có thể truy cập routes path và helper method.

class OrderDecorator < Draper::Decorator
  include Draper::LazyHelpers
  ...

  def link_show
    link_to 'Show', order_path(object), class: 'btn btn-primary btn-xs'
  end

  def link_destroy
    if has_order_permission?
      link_to 'Destroy', order_path(object), class: 'btn btn-danger btn-xs', method: :delete
    end
  end
end

Chúng ta có thể decorate bất cứ thứ gì, collection hoặc một đối tượng duy nhất Để sử dụng decorate lên một collection chúng ta làm như sau:

class OrdersController < Admin::BaseController
  def index
    @orders = Order.all.includes(:items, :vendor, vendor: [:brand]).order('date desc')
    @orders = OrderDecorator.decorate_collection(@orders)
  end
end

Cũng có thể bạn chỉ muốn decorate cho một đối tượng duy nhất:

@order = OrderDecorator.decorate(Order.first)

Ở trên view, bạn sử dụng decorator như cách bạn đã sử dụng model bình thường. Hãy nhìn vào kết quả của việc sử dụng decorator:

<table class="table table-hover">
  <thead>
    <tr>
      <th>Ordered date</th>
      <th>Items to order</th>
      <th>Vendor</th>
      <th>Term</th>
      <th>Discount</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <% @orders.each do |order| %>
      <tr>
        <td><%= order.formatted_ordered_date %></td>
        <td><%= order.ordered_items_amount %></td>
        <td><%= order.vendor_name %></td>
        <td><%= order.term %></td>
        <td><%= order.discount_percent %></td>
        <td>
          <%= order.link_show %>
          <%= order.link_destroy %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

Bây giờ đoạn code trên view đã dễ đọc và dễ hiểu hơn nhiều

SimpleDelegator

Nếu bạn không muốn thêm gem vào ứng dụng của mình, bạn chỉ có thể sử dụng lớp SimpleDelegator từ thư viện chuẩn của Ruby để thực hiện decorator pattern. SimpleDelegator thực hiện phương thức method_missing mà ruby calls khi không thể tìm thấy một tên phương thức hoặc một biến của đối tượng.

Hãy thử viết decorator cho một đối tượng theo cách này!

Bên trong thư mục app/decorators tạo ra decorator class kế thừa từ SimpleDelegator. Trong trường hợp của chúng tôi, nó sẽ là OrderDecorator (app/decorators/order_decorator.rb). Sau đó, thêm các phương thức sẽ được truy cập cho các đối tượng.

class OrderDecorator < SimpleDelegator
  def formatted_ordered_date
    date.nil? ? "Not ordered" : date.strftime("%m/%d/%Y - %H:%M")
  end

  def vendor_name
    is_custom ? "Custom" : vendor.brand.name
  end

  def ordered_items_amount
    items.map(&:amount_ordered).sum
  end

  def discount_percent
    "#{(discount * 100).round(2)} %"
  end
end

Có vẻ tương tự, phải không? Sự khác biệt là bây giờ chúng ta có thể gọi trực tiếp đến thuộc tính và phương thức. Khi chúng ta truyền một đối tượng vào constructor, SimpleDelegator sẽ delegate tất cả các phương thức của đối tượng, do đó chúng ta không phải lo lắng về các thuộc tính. Điều này có nghĩa khi chúng ta gọi là discount trong OrderDelegator, lớp SimpleDelegator ủy thác thuộc tính cho đối tượng Order.

Nhưng nếu chúng ta cần sử dụng helper bên trong decorator mới thì sao? Giải pháp đơn giản là tạo module ViewHelpers ...

module ViewHelpers
  def h
    ActionController::Base.helpers
  end

  def routes
    Rails.application.routes.url_helpers
  end 
end

... và sau đó include vào bên trong OrderDecorator

class OrderDecorator < SimpleDecorator
   include ViewHelpersdef link_show
       h.link_to 'Show', routes.order_path(self), class: 'btn btn-primary btn-xs'
    end
end

Thường thì bạn cần decorate cả collection chứ không chỉ một. Vì vậy chúng ta hãy đặt method đó bên trong BaseDecorator và sau đó cho phép OrderDecorator của chúng tôi kế thừa.

class BaseDecorator < SimpleDelegator
  def self.wrap_collection(collection)
    collection.map { |obj| new obj }
  end
  ...
end
class OrderDecorator < BaseDecorator
  ...
end

Quay trở lại rails controller, chúng ta gọi như sau:

# single object decoration
@order = OrderDecorator.new(order)

# collection decoration
@orders = OrderDecorator.wrap_collection(orders)

Ok. Đó là cách tạo ra đối tượng decorator sử dụng SimpleDelegator!

Reference

0