Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Rails Concerns: Hướng Dẫn Rõ Ràng Về Module Hiệu Quả

Nếu bạn từng dùng Ruby on Rails, bạn chắc chắn gặp khái niệm concerns. Khi khởi tạo project Rails mới, bạn có thư mục app/controllers/concerns và app/models/concerns. Nhưng concerns là gì? Và tại sao cộng đồng Rails đôi khi nói xấu về chúng?

Tổng Quan Nhanh

Một Rails Concern là bất kỳ module nào extend ActiveSupport::Concern module. Bạn có thể hỏi — concerns khác modules như thế nào? Khác biệt chính là Rails concerns cho phép bạn làm chút "phép màu", như sau:

# app/models/concerns/trashable.rb
module Trashable
  extend ActiveSupport::Concern
  included do
    scope :existing, -> { where(trashed: false) }
    scope :trashed, -> { where(trashed: true) }
  end
  def trash
    update_attribute :trashed, true
  end
end

Bạn thấy từ khóa included. Đó là chút "carbohydrates" của Rails rắc lên Ruby module. ActiveSupport::Concern cho phép bạn đặt code muốn evaluated bên trong block included. Ví dụ, muốn extract trashing logic ra khỏi model. included cho phép làm như trên và sau đó include concern vào model như sau:

class Song < ApplicationRecord
  include Trashable
  has_many :authors
  # ...
end

Rất tiện và naive ở điểm này, phải không? Model giảm cân và trashing giờ tái sử dụng across các models, không chỉ Song. Nhưng mọi thứ có thể phức tạp. Hãy đi sâu tìm hiểu.

Ví Dụ Kinh Điển Của Mixin

Trước khi đi sâu vào concerns, thêm giải thích. Khi thấy include SomeModule hoặc extend AnotherModule, đây gọi là mixins. Mixin là tập code có thể thêm vào các classes khác. Và như chúng ta biết từ Ruby docs, module là collection của methods và constants. Vậy chúng ta đang include modules với methods và constants vào các classes khác để chúng dùng.

Đó chính xác những gì làm với concern Trashable. Extract common logic quanh trashing model object vào module. Module này sau đó include ở nơi khác. Vì vậy, mixin là design pattern không chỉ dùng trong Ruby và Rails. Nhưng ở đâu dùng, người ta hoặc thích và nghĩ tốt, hoặc ghét và nghĩ nó dễ spin out of control.

Để hiểu rõ hơn, đi qua một vài pros và cons của việc dùng chúng. Hy vọng qua đó hiểu khi nào hoặc có nên dùng concerns.

Tôi Có Tất Cả

Khi quyết định extract gì đó vào concern, như Trashable, bạn giờ có quyền truy cập tất cả functionality của nơi Trashable được include. Điều này mang lại quyền lực lớn, nhưng như Richard Schneeman nói trong bài blog về chủ đề này — "with great power comes great ability to make complicated code". Ý ông là complicating code bạn rely on, thứ nên có trong concerns.

Nhìn lại Trashable:

module Trashable
  extend ActiveSupport::Concern
  included do
    scope :existing, -> { where(trashed: false) }
    scope :trashed, -> { where(trashed: true) }
  end
  def trash
    update_attribute :trashed, true
  end
end

Logic concern rely trên việc field trashed tồn tại nơi concern được include. Đúng không? Không vấn đề, đây là thứ chúng ta muốn. Nhưng, tôi thấy mọi người tempted kéo stuff khác từ model vào concern. Để minh họa cách này xảy ra, hãy tưởng tượng model Song có method featured_authors:

class Song < ApplicationRecord
  include Trashable
  has_many :authors
  def featured_authors
    authors.where(featured: true)
  end
  # ...
end
class Album < ApplicationRecord
  include Trashable
  has_many :authors
  def featured_authors
    authors.where(featured: true)
  end
  # ...
end

Để minh họa rõ hơn, thêm model Album cũng include Trashable. Giả sử muốn notify featured authors của song và album khi bị trash. Mọi người sẽ tempted đặt logic này trong concern như sau:

module Trashable
  extend ActiveSupport::Concern
  included do
    scope :existing, -> { where(trashed: false) }
    scope :trashed, -> { where(trashed: true) }
  end
  def trash
    update_attribute :trashed, true
    notify(featured_authors)
  end
  def notify(authors)
    # ...
  end
end

Ở đây, mọi thứ bắt đầu phức tạp. Vì logic trashing ngoài Song model, tempted đặt notify trong Trashable. Trong đó, điều "sai" xảy ra. featured_authors lấy từ model Song. OK, giả sử pass pull request review và CI checks.

Sau đó, vài tháng sau, requirement mới yêu cầu đổi cách present featured_authors cho songs. Ví dụ, chỉ show featured authors từ Europe. Tự nhiên, developer tìm nơi featured authors định nghĩa và sửa.

class Song < ApplicationRecord
  include Trashable
  has_many :authors
  def featured_authors
    authors.where(featured: true).where(region: 'Europe')
  end
  # ...
end
class Album < ApplicationRecord
  include Trashable
  has_many :authors
  # ...
end

Này hoạt động tốt nơi show authors, nhưng sau deploy production, người từ các nơi khác không còn được notify về bài hát của họ. Sai lầm như này dễ mắc khi dùng concerns. Ví dụ trên đơn giản và nhân tạo, nhưng những "in the wild" có thể super tricky.

Rủi ro ở đây là concern (mixin) biết nhiều về model nó được include vào. Được gọi là circular dependency. Song và Album depend trên Trashable cho trashing, Trashable depend trên cả hai cho định nghĩa featured_authors. Tương tự cho việc field trashed cần tồn tại trong cả hai models để Trashable hoạt động.

Đây là lý do no-concern club chống lại, và pro-concern club ủng hộ. Tôi nói, phiên bản đầu tiên của Trashable là cái tôi dùng trong codebase. Hãy xem làm sao version hai với notify tốt hơn.

Các Bạn Đến Từ Đâu

Nhìn lại Trashable với notify, phải làm gì đó. Điều khác xảy ra khi dùng concerns là tend to over-DRY things. Hãy thử làm đó, cho demo, với models hiện tại bằng cách tạo concern khác (bear with me):

module Authorable
  has_many :authors
  def featured_authors
    authors.where(featured: true)
  end
end

Sau đó Song và Album trông như sau:

class Song < ApplicationRecord
  include Trashable
  include Authorable
  # ...
end
class Album < ApplicationRecord
  include Trashable
  include Authorable
  # ...
end

Đã DRY hết, nhưng requirement featured authors từ Europe không được fulfill. Làm tệ hơn, giờ Trashable concern và models depend trên Authorable. What the hell? Chính câu hỏi của tôi khi deal với concerns trước đây. Khó track down methods từ đâu đến.

Giải pháp của tôi: giữ featured_authors gần models nhất có thể. Method notify không nên là phần của Trashable concern. Mỗi model tự lo notify, nhất là nếu notify subgroups khác nhau. Xem cách làm ít đau đớn hơn:

# Concerns
module Trashable
  extend ActiveSupport::Concern
  included do
    scope :existing, -> { where(trashed: false) }
    scope :trashed, -> { where(trashed: true) }
  end
  def trash
    update_attribute :trashed, true
  end
end

module Authorable
  has_many :authors
  # Other useful methods relate to authors across models.
  # If there are none, ditch the concern.
end

# Models
class Song < ApplicationRecord
  include Trashable
  include Authorable
  def featured_authors
    authors.where(featured: true).where(region: 'Europe')
  end
  # ...
end

class Album < ApplicationRecord
  include Trashable
  include Authorable
  def featured_authors
    authors.where(featured: true)
  end
  # ...
end

Concerns như này manageable và không quá complex. Bỏ qua functionality notify mô tả trước vì đó chủ đề cho ngày khác.

The Final Boss

Đối với Basecamp, Rails creators, concerns referencing other concerns dường như hoàn toàn ổn như DHH illustrate trong tweet trước đây:

Nhìn code screenshot, bạn hoặc mở miệng ngạc nhiên hoặc sốc. Tôi cảm thấy không có middle ground. Nếu có chance edit code này, tôi envision nó như "Final Concern Boss Fight". Nhưng broma riêng, điều thú vị là có comments nói concern nào depend trên concern nào. Nhìn:

# ...
include Subscribable # Depends on Readable
include Eventable # Depends on Recordables
# ...

Đặt comments như này helpful, nhưng vẫn setup cho việc sketchy, nhất là nếu bạn new to codebase. Being new và không aware tất cả "gotchas" code có chắc chắn gửi bạn xuống concern downward spiral.

Cái kiểu này là gì DHH share trong comment trong discussion. Response tweet hỏi làm sao folks làm việc với codebase này interact với concerns như này. DHH respond rằng họ không có nhiều written docs, rarely hire nên team well acquainted với những này.

Nhưng có experienced team biết codebase tốt làm argument cho dùng concerns là weird và không mạnh. Tôi đoán đó là feeling whether dùng chúng hay không. Bạn comfortable với multiple inheritances modules cung cấp, hay prefer composition? Up to you.

Kết Luận

Như đã thấy, concerns chỉ là modules cung cấp syntax sugar hữu ích để extract và DRY code. Nếu bạn có useful tools hơn dưới belt, maybe không nên reach out concerns ngay. Behavior như handling file attachments và trashing logic trong ví dụ có thể là good candidates extract vào modules (concerns).

Hy vọng bạn thấy được good và bad things khi deal với concerns và modules nói chung. Nhớ rằng no code is perfect. Và cuối cùng, làm sao bạn learn good và bad cho bạn nếu không try và possibly fail hoặc succeed?

No solution is perfect, và tôi hy vọng bạn hiểu Rails concerns way of doing things trong bài viết. Như lúc nào, dùng judgment của bạn và aware pros và cons.

Hẹn gặp lại, cheers!

P.S. Nếu muốn đọc bài Ruby Magic ngay khi xuất bản, đăng ký newsletter Ruby Magic để không bỏ lỡ!