Memoization là kỹ thuật bạn có thể dùng để tăng tốc method accessor. Nó cache kết quả của những method làm việc tốn thời gian, việc chỉ cần làm một lần. Trong Rails, memoization được dùng thường xuyên đến mức Rails từng có một module tự động memoize method cho bạn.
Sau đó, điều này bị gỡ bỏ gây tranh cãi, ủng hộ pattern memoization phổ biến mà tôi sẽ nói đầu tiên. Nhưng như bạn sẽ thấy, có những nơi pattern cơ bản này hoàn toàn không hoạt động đúng. Vì vậy chúng ta cũng sẽ xem các pattern memoization nâng cao, và học điều thú vị về Ruby trong quá trình đó!
Memoization siêu cơ bản
Bạn sẽ thấy pattern memoization này khắp nơi trong Ruby:
class User < ActiveRecord::Base def twitter_followers # assuming twitter_user.followers makes a network call @twitter_followers ||= twitter_user.followers end end ||= ít nhiều dịch thành @twitter_followers = @twitter_followers || twitter_user.followers. Nghĩa là bạn chỉ thực hiện network call lần đầu gọi twitter_followers, các lần sau chỉ trả về giá trị instance variable @twitter_followers.
Memoization nhiều dòng
Đôi khi, code chậm không vừa một dòng nếu không làm nó trở nên tồi tệ. Có vài cách mở rộng pattern cơ bản để hoạt động với nhiều dòng code, nhưng đây là cách tôi thích nhất:
class User < ActiveRecord::Base def main_address @main_address ||= begin maybe_main_address = home_address if prefers_home_address? maybe_main_address = work_address unless maybe_main_address maybe_main_address = addresses.first unless maybe_main_address end end end begin...end tạo một khối code trong Ruby có thể được xem như một thứ đơn lẻ, giống như {...} trong ngôn ngữ kiểu C. Đó là lý do ||= hoạt động tốt như trước.
Vậy nil thì sao?
Nhưng các pattern memoization này có một vấn đề ngầm nguy hiểm. Trong ví dụ đầu, nếu user không có twitter account, và API twitter followers trả về nil? Trong ví dụ thứ hai, nếu user không có address nào, và khối code trả về nil?
Mỗi lần gọi method, instance variable sẽ là nil, và chúng ta thực hiện lại các lần fetch tốn kém.
Vì vậy, ||= có lẽ không phải cách đúng. Thay vào đó, chúng ta phải phân biệt giữa nil và undefined:
class User < ActiveRecord::Base def twitter_followers return @twitter_followers if defined? @twitter_followers @twitter_followers = twitter_user.followers end end class User < ActiveRecord::Base def main_address return @main_address if defined? @main_address @main_address = begin main_address = home_address if prefers_home_address? main_address ||= work_address main_address ||= addresses.first # some semi-sensible default end end end Thật không may, cách này hơi xấu, nhưng hoạt động với nil, false, và mọi thứ khác. (Để xử lý trường hợp nil, bạn cũng có thể dùng Null Objects và mảng rỗng để tránh vấn đề này. Thêm một lý do để tránh nil!)
Và tham số thì sao?
Chúng ta có pattern memoization hoạt động tốt cho accessor đơn giản. Nhưng nếu bạn muốn memoize method nhận tham số, như ví dụ này?
class City < ActiveRecord::Base def self.top_cities(order_by) where(top_city: true).order(order_by).to_a end end Thực ra, Hash của Ruby có initializer hoạt động hoàn hảo cho tình huống này. Bạn có thể gọi Hash.new với một block:
Hash.new {|h, key| h[key] = some_calculated_value } Sau đó, mỗi lần bạn truy cập key chưa được set trong hash, nó sẽ thực thi block. Và nó truyền chính hash đó cùng với key bạn truy cập vào block.
Vì vậy, nếu bạn muốn memoize method này, bạn có thể làm như sau:
class City < ActiveRecord::Base def self.top_cities(order_by) @top_cities ||= Hash.new do |h, key| h[key] = where(top_city: true).order(key).to_a end @top_cities[order_by] end end Và bất kể bạn truyền gì vào order_by, kết quả đúng sẽ được memoize. Vì block chỉ được gọi khi key không tồn tại, bạn không phải lo về kết quả block là nil hay false.
Ngạc nhiên thay, Hash hoạt động tốt với key là mảng:
h = {} h[["a", "b"]] = "c" h[["a", "b"]] # => "c" Vì vậy bạn có thể dùng pattern này cho method với bất kỳ số lượng tham số nào!
Tại sao phải khổ công như vậy?
Tất nhiên, nếu bạn thêm các pattern memoization này vào nhiều method, code sẽ trở nên khó đọc rất nhanh. Method của bạn sẽ toàn ritual chứ không có substance.
Vì vậy nếu bạn làm việc trên app cần nhiều memoization, bạn có thể muốn dùng gem xử lý memoization cho bạn với API thân thiện. Memoist có vẻ là một lựa chọn tốt, và khá giống với thứ Rails từng có. (Hoặc, với kiến thức memoization mới, bạn thậm chí có thể tự xây dựng một cái).
Nhưng luôn thú vị khi khám phá các pattern như này, xem chúng được xây dựng như thế nào, nơi chúng hoạt động, và nơi có cạnh sắc. Và bạn có thể học được điều thú vị về một số tính năng Ruby ít người biết khi khám phá.