Rails Antipatterns, Best Practice Ruby on Rails Refactoring [Part 2]

Tiếp nối phần 1 Rails Antipatterns, Best Practice Ruby on Rails Refactoring [Part 1] đang dang dở khi nói tới các giải pháp xử lý Antiparttern cho AntiPattern Voyouristic Model, sau đây mình tiếp tục trình bày các giải pháp tiếp theo.

1.1.2 Push All find() Calls into Finders on the Model

Hầu hết những lập trình viên không quen với mô hình MVC trong Rails hay chưa quen thì thường có những dòng code mà đơn giản là nó không nên có. Trong phần này chúng ta sẽ đi qua vài ví dụ và một vài minh họa về kỹ thuật từ đó có thêm một số kiến thức về đưa những phần domain model ra khỏi views và controllers. Vấn đề được thảo luận có thể thể hiện trong cả 3 lớp của mô hình MVC, nhưng rõ nhất vẫn làm trong view hoặc controller. Ở một nơi nào đó mà chúng ta sử dụng model để gọi trực tiếp thay vì gọi bên trong model, đấy chính là nơi mà giảm việc tiếp cận và bảo trì. Cùng xem xét ví dụ về việc hiện thị thông tin của những users trong hệ thống được sắp xếp theo last_name và tạm thời chúng ta gọi trực tiếp từ view như sau:

<html>
  <body>
    <ul>
      <% User.find(:order => "last_name").each do |user| -%>
        <li><%= user.last_name %> <%= user.first_name %></li>
      <% end %>
    </ul>
  </body>
</html>

Dường như việc này thực sự là rất tường minh, phong cách này thường thấy ở những lập trình viên PHP. Trong PHP, thực sự là có cả những câu truy vấn SQL trong view ngay bên trong HTML giống như sau:

<html>
  <body>
    <?php
      $result = mysql_query('SELECT last_name, first_name FROM users ORDER BY last_name') or die('Query failed: ' . mysql_error());
      echo "<ul>\n";
      while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
        echo "\t<li>$line[0] $line[1]</li>\n";
      }
      echo "</ul>\n";
    ?>
  </body>
</html>

Nhìn vào cách thể hiện của PHP, thì với Rails dường như là một bước tiến. Tuy nhiên, cả 2 ví dụ có lẽ vẫn cần phải xem xét lại.

Ít nhất, việc đặt logic code bên trong views là một vi phạm đối với MVC. Và tệ hơn là việc logic code bị DRY (don't repeat yourself), là nguyên nhân việc maintain nhiều chỗ. Để tránh vi phạm MVC thì chúng ta cần đưa logic code vào controller, với trường hợp trên là đưa vào UsersController. Khi đó, chúng ta sẽ có:

class UsersController < ApplicationController
  def index
    @users = User.order("last_name")
  end
end

điều này đồng nghĩa với việc trong views chúng to có:

<html>
  <body>
    <ul>
      <% @users.each do |user| -%>
        <li><%= user.last_name %> <%= user.first_name %></li>
      <% end %>
    </ul>
  </body>
</html>

Điều tuyệt vời là không còn có logic code trong view nữa. Vậy chúng ta có nên dừng lại không? Không, đây lại chính là thời điểm để thay đổi, chúng ta sẽ di chuyển code vào trong model bắt đầu từ controller, và được như sau:

class UsersController < ApplicationController
  def index
    @users = User.ordered
  end
end

ở model User

class User < ActiveRecord::Base
  def self.ordered
    order("last_name")
  end
end

Tuy nhiên, trong Rails có cung cấp một khái niệm là scope, và chúng ta có thể viết gọn và đơn giản hóa hơn:

class User < ActiveRecord::Base
  scope :ordered, ->{order :last_name}
end

1.1.3 Keep Finders on Their Own Model

Di chuyển code ra ngoài Controller trong Rails và tùy biến trong Model là một hướng đi đúng đắn và mạnh mẽ. Sai lần phổ biến là di chuyển nhưng bỏ qua đặc tính của chúng tới những phần liên quan. Ví như, chúng ta sẽ thấy những câu truy vấn phức tạp như sau ở trong controller:

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @memberships = @user.memberships.where(:active => true)
      .limit(5).order("last_active_on DESC")
  end
end

Áp dụng vào 2 giải pháp trước, chúng ta sẽ viết thành các hàm bên trong model User. Thử nghĩ xem, bạn cảm thấy thế này đã ổn chưa?

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @recent_active_memberships = @user.find_recent_active_memberships
  end
end

class User < ActiveRecord::Base
  has_many :memberships

  def find_recent_active_memberships
    memberships.where(active: true)
      .limit(5).order("last_active_on DESC")
  end
end

Tất nhiên, chúng ta đều nhận ra, đây thực sự là một cải tiến đáng giá, UsersController trở nên thinner hơn, tên hàm thì tuyệt vời vì nó thể hiện được mục đích của chúng ta. Nhưng, thực sự là chúng ta có thể làm nhiều hơn thế. Trong ví dụ trước, UsersController được biết đến như là triển khai của Membership model. Chúng ta đã cải tiến một chút, nhưng vẫn dựa trên model User, trong khi có những phần xử lý của model Membership. Áp dụng ActiveRecord associations, chúng ta có thể định nghĩa những hàm trên model Membership và gọi tới dựa trên quan hệ giữa User và Membership:

class User < ActiveRecord::Base
  has_many :memberships

  def find_recent_active_memberships
    memberships.find_recently_active
  end
end

class Membership < ActiveRecord::Base
  belongs_to :user

  def self.find_recently_active
    where(active: true).limit(5).order("last_active_on DESC")
  end
end

Có vẻ tốt hơn nhiều so với trước, vì với mô hình MVC đã được thể hiện một cách rõ ràng, rành mạch. Nếu tốt hơn nữa, có thể tối ưu hơn bằng cách sử dụng scope như sau:

class User < ActiveRecord::Base
  has_many :memberships

  def find_recent_active_memberships
    memberships.only_active.order_by_activity.limit(5)
  end
end

class Membership < ActiveRecord::Base
  belongs_to :user

  scope :only_active, where(:active => true)
  scope :order_by_activity, order('last_active_on DESC')
end

Với bước cải tiến cuối cùng này, chúng ta đã khát quát nhiều đoạn code. Chúng ta đã có 3 hàm của class và thoải mái để xử lý theo ý muốn.

Như vậy, chúng ta đã đi qua AntiPattern thứ nhất cùng với những giải pháp đi kèm. Hẹn gặp các bạn ở phần tiếp theo AntiPattern: Fat Model