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 ViewHelpers … def 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