Refactoring fat models in Rails
Bài đăng này đã không được cập nhật trong 8 năm
Mở đầu
Khi tiếp xúc với framework Rails, chắc hẳn ai cũng đã quen với thuật ngữ Fat Model, Thin(skinny) Controller. Tuy nhiên chúng ta cũng nhận thấy rằng khi mà ứng dụng ngày càng được mở rộng, Model cũng ngày một phình to ra đến một ngày khi nhìn lại model đó, khoảng 500 -1000 dòng codes với hàng trăm methods ắt hẳn sẽ khiến chúng ta phải suy nghĩ lại.
Sử dụng Mixins cũng không đem lại nhiều hiệu quả, nó giống như việc bạn chia nhỏ một class model 1000 dòng ra làm 5 phần vậy. Nói nôm na là nó giống như việc bạn dọn dẹp một căn phòng bừa bộn bằng cách đóng một cái tủ có 5 ngăn rồi cố nhét hết đồ vào các ngăn đó, bề ngoài thì có vẻ như căn phòng gọn gàng và sạch sẽ, tuy nhiên thì bên trong các ngăn tủ vẫn là một mớ hỗn độn và chẳng refactor được tẹo nào.
The refactorings
1. Với Object
Xét ví dụ sau
class Constant < ActiveRecord::Base
def down_vote
if votting_label == "F"
nil
else
votting_label.succ
end
end
def vote_higher_than?(other_vote)
votting_label > other_vote.vote_label
end
def votting_label
if range <= 2 then "A"
elsif range <= 4 then "B"
elsif range <= 8 then "C"
elsif range <= 16 then "D"
else "F"
end
end
end
Ở đây có một số methods được lặp lại nhiều lần, điều đó có nghiã chúng ta cần phải tách riêng đối tượng đó ra, cụ thể ở đây là votting
. Chúng ta có thể tạo value Object cho đối tượng votting này
class Votting
def initialize letter
@letter = letter
end
def to_s
letter.to_s
end
class << self
def from_range range
if range <= 2 then "A"
elsif range <= 4 then "B"
elsif range <= 8 then "C"
elsif range <= 16 then "D"
else "F"
end
end
end
end
Khi đó model constant chỉ còn 1 method, việc tách ra một variant Object đã khiến model gọn và dễ đọc hơn rất nhiều. Chúng ta chỉ cần tập trung vào method xem nó có nghĩa gì, làm gì và ở đâu.
class Constant < ActiveRecord::Base
def votting
Votting.from_range(range)
end
end
Tách ra thôi vẫn chưa đủ, hãy xem ví dụ dưới đây. Ví dụ này nói về chức năng report đơn giản, thu thập dữ liệu order theo start_date
và end_date
, trả về tổng doanh số bán hàng.
class OrdersReport
def initialize(orders, start_date, end_date)
@orders = orders
@start_date = start_date
@end_date = end_date
end
def total_sales_within_date_range
orders_within_range =
@orders.select { |order| order.placed_at >= @start_date &&
order.placed_at <= @end_date }
orders_within_range.
map(&:amount).inject(0) { |sum, amount| amount + sum }
end
end
Trong trường hợp này ta có thể tách riêng một hàm xử lý orders_within_range
...
def total_sales_within_date_range
orders_within_range.
map(&:amount).inject(0) { |sum, amount| amount + sum }
end
private
def orders_within_range
@orders.select { |order| order.placed_at >= @start_date &&
order.placed_at <= @end_date }
end
So sánh hai cách viết ta thấy: Với cách viết thứ 2 người đọc khi đọc đến orders_within_range
thì sẽ nhận biết ngay đây là 1 private method lấy ra order với range tương ứng.Bên cạnh 1 method với 2 dòng code và 2 methods với 1 dòng code, việc tăng số lượng method nhưng bù lại code tinh gọn hơn, dễ dàng test hơn hẳn cũng là một cách tối ưu.
Tiếp tục với private method orders_within_range
, việc lặp lại method order.placed_at
cũng khiến dòng code trở nên rườm rà. Thay vào đó chúng ta có thể tạo một object method cho đối tượng order.
def orders_within_range
@orders.select { |order| order.placed_between?(@start_date, @end_date)}
end
class Order < ActiveRecord
def placed_between?(start_date, end_date)
self.placed_at >= start_date && self.placed_at <= end_date
end
end
Chúng ta đã rời phần xử lý đối tượng order về chính class object đó. Điều này khiến code được tường minh hơn.
2. Với Null Object
Ví dụ có class User, chúng ta muốn xuất ra thông tin của user đó
class User < ActiveRecord
def profile_link_text
"#{user ? user.name : "Guest"}'s Profile"
end
end
Method này trông có chút lộn xộn. Sẽ ra sao nếu trường name
trống. Thay vì việc phải thêm điều kiện kiểm tra, ta có thể tạo 1 class NullUser thay thế.
class NullUser
def name
"Guest"
end
end
Lúc này User model được viết lai như sau
class User < ActiveRecord
def user
@user ||= NullUser.new()
end
def profile_link_text
"#{user.name}'s Profile"
end
end
Kết luận Trên đây là một số phương pháp refactoring rails model. Để tìm hiểu sâu hơn và cụ thể hơn về việc áp dụng refactoring cho các parttens bạn có thể tham khả qua link: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/.
All rights reserved