Thông thường, global variable là xấu. Nhưng đôi khi, global ở đúng chỗ làm code đơn giản hơn nhiều.
Trong Rails, bạn có thể set data một lần trong request, nhưng dùng ở mọi layer của app. User nào đang request? Permission gì? Database connection nào?
Thông tin này nên khả dụng khắp code, nên truyền khắp nơi chỉ tạo ra noise. Nhưng dùng Ruby global variable, hoặc class variable, sẽ gây vấn đề riêng. Multiple thread có thể ghi đè, dẫn đến hỗn loạn, và có thể data hỏng không phục hồi.
Bạn cần thứ global, nhưng chỉ cho request của bạn.
Cách Ruby thuần
Thường thấy xử lý bằng Thread.current[]. Trông như sau:
Thread.current[:current_user] = user
Đủ đơn giản. Giải pháp ổn. Nhưng có hai nhược điểm lớn:
-
Ai đó có thể vô tình ghi đè data của bạn.
Nếu hai người chọn cùng key? Bạn đạp lên data của nhau. Nếu lưu user, sẽ rất tệ. Trong app riêng, có lẽ không vấn đề. Nhưng nếu viết gem, hoặc Rails app lớn và rối, cần nghĩ đến.
-
Không có cấu trúc tốt.
Thread.current[]chỉ là túi data lớn. Không dễ document. Muốn biết có thể lấy gì, phải tìm cái bạn để vào.Tất nhiên, nếu bạn để đủ nhiều global data đến nỗi đây thành vấn đề, bạn có việc lo lắng hơn thiếu cấu trúc của
Thread.current[]. Nhưng vẫn đáng nhớ.
Vậy có giải pháp tốt hơn Thread.current[] không? Như bạn tưởng tượng, Rails tự quản lý nhiều data request-local. Và hộp đồ chơi ActiveSupport có pattern khác.
Cách Rails
Nếu đào ActiveSupport, bạn có thể gặp ActiveSupport::PerThreadRegistry. Từ tên đã biết đây chính là thứ chúng ta cần.
Với ActiveSupport::PerThreadRegistry, ví dụ trước trông như:
class RequestRegistry
extend ActiveSupport::PerThreadRegistry
attr_accessor :current_user, :current_permissions
end
RequestRegistry.current_user = user
RequestRegistry.current_user # => user
Hơi tốn công hơn chút. Nhưng với ActiveSupport::PerThreadRegistry:
-
Bạn có nơi đặt documentation. Ai nhìn class sẽ biết chính xác global data bạn kỳ vọng, và global data họ có thể dùng.
-
API trông đẹp. Thực tế, trông như bạn set class variable. Đó chính là điều bạn làm nếu không lo thread.
-
Data set trên class sẽ được namespace. Không lo
RequestRegistry.current_userva chạmPhoneNumberApiRegistry.current_user, chúng tách biệt hoàn toàn.
Rails dùng ActiveSupport::PerThreadRegistry nội bộ cho ActiveRecord connection handling, lưu EXPLAIN query, ActiveSupport::Notifications. Nếu muốn thêm ví dụ hay về sức mạnh PerThreadRegistry, chính Rails là nơi tốt để bắt đầu.
Khi thread và request không khớp
Với một số Rails server, một thread xử lý nhiều request. Điều này nghĩa là gì bạn để vào PerThreadRegistry sẽ dính request sau. Có lẽ không phải điều bạn muốn.
Bạn có thể làm như Rails, dọn dẹp thứ trong PerThreadRegistry sau khi xong. Tùy vào gì bạn để, có thể bạn đang làm vậy rồi.
Trong comment, MattB chỉ ra giải pháp dễ hơn: request_store. Nó hoạt động như Thread.current[], nhưng tự clear sau mỗi request.
Điều này mở cửa cho ai đó tạo PerRequestRegistry, kết hợp an toàn của request_store với API của PerThreadRegistry. Nếu ai đã làm, tôi rất muốn nghe!
Thử ngay
Global data có thể xấu. Quá nhiều global nhanh dẫn đến code unmaintainable.
Nhưng nếu hợp lý giữ data ở nơi trung tâm cho cả request, hãy vượt ra khỏi Thread.current[]. Thử ActiveSupport::PerThreadRegistry hoặc request_store. Ít nhất, nó làm việc với global data ít rủi ro hơn.