Cấu trúc của các component trong Rails và các cách refactor code với các Ruby object
Trang web của bạn đang thu hút được người xem, và bạn đang phát triển nhanh chóng. Ruby on Rails là ngôn ngữ lập trình của bạn lựa chọn. Nhóm của bạn là lớn hơn và bạn muốn tránh "fat models, skinny controllers" như là một phong cách thiết kế cho các ứng dụng Rails của bạn. Tuy nhiên, bạn vẫn không ...
Trang web của bạn đang thu hút được người xem, và bạn đang phát triển nhanh chóng. Ruby on Rails là ngôn ngữ lập trình của bạn lựa chọn. Nhóm của bạn là lớn hơn và bạn muốn tránh "fat models, skinny controllers" như là một phong cách thiết kế cho các ứng dụng Rails của bạn. Tuy nhiên, bạn vẫn không muốn từ bỏ sử dụng Rails.
Không vấn đề gì. Hôm nay, chúng ta sẽ thảo luận làm thế nào để sử dụng thực hành tốt nhất OOP của để làm cho mã của bạn sạch hơn, độc lập nhiều hơn, và tách rời hơn.
Trong phạm vi bài viết này mình sẽ đề cập tới việc refactor code là chính. Nếu các bạn muốn tìm hiểu kĩ về các component trong Rails các bạn có tham khảo link tại đây
Có phải App của bạn đang cần phải refactor?
Hãy bắt đầu bằng cách nhìn vào cách bạn nên quyết định xem ứng dụng của bạn là một ứng viên tốt cho refactoring
Dưới đây là danh sách các số liệu và các câu hỏi tôi thường hỏi bản thân mình để xác định code của tôi có cần refactor không?
- Slow init tests. kiểm tra đơn vị ORO thường chạy nhanh với code độc lập, vì vậy test chạy chậm thường có thể là chỉ số về thiết kế xấu và tích hợp quá nhiều
- FAT models or controllers. Một model hoặc controller quá 200 dòng code (line of code - LOC) thường rất tốt để refactor
- Code quá lớn. Nếu bạn có các file ERB/HTML/HAML quá 30000 LOC hoặc code Ruby (không tính GEM) với hơn 50000 LOC thì đó cũng là một cơ hội tốt để refactor.
Hãy thử câu lệnh sau để kiểm tra số lượng dòng code Ruby mà project bạn có:
find app -iname "*.rb" -type f -exec cat {} ;| wc -l
Lệnh trên sẽ search toàn bộ các file có đuôi là .rb (các file ruby) trong folder /app và xuất ra số lượng dòng code. Xin lưu ý rằng con số này chỉ là xấp xỉ vì dòng comment cũng sẽ được tính vào đó.
Một tùy chọn khác chính xác hơn và nhiều thông tin hơn là sử dụng rake task của Rails stats, nó sẽ trả ra một tóm tắt nhanh chóng của LOC, số class, số method, tỉ lệ các method trong class, và tỉ lệ LOC trong một method. Ví dụ như sau:
$ bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- Tôi có thể trích xuất pattern trong codebase của tôi?
Tách các hành động
Hãy bắt đầu với một ví dụ thực tế:
Giả sử chúng ta muốn viết một ứng dụng theo dõi thời gian cho người chạy bộ. Tại trang chủ, người dùng có thể thấy thời gian họ đã nhập.
Mỗi entry thời gian có ngày, khoảng cách, thời gian, và bổ sung có liên quan "tình trạng" thông tin (ví dụ như thời tiết, loại địa hình, vv), và tốc độ trung bình có thể được tính toán khi cần thiết.
Chúng ta cần một trang báo cáo hiển thị tốc độ trung bình và khoảng cách mỗi tuần.
Nếu tốc độ trung bình cho các mục nhập cao hơn so với tốc độ trung bình tổng thể, chúng ta sẽ thông báo cho người sử dụng với một tin nhắn SMS (ví dụ này, chúng ta sẽ sử dụng Nexmo API RESTful để gửi tin nhắn SMS).
Trang chủ sẽ cho phép bạn chọn các khoảng cách, ngày tháng và thời gian chạy bộ để tạo ra một mục nhập tương tự như sau:
Chúng ta cũng có một trang thống kê đó là cơ bản một báo cáo hàng tuần bao gồm tốc độ trung bình và khoảng cách di chuyển theo từng tuần.
- Các bạn có thể xem ví dụ online tại đây
Code
Cấu trúc của một ứng dụng sẽ như sau:
. ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Chúng ta sẽ không nói tới model User vì nó không có j đặc biệt do chúng ta sử dụng gem Devise để xác thực.
Với model Entry, nó sẽ chưa các xử lí logic của ứng dụng.
Chúng ta sẽ kiểm tra sự tồn tại các attributes khoảng cách (distance), thời gian (time_period), ngày tháng (date_time) và trạng thái (status) của từng entry.
Mỗi khi tạo ra một entry, chúng ta so sánh tốc độ trung bình của người dùng với mức trung bình của tất cả các người dùng khác trong hệ thống, và thông báo cho người dùng bằng cách sử dụng tin nhắn SMS Nexmo (chúng ta sẽ không thảo luận về cách thư viện Nexmo được sử dụng, mặc dù tôi muốn để chứng minh một trường hợp mà chúng ta sử dụng một thư viện bên ngoài).
Lưu ý rằng, model Entry chứa nhiều hơn các logic xử lí một mình. Nó cũng xử lí các validations và callbacks
File entries_controller.rb có các actions CRUD. EntriesController#index lấy ra các entry của current_user và sắp xếp theo thời gian tạo, trong khi EntriesController#create tạo ra entry mới. Không cần phải thảo luận rõ ràng và trách nhiệm của EntriesController#destroy :
statistics_controller.rb có trách nhiệm tính toán các báo cáo theo tuần, StatisticsController#index lấy ra các entry để ghi log cho user và nhóm lại theo tuần, sử dụng method #group_by của class Enumerable trong Rails. Sau đó nó sẽ cố gắng để decorate các kết quả sử dụng một số private method
Chúng tôi không thảo luận nhiều về view ở đây, vì code sẽ tự giải thích.
Dưới đây là view để hiển thị các entry log lại theo user (index.html.erb). Đây là template sẽ được dùng để hiển thị các kết quả của các hành động index (method) trong entry controller:
Chúng ta sử dụng patials render @entries, để kéo code dùng chung vào trong patial template _entry.html.erb nên chúng ta sẽ giữa code DRY và tái sử dụng:
Tương tự với patial _form. Thay cho việc sử dụng các đoạn code giống nhau (new và edit), nên tạo ra một patial form để tái sử dụng.
Đối với các trang xem báo cáo hàng tuần, statistics/index.html.erb cho thấy một số liệu thống kê và báo cáo hàng tuần của người sử dụng bằng cách nhóm một số entry
Và cuối cùng, là entries_helper.rb, bao gồm 2 helper readable_time_period and readable_speed để làm các attributes cho dễ hiểu.
Mọi thứ vẫn chưa có j khác biệt cả.
Hầu hết các bạn sẽ lập luận refactoring này là chống lại các nguyên tắc KISS và sẽ làm cho hệ thống phức tạp hơn.
Nên hệ thống có thực sự cần phải refactor?
Hoàn toàn không, nhưng chúng ta sẽ xem xét nó chỉ cho mục đích trình diễn.
Sau khi tất cả, nếu bạn kiểm tra các phần trước, và các đặc tính mà chỉ ra một ứng dụng cần refactor, nó trở nên rõ ràng rằng các ứng dụng trong ví dụ của chúng tôi không phải là một ứng cử viên hợp lệ cho tái cấu trúc.
Life Cycle
Chúng ta sẽ bắt đầu bằng việc giải thích về cấu trúc của MVC pattern trong Rails
Thông thường, sẽ bắt đầu khi brower tạo một request ví dụ như https://www.toptal.com/jogging/show/1
Web server nhận request và sử dụng routes để tìm tới controller được sử dụng
Controller sẽ xử lí request từ người dùng, dữ liệu gửi lên, cookie, sessions, etc ..., và gọi tới model để lấy dữ liệu.
Model là các class Ruby để trao đổi với database, lưu và kiểm tra dữ liệu, xử lí các logic công việc và ngoài ra xử lí các công việc khác. Các view là những thứ mà người dùng nhìn thấy: HTML, CSS, XML, Javascript, JSON.
Nếu chúng ta muốn hiển thị chuỗi các yêu cầu life cycle của Rails, nó sẽ như sau:
Những gì tôi muốn đạt được là thêm nhiều abstraction sử dụng plain old ruby objects (POROs) và các kiểu pattern giống như sau cho action create/update:
Và các pattern như sau cho action show/index:
Bằng cách thêm các POROs abstractions chúng tôi sẽ đảm bảo tách biệt hoàn toàn giữa trách nhiệm SRP, những việc mà Rails không phải là tốt.
Trên đây tôi đã trình bày cho các bạn về cấu trúc cơ bản của một Rails project. Hi vọng các bạn sẽ có một cái nhìn tổng quan về các component trong rails. Trong phần tiếp theo tôi sẽ trình bày chi tiết các cách để refactor bằng cách sử dụng các kĩ thuật như: Form object, Service object, Query object, Decorator/Presenter, Value object.