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

Sau đây sẽ là một chuỗi bài viết nói về Rails AntiPattern, chuỗi bài viết này được lấy từ quyển sách Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby) của tác giả Tammer Saleh và Pytel Chad. Đây là quyển sách có đánh giá cao trên amazon và được nhiều người khuyên rằng nên có trong tủ sách của bạn.

Quyển sách có 10 chương với nhiều nội dung được đề cập như model, views, controller, service, database, deploy và mở rộng,.... Trong mỗi chương cũng có nhiều vấn được đề cập. Nội dung là những kiến thức thực tế khi trong quá trình làm những dự án thực tế được rút ra của tác giả và điều này thực sự sẽ cho bạn những kiến thức bổ ích trong quá trình làm việc những ứng dụng sử dụng Ruby on Rails framework.

Để bắt đầu, chúng ta cần làm rõ một số khái niệm:

AntiPattern là gì?

AntiPattern là những giải pháp đưa ra để giải quyết những vấn đề trong quá trình làm việc tuy nhiên lại được chứng minh là không tốt, và nó ngược lại với Pattern.

Thời gian AntiPattern được đưa ra là vào năm 1995 bởi Andrew Koenig, lấy cảm hứng từ cuốn sách Design Patterns của nhóm tác giả được biết đến như là Gang of Four, là cuốn sách phát triển những khái niệm về mẫu thiết kế (design pattern) trong công nghệ phần mềm. 3 năm sau đó, AntiPattern trở nên phổ biến hơn với cuốn sách AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis (William Brown, Raphael Malveau, Skip McCormick, and Tom Mowbray). Theo tác giả của AntiPattern, phải có ít nhất 2 yếu tố để phân biệt một antipattern từ một thói quen, thực hành và ý tưởng kém:

  • Sự lặp đi lặp lại của một action, process, và cấu trúc ban đầu là tốt nhưng sau đó thì gân tổn hại nhiều hơn là có lợi
  • Một giải pháp cấu trúc lại được ghi chép đầy đủ, được chứng minh trong thực tế và lặp đi lặp lại

Refactoring là gì?

Refactoring là hành động thay đổi mã của ứng dụng nhưng không làm thay đổi hành vi thay vì nâng cao chất lượng của ứng dụng. Việc này được dùng để cải thiện khả năng đọc, giảm độ phức tạp, tăng khả năng bảo trì và mở rộng trong tương lai của hệ thống.

Sau đây, chúng ta sẽ bắt tay vào chương đầu tiên Models

I. Models

Lớp model trong ứng dụng thường cung cấp cấu trúc cốt lõi của ứng dụng. Đặc biệt nếu chúng ta tuân thủ theo một ứng dụng RESTful thì những thành phần bên trong models sẽ cần đi qua controller và views. Lớp model cũng nên chứa business logic của ứng dụng. Vậy nên models sẽ được chú ý đến rất nhiều bởi lập trình viên trong vòng đời phát triển dự án. Vì có rất nhiều sự chú ý tới models, và cũng vì trách nhiệm phải chứa đựng chúng. Điều này tương đối dễ dàng cho những thứ nằm ngoài tầm kiểm soát trong models khi ứng dụng phát triển và tiến hóa. Ngoài ta, do tính chất mạnh mẽ của lớp models, đặc biệt là Active Record ORM, nó xử lý dễ dàng những thứ nằm ngoài tầm kiểm soát.

May mắn thay, Rails cung cấp cấu trúc và công cụ cần thiết để dừng sự chậm chạp này (hoặc nhanh) trong vòng xoắn đi xuống, và thường vấn đề là làm sao để sử dụng chúng một cách hiểu quả. Ngoài ra, điều quan trọng là luôn có sự bảo vệ ứng dụng của bạn. Đặc biệt, cần có unit test tốt để đảm bảo code của bạn chạy đúng và cung cấp một thử nghiệm sẽ hỗ trợ trong quá trình bảo trì, tái cấu trúc và sửa đổi bổ sung.

Trong chương này, chúng ta sẽ nói tới nhiều cảm bẫy phổ biến trong models của ứng dụng. Và chúng ta cũng đưa ra những giải pháp để xử lý ngay từ lần đầu tiên bạn gặp phải.

1.1 AntiPattern: Voyeuristic Models

Vòng đời của một hàm mà ActiveRecord cung cấp cùng với callbacks, validattion, cấu trúc và tổ chức trong lớp View, Controller của ROR cung cấp công cụ mạng mẽ để xây dụng ứng dụng web. Tuy nhiên, nếu công cụ đó không được sử dụng đúng cách sẽ trở thành con dao 2 lưỡi khi phá vỡ nguyên tắc lập trình hướng đối tượng, không đóng gói và có tổ chức kém.

Vì thế hãy luôn nhớ rằng sức mạnh lớn cũng cần có trách nhiệm đủ lớn. Một lập trình viên có thể tạo ra ứng dụng phá vỡ những nguyên lý cơ bản của lập trình hướng đối tượng với nhiều lý do khác nhau. Ví dụ như, một lập trình viên chưa từng tiếp cận với framework thì khó mà tuân thử MVC một cách đúng đắn. Hoặc một lập trình viên có kinh nghiệm thường sẽ bị ảnh hưởng bởi những gì anh ta có khi áp dụng vào ROR. Hoặc như là việc quá choáng ngợp trước những gì mà Rails cung cấp sẽ làm cho lập trình viên xây dựng ứng dụng quá nhanh, không có tầm nhìn, không có tình kỷ luật. Cả 2 lập trình viên giàu kinh nghiệm và thiếu kinh nghiệm cũng có vấn đề khi tiếp cận với ROR lần đầu tiên.

Các phần sau thể hiện một vài tình huống vi phạm các nguyên lý cốt lõi của MVC và lập trình hướng đối tượng. Chúng ta cũng có những giải pháp để sửa chữa hoặc thay thế những vi phạm này.

1.1.1 Follow the Law of Demeter

Một tính năng mạnh mẽ của Rails là ActiveRecord associations, chúng ta cũng dễ dàng cài đặt, khai báo và sử dụng. Thông qua mối quan hệ giữa các model, chúng ta có thể dễ dàng lấy thông tin, đặc biệt là ở lớp views. Tuy nhiên, nó có thể làm cho cấu trúc trở nên tẻ nhạt và dễ bị lỗi.

Cùng xem những đoạn code như sau:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices
end

class Invoice < ActiveRecord::Base
  belongs_to :customer
end

Bên ngoài views, chúng ta có:

<%= @invoice.customer.name %>
<%= @invoice.customer.address.street %>
<%= @invoice.customer.address.city %>,
<%= @invoice.customer.address.state %>
<%= @invoice.customer.address.zip_code %>

Chúng ta có thể dễ dàng lấy thông tin thông qua mối quan hệ giữa các đối tượng. Điều này thực sự là một điểm mạnh nhưng có một vài lý do mà nó không phải là lý tưởng. Đối với đóng gói (encapsulation), invoice không nên thông qua đối tượng customer để lấy thông tin street của address object.

Bởi vì, nếu trong tương lai, ứng dụng của chúng ta thay đổi, một customer có cả billing address và shipping address, và lúc đó bạn sẽ phải thay đổi code rất nhiều nơi.

Để tránh những vấn đề như vừa mô tả, điều quan trọng là cần tuân theo Low of Demeter, và được biết tới như là Principle of Least Knowledge. Luật này được phát minh tại Đại Học Northeastern năm 1987, đưa ra khái niệm mà một đối tượng có thể gọi các phương thức của đối tượng có liên quan nhưng không thông qua đối tượng liên quan ở một đối tượng liên quan thứ 3. Trong Rails, chúng ta có thể làm điều này với việc chỉ sử dụng một dấu chấm. Ví dụ, thay vì sử dụng @invoice.customer.name thì chúng ta sử dụng @invoice.customer_name. Chúng ta có thể viết lại code trên như dưới đây:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices

 def street
  address.street
 end

  def city
    address.city
  end

  def state
    address.state
  end

  def zip_code
    address.zip_code
  end
end

class Invoice < ActiveRecord::Base
  belongs_to :customer

  def customer_name
   customer.name
  end

  def customer_street
   customer.street
  end

  def customer_city
   customer.city
  end

  def customer_state
    customer.state
  end

  def customer_zip_code
    customer.zip_code
  end
end

Và ở ngoài view, chúng ta có:

<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>,
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>

Rõ ràng chúng ta thấy sẽ gọn hơn, tuy nhiên phương pháp này cũng có nhược điểm. Đó là có nhiều hàm rải rác trong các lớp, và nếu mọi thứ thay đổi, chúng ta lại phải cất công sửa chữa ở những chỗ liên quan, tuy có ít hơn so với cách dùng ban đầu. Ngoài ra, lớp invoice có những hàm mà chẳng liên quan gì tới đối tượng invoice cả. Và đây cũng là nhược điểm chung của Law of Demeter, nó cũng không có gì khác với ROR cả.

Rất may là, Rails đã có giải pháp cho vấn đề này, đó là dùng delegate. Viết lại chúng ta có như sau:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices
  delegate :street, :city, :state, :zip_code, :to => :address
end

class Invoice < ActiveRecord::Base
  belongs_to :customer
  delegate :name, :street :city, :state, :zip_code, :to => :customer,
    :prefix => true
end

Và trong view, tất nhiên chúng ta sẽ có:

<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>,
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>

Điều này thật tuyệt vời khi chúng ta có thể tuân theo Law of Demeter và model của chúng ta cũng không bị lộn xộn nữa.