Trong khám phá hôm nay về những mỏ ngọc ẩn trong thư viện chuẩn của Ruby, chúng ta sẽ nhìn vào delegation (ủy quyền).
Thật tiếc, thuật ngữ này — giống như nhiều thuật ngữ khác — đã trở nên hơi mờ nhạt qua nhiều năm và mang ý nghĩa khác nhau đối với những người khác nhau. Theo Wikipedia:
Delegation (ủy quyền) đề cập đến việc đánh giá một thành viên (thuộc tính hoặc phương thức) của một object (receiver) trong ngữ cảnh của một object gốc khác (sender). Delegation có thể được thực hiện tường minh, bằng cách truyền object gửi cho object nhận, điều này có thể làm được trong bất kỳ ngôn ngữ hướng đối tượng nào; hoặc ngầm hiểu, bởi các quy tắc tra cứu thành viên của ngôn ngữ, đòi hỏi hỗ trợ của ngôn ngữ cho tính năng này.
Tuy nhiên, thường xuyên hơn, mọi người cũng dùng thuật ngữ này để mô tả một object gọi phương thức tương ứng của một object khác mà không truyền chính nó làm đối số, điều này chính xác hơn có thể được gọi là "forwarding" (chuyển tiếp).
Với việc làm rõ đó, chúng ta sẽ dùng "delegation" để mô tả cả hai pattern này trong phần còn lại của bài viết.
Delegator
Hãy bắt đầu khám phá delegation trong Ruby bằng cách nhìn vào lớp Delegator của thư viện chuẩn, cung cấp một số pattern delegation.
SimpleDelegator
Pattern dễ nhất, và cũng là pattern tôi gặp nhiều nhất trong thực tế, là SimpleDelegator, nó wrap một object được cung cấp qua initializer và sau đó delegate tất cả các phương thức bị thiếu cho nó. Hãy xem nó hoạt động như thế nào:
require 'delegate' User = Struct.new(:first_name, :last_name) class UserDecorator < SimpleDelegator def full_name "#{first_name} #{last_name}" end endĐầu tiên, chúng ta cần require 'delegate' để làm cho SimpleDelegator khả dụng cho code của chúng ta. Chúng ta cũng dùng Struct để tạo một lớp User đơn giản với các accessor first_name và last_name. Sau đó chúng ta thêm UserDecorator định nghĩa phương thức full_name kết hợp các phần tên riêng thành một chuỗi đơn. Đây là nơi SimpleDelegator phát huy tác dụng: vì không có first_name hay last_name nào được định nghĩa trên lớp hiện tại, chúng sẽ được gọi trên object được wrap:
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"SimpleDelegator cũng cho phép chúng ta ghi đè các phương thức được delegate với super, gọi phương thức tương ứng trên object được wrap. Chúng ta có thể dùng điều này trong ví dụ của mình để chỉ hiển thị chữ cái đầu tiên thay vì full first name:
class UserDecorator < SimpleDelegator def first_name "#{super[0]}." end enddecorated_user.first_name #=> "J." decorated_user.full_name #=> "J. Doe"Delegator
Khi đọc các ví dụ trên, bạn có thắc mắc UserDecorator của chúng ta biết delegate cho object nào không? Câu trả lời nằm trong lớp cha của SimpleDelegator — Delegator. Đây là một lớp cơ sở trừu tượng để định nghĩa các scheme delegation tùy chỉnh bằng cách cung cấp triển khai cho __getobj__ và __setobj__ để lấy và thiết lập delegation target tương ứng. Sử dụng kiến thức này, chúng ta dễ dàng xây dựng phiên bản riêng của SimpleDelegator cho mục đích demo:
class MyDelegator < Delegator attr_accessor :wrapped alias_method :__getobj__, :wrapped def initialize(obj) @wrapped = obj end end class UserDecorator < MyDelegator def full_name "#{first_name} #{last_name}" end endĐiều này khác một chút so với triển khai thực của SimpleDelegator mà gọi __setobj__ trong phương thức initialize của nó. Vì lớp delegator tùy chỉnh của chúng ta không cần nó, chúng ta hoàn toàn bỏ qua phương thức đó.
Điều này nên hoạt động chính xác như ví dụ trước; và thực sự nó hoạt động:
UserDecorator.superclass #=> MyDelegator < Delegator decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"DelegateMethod
Pattern delegation cuối cùng Delegate cung cấp cho chúng ta là phương thức Object.DelegateClass có tên hơi lạ. Phương thức này sinh ra và trả về một lớp delegator cho một lớp cụ thể, mà chúng ta sau đó có thể kế thừa từ nó:
class MyClass < DelegateClass(ClassToDelegateTo) def initialize super(obj_of_ClassToDelegateTo) end endMặc dù điều này có thể trông gây nhầm lẫn lúc đầu — đặc biệt là việc phía bên phải của kế thừa có thể chứa code Ruby tùy ý — nhưng nó thực tế tuân theo các pattern chúng ta đã khám phá trước đó, tức là nó tương tự như việc kế thừa từ SimpleDelegator.
Thư viện chuẩn của Ruby sử dụng tính năng này để định nghĩa lớp Tempfile delegate phần lớn công việc cho lớp File trong khi thiết lập một số quy tắc đặc biệt về vị trí lưu trữ và xóa file. Chúng ta có thể dùng cùng cơ chế để thiết lập một lớp Logfile tùy chỉnh như sau:
class Logfile < DelegateClass(File) MODE = File::WRONLY|File::CREAT|File::APPEND def initialize(basename, logdir = '/var/log') # Tạo logfile tại vị trí do logdir chỉ định path = File.join(logdir, basename) logfile = File.open(path, MODE, 0644) # Điều này sẽ gọi phương thức initialize của Delegator, nên từ điểm này trở đi # chúng ta có thể gọi bất kỳ phương thức nào từ File trên instance Logfile. super(logfile) end endForwardable
Thú vị thay, thư viện chuẩn của Ruby cung cấp cho chúng ta một thư viện khác cho delegation dưới dạng module Forwardable và các phương thức def_delegator và def_delegators của nó.
Hãy viết lại ví dụ UserDecorator ban đầu của chúng ta với Forwardable.
require 'forwardable' User = Struct.new(:first_name, :last_name) class UserDecorator extend Forwardable def_delegators :@user, :first_name, :last_name def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"Sự khác biệt dễ nhận thấy nhất là delegation không được cung cấp tự động qua method_missing, mà thay vào đó, cần được khai báo tường minh cho mỗi phương thức chúng ta muốn forward. Điều này cho phép chúng ta "ẩn" bất kỳ phương thức nào của object được wrap mà chúng ta không muốn expose cho client, giúp chúng ta kiểm soát tốt hơn public interface và là lý do chính tôi thường ưu tiên Forwardable hơn SimpleDelegator.
Một tính năng hay khác của Forwardable là khả năng đổi tên các phương thức được delegate qua def_delegator, chấp nhận một đối số thứ ba tùy chọn chỉ định alias mong muốn:
class UserDecorator extend Forwardable def_delegator :@user, :first_name, :personal_name def_delegator :@user, :last_name, :family_name def initialize(user) @user = user end def full_name "#{personal_name} #{family_name}" end endUserDecorator trên chỉ expose các phương thức đã alias là personal_name và family_name, trong khi vẫn forward đến first_name và last_name của object User được wrap:
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.first_name #=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8> decorated_user.personal_name #=> "John"Tính năng này có thể khá hữu íchsometimes. Tôi đã dùng nó thành công trong quá khứ cho các việc như migrate code giữa các thư viện có interface tương tự nhưng kỳ vọng khác nhau về tên phương thức.
Ngoài Thư viện Chuẩn
Mặc dù các giải pháp delegation hiện có trong thư viện chuẩn, cộng đồng Ruby đã phát triển một số lựa chọn thay thế qua nhiều năm và chúng ta sẽ khám phá hai trong số chúng tiếp theo.
delegate (Rails)
Cân nhắc đến sự phổ biến của Rails, phương thức delegate của nó có thể là hình thức delegation phổ biến nhất được dùng bởi các Ruby developer. Đây là cách chúng ta có thể dùng nó để viết lại UserDecorator đáng tin cậy của chúng ta:
# Trong một app Rails thực tế, điều này thường là subclass của ApplicationRecord User = Struct.new(:first_name, :last_name) class UserDecorator attr_reader :user delegate :first_name, :last_name, to: :user def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"Điều này khá giống với Forwardable, nhưng chúng ta không cần dùng extend vì delegate được định nghĩa trực tiếp trên Module và do đó khả dụng trong mọi thân lớp hoặc module (tốt hay xấu, bạn tự quyết). Tuy nhiên, delegate có vài chiêu bài thú vị. Đầu tiên, có option :prefix sẽ thêm prefix tên object chúng ta delegate vào tên phương thức được delegate. Vì vậy,
delegate :first_name, :last_name, to: :user, prefix: truesẽ sinh ra phương thức user_first_name và user_last_name. Hoặc chúng ta có thể cung cấp một custom prefix:
delegate :first_name, :last_name, to: :user, prefix: :account Bây giờ chúng ta có thể truy cập các phần khác nhau của tên user như account_first_name và account_last_name.
Một option thú vị khác của delegate là option :allow_nil. Nếu object chúng ta delegate hiện là nil — ví dụ vì một quan hệ ActiveRecord chưa được set — chúng ta thường sẽ kết thúc với NoMethodError:
decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nilTuy nhiên, với option :allow_nil, lời gọi này sẽ thành công và trả về nil thay vì lỗi:
class UserDecorator delegate :first_name, :last_name, to: :user, allow_nil: true ... end decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> nilCasting
Lựa chọn delegation cuối cùng chúng ta sẽ xem là gem Casting của Jim Gay, cho phép developer "delegate methods in Ruby and preserve self" (delegate phương thức trong Ruby và bảo toàn self). Đây có lẽ là gần nhất với định nghĩa chặt chẽ của delegation, vì nó dùng bản chất động của Ruby để tạm thời rebind receiver của một lời gọi phương thức, tương tự như:
UserDecorator.instance_method(:full_name).bind(user).call #=> "John Doe"Điều thú vị nhất ở đây là developer có thể thêm hành vi cho object, mà không thay đổi hệ thống kế thừa (superclass hierarchies) của chúng.
require 'casting' User = Struct.new(:first_name, :last_name) module UserDecorator def full_name "#{first_name} #{last_name}" end end user = User.new("John", "Doe") user.extend(Casting::Client) user.delegate(:full_name, UserDecorator)Ở đây chúng ta extend user với Casting::Client, cho chúng ta truy cập phương thức delegate. Thay vào đó, chúng ta có thể dùng include Casting::Client bên trong lớp User để cung cấp khả năng này cho tất cả instances.
Ngoài ra, Casting cung cấp các option để tạm thời thêm hành vi cho vòng đời của một block hoặc cho đến khi thủ công gỡ bỏ. Để điều này hoạt động, chúng ta trước hết cần enable delegation của missing methods:
user.delegate_missing_methodsĐể thêm hành vi trong khoảng thời gian của một block đơn, chúng ta có thể dùng phương thức lớp delegating của Casting:
Casting.delegating(user => UserDecorator) do user.full_name #=> "John Doe" end user.full_name #NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">Thay vào đó, chúng ta có thể thêm hành vi cho đến khi chúng ta gọi rõ ràng uncast:
user.cast_as(UserDecorator) user.full_name #=> "John Doe" user.uncast NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">Mặc dù phức tạp hơn một chút so với các giải pháp khác, Casting cung cấp rất nhiều sự kiểm soát và Jim minh họa các trường hợp sử dụng khác nhau và nhiều hơn trong cuốn sách Clean Ruby của ông.
Tóm tắt
Delegation và method forwarding là các pattern hữu ích để chia trách nhiệm giữa các object liên quan. Trong các project Ruby thuần túy, cả Delegator và Forwardable đều có thể dùng, trong khi code Rails thường thiên về phương thức delegate của nó. Để có sự kiểm soát tối đa về cái gì được delegate, gem Casting là một lựa chọn tuyệt vời, dù nó hơi phức tạp hơn các giải pháp khác.
Tác giả khách Michael Kohl yêu Ruby từ khoảng năm 2003. Ông cũng thích viết và nói về ngôn ngữ này và co-organize Bangkok.rb và RubyConf Thailand.