Trong bài trước, tôi nhắc đến tính năng mới Rails 5.1: delegate_missing_to. Với delegate_missing_to, method không tìm thấy trên object này sẽ được gọi trên object kia:
class Player
delegate_missing_to :@user
def initialize(user)
@user = user
end
def points
Game.points_for_user(user.id)
end
end
Player.new(user).name # gọi user.name
Như Gavin bình luận: đây có vẻ cách lạ để tránh inheritance. Tại sao không dùng subclass? Có hiệu quả tương tự, không cần feature mới. Có vẻ lạ.
Nhưng phải có lý do delegate_missing_to được thêm. Với Rails features, pull request là nơi tìm lý do. Trong PR này, DHH giải thích tại sao đề xuất feature:
Đây là pattern phổ biến khi muốn build decorator:
Đó là điểm khởi đầu tốt để đào sâu.
Tại sao dùng Decorator?
Khi build decorator, bạn thay đổi cách object hoạt động, mà không tạo subclass mới. Ví dụ code trước:
class Player
delegate_missing_to :@user
def initialize(user)
@user = user
end
def points
Game.points_for_user(user.id)
end
end
Bạn nói "Player decorates user", vì Player gần như hành động như User, nhưng có method thêm points. Và làm được điều này không dùng inheritance.
Tại sao cần cái này? Câu hỏi khó, vì như nhiều design pattern, không luôn rõ chỗ nào dùng decorator thay vì cách khác.
Khi nào dùng Decorator?
Decorator có thể chỉ là cách phức tạp hơn để làm inheritance. So sánh hai dòng:
player = Player.new(User.new(name: "Justin")) # Player decorates User
player = Player.new(name: "Justin") # Player subclasses User
Dòng 2 rõ ràng tốt hơn, đúng không? Ở đây decorator chỉ lãng phí code.
Nhưng đôi khi bạn muốn thêm chức năng vào object sau này, ở nơi xa nơi object được tạo. Ví dụ code như sau:
user = User.find(1)
... một lúc sau ...
player = Player.new(user)
Giờ bạn tạo user như thích, ở method nào cũng được. Code tạo User không biết hoặc không quan tâm class Player tồn tại. Vẫn dùng được User gốc nếu không muốn method thêm.
Điều này giúp tách behavior vào các class riêng biệt. Mỗi class focus vào cách User object được dùng trong tình huống cụ thể - Player, Employee, Developer. Với inheritance, mọi thứ dễ bị lẫn lộn.
MrChris nhắc thêm benefit của decorator trong comment:
Khi decorate object, chỉ gọi được public method trên object đó. Khi subclass, có thể gọi bất kỳ method nào, kể cả private. Điều này khiến subclass dễ break hơn, vì có thể vô tình phụ thuộc vào implementation detail của parent. Những detail đó thường thay đổi nhiều hơn public method.
Decorator đặc biệt hữu ích khi tách class lớn. Với decorator, dễ tuân thủ Single-Responsibility Principle - mỗi decorator làm một việc và làm tốt, có thể combine decorator để có behavior phức tạp hơn.
Ruby có nhiều cách share behavior: subclass, include module, thậm chí grab method từ class này gắn vào class kia. Decorator pattern cho điều khác một chút. Chỉ dùng instance variable và method call - building block của bất kỳ OO language nào. Từ những cơ sở đó, có behavior linh hoạt khi runtime - không làm phức tạp code.