Rails AntiPattern: Duplicate Code Duplication (p2)
Hôm nay mình sẽ tiếp tục bài viết Rails AntiPattern: Duplicate Code Duplication (p1) cùng với giải pháp tránh lặp code tiếp theo, đó là sử dụng metaprogramming. Metaprogramming là một công cụ tuyệt vời để tạo ra các đoạn code DRY bằng các ngôn ngữ rất năng động. Metaprogramming hiểu đơn giản là ...
Hôm nay mình sẽ tiếp tục bài viết Rails AntiPattern: Duplicate Code Duplication (p1) cùng với giải pháp tránh lặp code tiếp theo, đó là sử dụng metaprogramming.
Metaprogramming là một công cụ tuyệt vời để tạo ra các đoạn code DRY bằng các ngôn ngữ rất năng động. Metaprogramming hiểu đơn giản là "Code sinh ra code" nghĩa là mình viết một chương trình và chương trình này sẽ sinh ra, điều khiển các chương trình khác hoặc làm 1 phần công việc ở thời điểm biên dịch. Chính vì vậy, metaprogramming giúp source của chúng ta trở nên ngắn gọn hơn, giảm thiểu vấn đề duplicate, như các method có chức năng tương tự nhau trong source code (nhất là trong test), dễ dàng thay đổi cũng như chỉnh sửa, giúp hệ thống trở nên gọn nhẹ và mượt mà hơn.
Để minh họa việc sử dụng Metaprogramming để giữ ứng dụng DRY. Cùng xem ví dụ: Purchase model từ một ứng dụng của một cửa hàng. Ví dụ đầu tiên là test cho model:
class PurchaseTest < Test::Unit::TestCase context "Given some Purchases of each status" do setup do %w(in_progress submitted approved shipped received canceled).each do |s| Factory(:purchase, :status => s) end end context "Purchase.all_in_progress" do setup { @purchases = Purchase.all_in_progress } should "not be empty" do assert !@purchases.empty? end should "return only in progress purchases" do @purchases.each do |purchase| assert purchase.in_progress? end end should "return all in progress purchases" do expected = Purchase.all.select(&:in_progress?) assert_same_elements expected, @purchases end end end
Một Purchase object có thể có nhiều trạng thái: in_progress, submitted, approved, shipped, received, and canceled. Nó cũng định nghĩa các class method để tìm các đối tượng Purchase của 1 trạng thái cụ thể.
Việc thực hiện chính xác của Purchase model sẽ là khai báo từng cặp phương thức cho mọi trạng thái (ví dụ, các phương thức all_submitted và submitted?) bằng tay:
class Purchase < ActiveRecord::Base validates_presence_of: status validates_inclusion_of: status, : in => %w(in_progress submitted approved shipped received canceled) # Status Finders def self.all_in_progress where(: status => "in_progress") end def self.all_submitted where(: status => "submitted") end def self.all_approved where(: status => "approved") end def self.all_shipped where(: status => "shipped") end def self.all_received where(: status => "received") end def self.all_canceled where(: status => "canceled") end # Status Accessors def in_progress? status == "in_progress" end def submitted? status == "submitted" end def approved? status == "approved" end def shipped? status == "shipped" end def received? status == "received" end def canceled? status == "canceled" end end
Rõ ràng đoạn code này có vấn đề bảo trì trong quá trình phát triển. Vấn đề thực sự ở đây là đoạn code chưa DRY. Giả sử Purchase model của chúng ta có thêm status thì chúng ta sẽ phải sửa ở 3 chỗ validate, accessor, finder. Bạn nên để status ở 1 nơi duy nhất và bạn có thể thực hiện nó với metaprogramming:
class Purchase < ActiveRecord::Base STATUSES = %w(in_progress submitted approved shipped received) validates_presence_of: status validates_inclusion_of: status, : in => STATUSES # Status Finders class << self STATUSES.each do |status_name | define_method "all_#{status_name}" where(: status => status_name) end end end # Status Accessors STATUSES.each do |status_name | define_method "#{status_name}?" status == status_name end end end
Vấn đề mà hầu hết các lập trình viên Ruby mới bắt đầu găp phải với metaprogramming là sự phức tạp được giới thiệu; đây là một điều hoàn toàn đúng. Trước khi bắt đầu, hãy xem qua việc triển khai này và thảo luận một chút về những gì đang diễn ra.
Khía cạnh quan trọng nhất của đoạn code này là danh sách các trạng thái được giữ rõ ràng trong mảng Purchase :: STATUSES. Đây là vị trí độc nhất, có thẩm quyền của danh sách, và việc thay đổi danh sách sẽ ngay lập tức thay đổi code khi cần thiết.
DRY không chỉ là về các dòng code. Mình tin rằng code ít hơn sẽ là code tốt hơn và mình sẽ tổ chức lại đoạn code trên bằng cách sử dụng scope:
class Purchase < ActiveRecord::Base validates :status, :presence => true, :inclusion => { :in => %w(in_progress submitted approved shipped received canceled) } # Status Finders scope :all_in_progress, where(:status => "in_progress") scope :all_submitted, where(:status => "submitted") scope :all_approved, where(:status => "approved") scope :all_shipped, where(:status => "shipped") scope :all_received, where(:status => "received") scope :all_canceled, where(:status => "canceled") # Status Accessors def in_progress? status == "in_progress" end def submitted? status == "submitted" end def approved? status == "approved" end def shipped? status == "shipped" end def received? status == "received" end def canceled? status == "canceled" end end
Ví dụ về metaprogramming trước có thể nhiều bạn sẽ gặp phải khó khăn khi đọc code và điều này có thể giải quyết bằng cách đặt code vào 1 macro:
# lib/extensions/statuses.rb class ActiveRecord::Base def self.has_statuses(*status_names) validates :status, :presence => true, :inclusion => { :in => status_names } # Status Finders status_names.each do |status_name| scope "all_#{status_name}", where(:status => status_name) end # Status Accessors status_names.each do |status_name| define_method "#{status_name}?" do status == status_name end end end end class Purchase < ActiveRecord::Base has_statuses :in_progress, :submitted, :approved, :shipped, :received end