Refactoring fat models in Rails
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 ...
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 ObjectXé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 ObjectVí 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/.