Skinny controllers through refactoring
Bài đăng này đã không được cập nhật trong 7 năm
Controller có thể mất đi kiểm soát khi development. Skinny controller through refactoring - hay có thể hiểu là làm cho controller
trong mô hình MVC như rails framework đang áp dụng trở nên đơn giản và đúng vai trò hơn trong việc điều khiển nhận và trả về dữ liệu trên server. Công việc của controller phải trở nên thật sự đơn giản. Trong một mô hình MVC, controller đóng vai trò được biết như làm thế nào để làm việc được với model theo thứ tự để lấy ra những thứ mà View cần. Theo cách hiểu thì controller nhận yêu cầu từ người dùng (thông qua params
) và quyết định output có thể trả về (ví dụ như trả về một file JSON)
Thông thường bạn thấy người điều hướng "traffic cop" sẽ đơn giản, nhưng thỉnh thoảng bạn có thể thấy controller ngày càng phình ra như phải cần nhiều phương thức private để giúp xử lý những input phức tạp từ user để trả về output hay query phức tạp. Nếu bạn thấy controller ngày càng trở nên phình ra, hoặc khó để test tất cả các viễn cảnh mà chúng ta phải sử lý, thì đó là lúc mà bạn cần phải refactor code.
Xử lý những query logic phức tạp
Lấy ví dụ về một controller mà yêu cầu chúng ta phải xử lý một số cá logic để handle việc sắp xếp sorting
, phân trang paginage
và có khả năng lọc filter dữ liệu trong cùng một action index
. Chúng ta xử lý chúng trong một class được gọi là RetalUnitsIndex
. Class này sẽ có một trách nhiệm đơn giản, và trách nhiệm đó là xử lý params
của người dùng, và tạo một query từ chúng. Và một phần trong công việc đó là xây dựng các liên kết phân trang tương ứng với các query để truy xuất dữ liệu từ trang này sang trang khác.
Và đó là cái mà chúng ta mong đợi từ phương thức index
xử lý, trông có vẻ khá đơn giản phải không?
def index
rental_units_index = RentalUnitsIndex.new(self)
render json: rental_units_index.rental_units, links: rental_units_index.links
end
Để RetalUnitsIndex
có thể làm công việc của chúng, chúng ta truyền vào chính đối tượng controller self
. Bằng cách truy cập tới controller, chúng ta không chỉ truy cập tới params
mà cũng đồng thời truy cập tới URLs helper mà Rails tạo ra như một phần routing system.
Và đây là class được mô tả, chúng ta sẽ đi vào một số phương thức cụ thể mà chúng ta sẽ viết test để chắc rằng chúng sẽ hoạt động như mong muốn:
class RentalUnitsIndex
DEFAULT_SORTING = {created_at: :desc}
SORTABLE_FIELDS = [:rooms, :price_cents, :created_at]
PER_PAGE = 10
delegate :params, to: :controller
delegate :rental_units_url, to: :controller
attr_reader :controller
def initialize(controller)
@controller = controller
end
def rental_units
@rental_units ||= RentalUnit.includes(:user).
order(sort_params).
paginate(page: current_page, per_page: PER_PAGE)
end
def links
{
self: rental_units_url(rebuild_params),
first: rental_units_url(rebuild_params.merge(first_page)),
prev: rental_units_url(rebuild_params.merge(prev_page)),
next: rental_units_url(rebuild_params.merge(next_page)),
last: rental_units_url(rebuild_params.merge(last_page))
}
end
private
def current_page
(params.to_unsafe_h.dig(:page, :number) || 1).to_i
end
def first_page
{page: {number: 1}}
end
def next_page
{page: {number: next_page_number}}
end
def prev_page
{page: {number: prev_page_number}}
end
def last_page
{page: {number: total_pages}}
end
def total_pages
@total_pages ||= rental_units.total_pages
end
def next_page_number
[total_pages, current_page + 1].min
end
def prev_page_number
[1, current_page - 1].max
end
def sort_params
SortParams.sorted_fields(params[:sort], SORTABLE_FIELDS, DEFAULT_SORTING)
end
def rebuild_params
@rebuild_params ||= begin
rejected = ['action', 'controller']
params.to_unsafe_h.reject { |key, value| rejected.include?(key.to_s) }
end
end
end
Chúng ta sử dụng delegate phương thức trong trường hợp này để có thể truy cập dễ dàng params
khi gọi params
thay vì controller.params
.
Viết test cho class
Bằng cách tạo ra class trích xuất xử lý từ controller, nó cho phép chúng ta có thể dễ dàng viết test cho các phương thức xử lý. Chúng ta không phải tạo đầy đủ request tới controller mỗi lần, thay vào đó chúng ta chỉ cần test riêng mỗi phương thức và output của phương thức đó.
Chúng ta sẽ tạo ra thêm một class để hỗ trợ cho việc viết test. Nó rất đơn giản và cơ bản để tạo ra fake/stubbed thực thể của controller và params. Chúng ta sử dụng thêm ActionController::Parameters
hay strong params class để cho phép chúng ta xử cho phép và yêu cầu những key cụ thể trong params.
RSpec.describe RentalUnitsIndex, :type => :model do
class FakeController
attr_accessor :params
def initialize(params)
@params = params
end
def rental_units_url(*args)
"http://www.fake.com"
end
end
let(:controller) { FakeController.new(params) }
let(:params) do
ActionController::Parameters.new({
sort: sort,
page: page
})
end
let(:sort) { "-rooms" }
let(:page) { {number: "1"} }
let(:rui) { RentalUnitsIndex.new(controller) }
Bằng cách đặt params trong câu lệnh let
, chúng ta có thể tạo riêng mỗi params riêng biệt với mỗi test case với các phương thức.
Đầu tiên chúng ta sẽ test phương thức rental_units
phương thức để chắc chắn rằng nó có thể truy xuất data chính xác:
describe ".rental_units" do
it "queries rental units" do
rental_unit = create(:rental_unit)
expect(rui.rental_units).to include(rental_unit)
end
it "sorts by sort param" do
rental_units = (1..5).to_a.map { create(:rental_unit, rooms: Random.rand(1..10)) }
expect(rui.rental_units.map(&:rooms)).to eq(rental_units.map(&:rooms).sort.reverse)
end
it "paginates results" do
(described_class::PER_PAGE + 1).times { create(:rental_unit) }
expect(rui.rental_units.size).to eq(described_class::PER_PAGE)
end
end
Tiếp theo chúng ta test links. Bằng cách sau chúng ta có thể chắc chắn rằng link được tạo ra với các keys là đúng.
describe ".links" do
it "builds link hash" do
expect(rui.links.keys).to eq([:self, :first, :prev, :next, :last])
end
end
Cuối cùng chúng ta test cho private
phương thức current_page
bởi vì nó thường xuyên được sử dụng khi tạo ra các liên kết links.
describe ".current_page" do
context "present" do
let(:page) { {number: "2"} }
it "finds current page as integer" do
expect(rui.send(:current_page)).to eq(2)
end
end
context "missing" do
let(:page) { {} }
it "sets default page to 1" do
expect(rui.send(:current_page)).to eq(1)
end
end
end
Kết luận
Đừng để bị giới hạn khi sử dụng với ActiveRecord::Base
models trong ứng dụng Rails của bạn. Nó dễ dàng quên đi bằng cách nghĩ sự thật bạn đang viết Ruby code, do đó bạn có thể thoải mái sử dụng class riêng, điều đó có thể giúp bạn viết ra một ứng dụng tốt hơn. Bằng cách tách một vài chức năng được viết trong controller ra class riêng của bạn, chúng ta hoàn toàn có thể giúp controller đơn giản hơn. và đồng thời cũng giúp chúng ta có thể test đơn giản hơn các chức năng trong một index
action.
Refs
All rights reserved