Refactor rails view

Tại sao không nên đặt logic trong view?

Thực sự chúng ta hoàn toàn hiểu được nguyên nhân chính không nên để logic phức tạp ở trong view, và tất nhiên đó là testing. Điều tôi muốn nói ở đây là không phải chúng ta không test được những logic đã được đặt trong view, nhưng thực sự điều đó là phức tạp. Đương nhiên tôi cũng là một người lười biếng và tôi chẳng thể nào muốn mình làm thêm bất cứ việc nào nữa.

Nguyên nhân thứ hai là view nên có một ít code động nếu có thể. Điều này làm cho code gọn gàng hơn và cũng dễ dàng để chúng ta thay đổi cũng như maintain những dòng code đó.

Tôi thấy có một vài convention để đảm bảo cho các bạn làm tốt hơn với phần view của mình.

  • Chỉ nên sử dụng 1 dấu chấm khi gọi method. Điều này cũng được biết tới như là Law of Demeter. Một lời khuyên chân thành từ tôi là bạn nên tránh việc gọi nhiều hơn một method. Một ví dụ @employee_skill.skill.name. Rõ ràng chúng ta nên xử lý ở model hoặc controller hơn là gọi như vậy.
class Employee
  def skill_name
    skill.name
  end
end

=> @employee.skill_name
  • Không nên query database từ views. Lỗi này xảy ra khá phổ biến, và bạn không nên thực hiện điền này trong views.
  • Tránh gán các biến trong views. Việc tính toán trong views là không nên, bạn chỉ nên sử dụng biến để hiện thị lên mà thôi.

Bên cạnh việc áp dụng các convention tôi đã gợi ý ở trên thì chúng ta có một số kinh nghiệm để giải quyết logic ở trong views.

Những sai lầm với helpers

Rails cung cấp cho chúng ta một tool khá mạnh là helpers. Chúng ta có thể định nghĩa các method bên trong các helpers và sau đó sử dụng chúng ở views một các ảo diệu. Điều đó thật tuyệt, tuy nhiên tôi sẽ chỉ cho các bạn thấy những điểm mà tôi thực sự không thích ở helpers:

  • Helpers thường lấy dữ liệu từ database. Ví dụ: visible_comments_for(article)
  • Một lượng lớn helpers dùng để tạo html. Trong trường hợp này, khi tôi cần thay đổi, tôi sẽ phải thay đổi rất nhiền chỗ trong helper. Điều đó thật bất tiện. Và tất nhiên, bài học rút ra là helpers chỉ nên định dạnh dữ liệu.
  • Helper thì không rõ ràng về object mà chúng ta nhận được từ helper method. Và điều này làm cho việc xử lý công việc theo OOP trở nên khó hiểu.
  • Không thể sử dụng inheritance với helpers
  • Thật khó để quản lý việc phục thuộc của helper này với helper khác.
  • Test dễ những không hoàn toàn là ổn.

Fat models

Thực sự đây không phải là một lựa chọn thông minh, tuy nhiên tôi cũng đề cập ở đây như là một giải pháp cho bài toán của chúng ta hôm nay. Chúng ta có thể đóng góp những view logic ở bên trong model. Tât nhiên, điều này làm cho số lượng dòng code trong model tăng lên một cách đánh kể. Đổi lại chúng ta có thể test dễ dàng và sử dụng cho mục đích kế thừa nếu có.

Để tránh viejc fat models, có thể tách thành các module template khác nhau, sau đó include chúng vào model.

Decorators

Để phân chia views có liên quan đến logic trong model đồng thời vẫn sử dụng OOP, chúng ta nên sử dụng Decorator Pattern. Pattern này cho phép thêm các hành vi tới single object. Thực tế có một vài gem implement pattern này. Một trong số đó là draper. Nó có DSL rất tuyệt vời, không chỉ decorating cho model và còn cho cả những relations.

Decorator pattern được thiết kế để thay thế object cùng với những method đi kèm nên chúng ta có thể sử dụng decorator object giống như việc sự dụng object.

Việc test với giải pháp này cũng rất dễ. Có thể dùng stubs để test mà không lo hitting vào database.

Sử dụng decorator là một giải pháp hay và gọn gàng. Nhưng nếu tôi cần xử lý logic phức tạp trên nhiều model không có quan hệ với nhau hoặc logic không hẳn nằm trên hầu hết model. Lúc đó decorator pattern là Wrapper.

View object

Về mặt khái niệm, view object khá đơn giản. Tất cả logic cần ở trong view thì chúng ta gộp chúng lại thành view object.

View object thỉnh thoảng có thể xem như là một decorator đơn giản, đó là khi logic của bạn chỉ trên một model. Trong trường hợp của chúng ta thì phức tạp hơn nhiều và chúng ta cần nhóm lại thành một object.

class DiscussionViewObject
  attr_reader :discussion

  delegate :name, :created_at, to: :discussion

  def initialize(discussion)
    @discussion = discussion
  end

  def name_with_time
    @name_with_time ||= created_at.strftime('%Y/%m/%d') + name
  end
end

Trường hợp thứ hai này khi mà logic của bạn không dựa trên một số models hoặc tùy theo request. Khi đó chúng ta có thể sử dụng Presenter một phần trong MVP pattern.

MVP là mọt pattern được biết tới trong C#/Java, được sử dụng để xây dựng interfaces. Cho phép chúng ta phần chia: model chứa logic, present chứ toàn bộ view logic và view.

Sự khác nhau cơ bản giữa presents và những gì chúng ta đang có là chúng ta vẫn có controller và nhận các yêu càu từ người dùng.

Một ví dụ về View Object chứa logic từ 2 models không có relation.

class IssuesPresenter
  attr_reader :issues, :filters

  def initialize(issues, filters)
    @issues, @filters = issues, filters
  end

  def has_selected_filters?
    filter.any?
  end

  def all_issues_are_resolved?
    issues.all?(:resolved?)
  end
end

View Object cho phép chúng ta xây dụng những trang có logic phức tạp.

Chúng ta có thể tạo ra nhiều view object nếu chúng ta muốn, và điều đó là cần thiết.

Chúng ta có nhiều ưu điểm khi sử dụng giải pháp này:

  • View Object là PORO, vậy nên chúng ta thoải mái sử dụng OOP trong ruby: mixins, inheritance, ....
  • View Object không bị giới hạn về model.
  • Test dễ dàng.

Như vậy để refactor view chúng ta có khá nhiều giải pháp và trên đây là một số giải pháp. Việc áp dụng giải pháp nào là tùy vào yêu cầu bài toán và khả năng áp dụng.