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 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_dateend_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/.