7 design pattern để tái cấu trúc MVC components trong Rails △

Để các Model, View, Controller trong rails được gọn gàng, chúng ta phải liên tục tái cấu trúc lại code. Tái cấu trúc là một tiến trình tái cơ cấu lại code hiện có. Trong khi tái cấu trúc không làm thay đổi bất cứ cái gì từ phía góc nhìn của end user, nó giúp cho code được sạch sẽ, dễ dàng bảo trì, test, đem lại nhiều ích lợi cho developer.

Tái cấu trúc tuân theo một quy tắc đơn giản là nếu bạn tạo ra một mớ hỗn độn thì chính bạn nên là người tự dọn dẹp nó. Tái cấu trúc là việc liên tục dọn dẹp những thứ xảy ra sau khi code thay đổi. Bạn không thể xây dựng 1 tòa nhà chọc trời hay vẽ 1 bức tranh kiệt tác mà không có nhiều mớ hỗn độn trong quá trình này, và cũng giống như việc viết ra code chất lượng. Đó chính là lý do tại sao chúng ta cần phải tái cấu trúc lại code mỗi khi implement một tính năng mới nào đó.

Ở đây mình sẽ giới thiệu 7 design pattern để tái cấu trúc lại code:

  • Service Objects.
  • Value Objects.
  • Form Objects.
  • Query Objects.
  • View Objects.
  • Policy Objects.
  • Decorators.

Service Objects

Service Object được sử dụng khi một action có các tính chất:

  • phức tạp (ví dụ như tính tiền lương).
  • sử dụng API từ bên ngoài.
  • không thuộc về riêng 1 model nào đó (ví dụ như xóa các outdated data).
  • sử dụng nhiều model (ví dụ như import data từ 1 file ra nhiều model khác nhau).

Để giải quyết những vấn đề này, chúng ta đóng gói hoạt động lại với một service bên ngoài.

Kết quả là CheckoutService chịu trách nhiệm về việc tạo và thanh toán tài khoản customer. Nhưng sau khi giải quyết được việc có qua nhiều logic trong controller như trên, ta lại gặp 1 vấn đề khác đó là chuyện gì sẽ xảy ra nếu service bên ngoài kia throw 1 exception (ví dụ như credit card không hợp lệ) và chúng ta phải điều hướng user tới 1 page khác ?.

Để giải quyết việc này chúng ta thêm 1 CheckoutService call và chặn các exception với Interactor Object. Interactor được sử dụng để gói gọn các logic nghiệp vụ. Mỗi interactor thường mô tả 1 quy tắc nghiệp vụ.

Mô hình Interactor giúp chúng ta đạt được Nguyên tắc Trách nhiệm Duy nhất (SRP) bằng cách sử dụng plain old Ruby objects (POROs) – để lại các model chỉ chịu trách nhiệm ở mức ổn định. Interactors gần giống với Service Object nhưng thường trả về một số giá trị cho biết trạng thái thực thi và các thông tin khác (ngoài các hành động thực thi). Dưới đây là 1 ví dụ:

Bằng cách chuyển tất cả ngoại lệ liên quan đến card ra ngoài, controller của chúng ta đã trở nên gọn gàng hơn, chỉ chịu trách nhiệm chuyển hướng người dùng đến các trang thanh toán thành công hay không thành công.

Value Objects

Value Object khuyến khích các đối tượng đơn giản, nhỏ (thường chỉ chứa các value cho trước) và cho phép bạn so sánh các đối tượng này theo một logic nhất định hoặc đơn giản dựa trên các thuộc tính cụ thể (và không dựa trên danh tính của chúng). Một ví dụ về value object là các đối tượng biểu diễn các giá trị tiền bằng các loại tiền tệ khác nhau. Sau đó chúng ta có thể so sánh các value object này theo 1 loại tiền cụ thể (ví dụ USD). Ví dụ, value object cũng có thể biểu thị nhiệt độ và được so sánh bằng thang Kelvin.

Chúng ta có thể chuyển logic so sánh nhiệt độ sang Model, vậy nên Controller chỉ chuyển các tham số tới phương thức cập nhật. Nhưng Model vẫn không lý tưởng – nó biết quá nhiều về cách xử lý chuyển đổi nhiệt độ.

Để làm cho model trở nên gọn hơn, chúng ta tạo Value Object. Khi khởi tạo, các đối tượng nhận các giá trị nhiệt độ và thang độ. Khi so sánh các đối tượng này, spaceship method (<=>) sẽ so sánh nhiệt độ của chúng, chuyển thành Kelvin.

Value Object này cũng chứa 1 method to_h để gán các thuộc tính khối. Value Object cung cấp các method from_kelvin, from_celsius, and from_fahrenheit để dễ dàng tạo các object từ nhưng thang độ khác nhau.(ví dụ Temperature.from_celsius(0) sẽ tạo 1 object với 0°C hoặc 273°К)

Kết quả là ta có 1 model và controller khá gọn gàng. Controller (AutomatedThermostaticValvesController) không hề biết gì về các chuyển đổi nhiệt độ, Model (AutomatedThermostaticValve) thì cũng không biết gì luôn mà chỉ sử dụng duy nhất các method từ Temperature value object.

Form Objects

Form Object là một design pattern đóng gói logic liên quan đến việc xác thực và lưu trữ dữ liệu.

Một giải pháp là di chuyển logic xác nhận vào 1 class chịu trách nhiệm riêng biệt duy nhất là UserForm:

Sau khi chúng ta chuyển logic xác thực sang UserForm, chúng ta có thể sử dụng nó trong Controller như sau:

Kết quả là, Model user không còn chịu trách nhiệm xác thực dữ liệu:

Query Objects

Query Object là một design pattern cho phép chúng ta trích xuất logic truy vấn từ Controller và Model thành các lớp có thể tái sử dụng.

Bước đầu tiên để refactor lại controller này là giấu và đóng gói các điều kiện truy vấn cơ bản và cung cấp một API đơn giản cho query models. Trong rails chúng ta có thể sử dụng scope:

Bây giờ chúng ta có thể sử dụng API đơn giản này để truy vấn mọi thứ chúng ta cần mà không phải lo lắng về việc triển khai cơ bản. Nếu article schema bị thay đổi, ta chỉ cần thực hiện thay đổi đối với class article:

Cách làm trên cũng tốt, tuy nhiên lại phát sinh một số vấn đề mới. Chúng ta phải tạo scope cho mỗi điều kiện truy vấn ta muốn đóng gói. Làm cho model trở nên chật chội với các combination khác nhau của mỗi scope cho các use case khác nhau. Một vấn để khác là scope không thể tái sử dụng cho các model khác nhau. Không nhưng thế chúng ta còn phá vỡ quy tắc trách nhiệm duy nhất vì đã ném tất cả các trách nhiệm liên quan đến truy vấn vào class Article. Lời giải cho vấn để này là sử dụng Query Object

Bây giờ nó có thể tái sử dụng được, chúng ta có thể sử dụng lớp này để truy vấn bất kỳ kho lưu trữ nào khác có lược đồ tương tự:

Nếu chúng ta muốn chain chúng:

Bây giờ chúng ta có một lớp có thể tái sử dụng với tất cả các logic truy vấn được đóng gói, với một giao diện đơn giản, dễ dàng để kiểm tra.

View Objects

View Object cho phép chúng ta lấy dữ liệu và tính toán chỉ cần thiết để hiển thị Model trong View – chẳng hạn như trang HTML cho trang web hoặc JSON response API – nằm ngoài Controller và Model.

Để giải quyết vấn đề này, chúng ta tạo 1 presenter class và tạo 1 ArticlePresenter class instance. ArticlePresenter method trả về các tag mong muốn với các phép tính thích hợp:

Bây giờ chúng ta đã có View không bao gồm bất kì logic tính toán nào hết , tất cả đã được chuyển qua presenter và có thể tái sử dụng ở View khác.

Policy Objects

Policy Object gần tương tự với Service Object, nhưng chịu trách nhiệm cho các hoạt động đọc trong khi các Service Object chịu trách nhiệm cho các hoạt động ghi. Policy Object đóng gói các quy tắc nghiệp vụ phức tạp và có thể dễ dàng được thay thế bằng các Policy Object khác với các rule khác nhau. Ví dụ: chúng ta có thể kiểm tra xem người dùng khách có thể truy xuất một số tài nguyên nhất định hay không bằng cách dùng Policy Object. Nếu người dùng là admin, chúng ta có thể dễ dàng thay đổi Policy Objecth khách này thành Policy Object của admin chứa các rule của admin.

Để làm gọn lại controller, chúng ta chuyển policy logic qua Model. Kết quả là tất cả đã được ném ra khỏi controller nhưng Model lại biết quá nhiều về Redis và Project class logic.

Trong trường hợp này, ta có thể làm cả Model va Controller gon lại bằng cách chuyển các logic policy qua Policy Object:

Kết quả là 1 controller và model khá sạch sẽ gọn gàng. Policy Object đóng gói logic kiểm tra quyền, và tất cả các phụ thuộc bên ngoài được inject từ Controller vào Policy Object. Tất cả các class đều làm công việc của riêng mình và không có ai khác.

Decorators

Decorator cho phép chúng ta thêm bất kỳ loại hành vi phụ trợ nào vào các đối tượng riêng lẻ mà không ảnh hưởng đến các đối tượng khác của cùng một class. Design Pattern này được sử dụng rộng rãi để phân chia chức năng trên các class khác nhau và là một lựa chọn tốt cho các class con để tôn trọng Nguyên tắc về trách nhiệm duy nhất (Single Responsibility Principle).

Chúng tôi có thể giải quyết vấn đề này với gem Draper, di chuyển tất cả logic đến các method CarDecorator:

Sau đó thì View chỉ cần thế này:

Tổng kết lại

Những khái niêm trên cung cấp cho chúng ta những hiểu biết cơ bản về việc refactor lại code, khi nào cần refactor và refactor thế nào. Bằng cách viêt code cẩn thận và đặt các logic hợp lý ngay từ ban đầu, bạn có thể giảm phần lớn thời gian ngồi hì hục refactor lại code.

Hy vọng bài viêt này có ích với các ban.

Chào thân ái và không hẹn gặp lại △.

wasted 10 minutes of your life