Trong chuyến khám phá hôm nay về những viên ngọc ẩn trong thư viện tiêu chuẩn của Ruby, chúng ta sẽ xem xét sự ủy quyền.
Thật không may, thuật ngữ này - giống như rất nhiều thuật ngữ khác - đã trở nên hơi lộn xộn trong những năm qua và có nghĩa là những điều khác nhau đối với những người khác nhau. Theo Wikipedia:
Ủ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 đối tượng (người nhận) trong ngữ cảnh của đối tượng ban đầu khác (người gửi). Việc ủy quyền có thể được thực hiện một cách rõ ràng, bằng cách chuyển đối tượng gửi đến đối tượng nhận, có thể được thực hiện bằng bất kỳ ngôn ngữ hướng đối tượng nào; hoặc mặc nhiên, bởi các quy tắc tra cứu thành viên của ngôn ngữ, yêu cầu hỗ trợ ngôn ngữ cho tính năng.
Tuy nhiên, thường thì mọi người cũng sử dụng thuật ngữ này để mô tả một đối tượng gọi phương thức tương ứng của một đối tượng khác mà không chuyển chính nó làm đối số, có thể được gọi chính xác hơn là "chuyển tiếp".
Với điều đó, chúng tôi sẽ sử dụng "ủy quyền" để mô tả cả hai mẫu này cho phần còn lại của bài viết.
Người ủy quyền
Hãy bắt đầu khám phá về ủy quyền trong Ruby bằng cách xem Delegator
của thư viện chuẩn lớp cung cấp một số mẫu ủy quyền.
SimpleDelegator
Cái dễ nhất trong số này, và cái mà tôi đã gặp nhiều nhất, là SimpleDelegator
, bao bọc một đối tượng được cung cấp thông qua trình khởi tạo và sau đó ủy quyền tất cả các phương thức còn thiếu cho nó. Hãy xem điều này trong hành động:
require 'delegate'
User = Struct.new(:first_name, :last_name)
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
Trước tiên, chúng tôi cần require 'delegate'
để tạo SimpleDelegator
có sẵn cho mã của chúng tôi. Chúng tôi cũng đã sử dụng Struct
để tạo một User
đơn giản lớp với first_name
và last_name
người truy cập. Sau đó, chúng tôi đã thêm UserDecorator
xác định một full_name
phương pháp kết hợp các phần tên riêng lẻ thành một chuỗi duy nhất. Đây là nơi SimpleDelegator
có tác dụng:vì cả first_name
đều không cũng không phải last_name
được định nghĩa trên lớp hiện tại, thay vào đó chúng sẽ được gọi trên đối tượng được bao bọc:
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"
SimpleDelegator
cũng cho phép chúng tôi ghi đè các phương thức được ủy quyền bằng super
, gọi phương thức tương ứng trên đối tượng được bọc. Chúng tôi có thể sử dụng điều này trong ví dụ của mình để chỉ hiển thị chữ cái đầu thay vì tên đầy đủ:
class UserDecorator < SimpleDelegator
def first_name
"#{super[0]}."
end
end
decorated_user.first_name
#=> "J."
decorated_user.full_name
#=> "J. Doe"
Người ủy quyền
Trong khi đọc các ví dụ trên, bạn có tự hỏi làm thế nào UserDecorator
của chúng tôi biết đối tượng nào để ủy quyền? Câu trả lời cho điều đó nằm trong SimpleDelegator
lớp cha của— Delegator
. Đây là một lớp cơ sở trừu tượng để xác định các lược đồ ủy quyền tùy chỉnh bằng cách cung cấp các triển khai cho __getobj__
và __setobj__
để nhận và đặt mục tiêu ủy quyền tương ứng. Sử dụng kiến thức này, chúng tôi có thể dễ dàng xây dựng phiên bản SimpleDelegator
của riêng mình cho mục đích trình diễn:
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 hơi khác với SimpleDelegator
triển khai thực sự gọi __setobj__
trong initialize
của nó phương pháp. Vì lớp người đại diện tùy chỉnh của chúng tôi không cần đến nó, chúng tôi hoàn toàn bỏ qua phương pháp đó.
Điều này sẽ hoạt động chính xác như ví dụ trước của chúng tôi; và thực sự nó có:
UserDecorator.superclass
#=> MyDelegator < Delegator
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"
DelegateMethod
Mẫu ủy quyền cuối cùng Delegate
cung cấp cho chúng tôi là Object.DelegateClass
phương pháp. Thao tác này tạo và trả về một lớp ủy nhiệm cho một lớp cụ thể, sau đó chúng ta có thể kế thừa từ:
class MyClass < DelegateClass(ClassToDelegateTo)
def initialize
super(obj_of_ClassToDelegateTo)
end
end
Mặc dù điều này thoạt nhìn có vẻ khó hiểu - đặc biệt là thực tế là phía bên phải của kế thừa có thể chứa mã Ruby tùy ý - nó thực sự tuân theo các mẫu mà chúng tôi đã khám phá trước đó, tức là nó tương tự như kế thừa từ SimpleDelegator
.
Thư viện chuẩn của Ruby sử dụng tính năng này để xác định Tempfile
của nó lớp ủy nhiệm phần lớn công việc của nó vào File
trong khi thiết lập một số quy tắc đặc biệt liên quan đến vị trí lưu trữ và xóa tệp. Chúng tôi có thể sử dụng cùng một cơ chế để thiết lập Logfile
tùy chỉnh lớp học như thế này:
class Logfile < DelegateClass(File)
MODE = File::WRONLY|File::CREAT|File::APPEND
def initialize(basename, logdir = '/var/log')
# Create logfile in location specified by logdir
path = File.join(logdir, basename)
logfile = File.open(path, MODE, 0644)
# This will call Delegator's initialize method, so below this point
# we can call any method from File on our Logfile instances.
super(logfile)
end
end
Có thể chuyển tiếp
Điều thú vị là, thư viện chuẩn của Ruby cung cấp cho chúng ta một thư viện khác để ủy quyền ở dạng Forwardable
mô-đun và def_delegator
của nó và def_delegators
phương pháp.
Hãy viết lại UserDecorator
ban đầu của chúng ta ví dụ 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 đáng chú ý nhất là ủy quyền không được cung cấp tự động qua method_missing
, nhưng thay vào đó, cần phải được khai báo rõ ràng cho mỗi phương thức mà chúng ta muốn chuyển tiếp. Điều này cho phép chúng tôi "ẩn" bất kỳ phương thức nào của đối tượng được bọc mà chúng tôi không muốn hiển thị cho khách hàng của mình, điều này cho phép chúng tôi kiểm soát nhiều hơn đối với giao diện công khai của mình và là lý do chính mà tôi thường thích Forwardable
qua SimpleDelegator
.
Một tính năng thú vị khác của Forwardable
là khả năng đổi tên các phương thức được ủy quyền thông qua def_delegator
, chấp nhận đối số thứ ba tùy chọn chỉ định bí danh 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
end
UserDecorator
ở trên chỉ để lộ bí danh personal_name
và family_name
, trong khi vẫn chuyển tiếp đến first_name
và last_name
của User
được bọc đối tượng:
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 đôi khi có thể trở nên khá hữu ích. Trước đây, tôi đã sử dụng thành công nó cho những việc như di chuyển mã giữa các thư viện có giao diện tương tự nhưng các kỳ vọng khác nhau về tên phương thức.
Bên ngoài Thư viện Chuẩn
Bất chấp các giải pháp ủy quyền hiện có trong thư viện tiêu chuẩn, cộng đồng Ruby đã phát triển một số giải pháp thay thế trong nhiều năm và chúng ta sẽ khám phá hai trong số chúng tiếp theo.
đại biểu
Xem xét mức độ phổ biến của Rails, delegate
của nó method cũng có thể là hình thức ủy quyền được các nhà phát triển Ruby sử dụng phổ biến nhất. Đây là cách chúng tôi có thể sử dụng nó để viết lại UserDecorator
cũ đáng tin cậy của chúng tôi :
# In a real Rails app this would most likely be a subclass of 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 tôi không cần sử dụng extend
kể từ khi delegate
được xác định trực tiếp trên Module
và do đó có sẵn trong mọi lớp hoặc nội dung mô-đun (tốt hơn hay tệ hơn là do bạn quyết định). Tuy nhiên, delegate
có một vài thủ thuật gọn gàng lên tay áo của nó. Đầu tiên, có tiền tố :prefix
tùy chọn này sẽ đặt trước tên phương thức được ủy quyền với tên của đối tượng mà chúng tôi đang ủy quyền. Vì vậy,
delegate :first_name, :last_name, to: :user, prefix: true
sẽ tạo user_first_name
và user_last_name
các phương pháp. Ngoài ra, chúng tôi có thể cung cấp tiền tố tùy chỉnh:
delegate :first_name, :last_name, to: :user, prefix: :account
Giờ đây, chúng tôi có thể truy cập các phần khác nhau của tên người dùng dưới dạng account_first_name
và account_last_name
.
Một tùy chọn thú vị khác của delegate
là :allow_nil
của nó quyền mua. Nếu đối tượng mà chúng tôi ủy quyền hiện là nil
—Ví dụ do ActiveRecord
chưa được đặt mối quan hệ — chúng tôi thường kết thúc bằng NoMethodError
:
decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil
Tuy nhiên, với :allow_nil
tùy chọn, cuộc gọi này sẽ thành công và trả về nil
thay vào đó:
class UserDecorator
delegate :first_name, :last_name, to: :user, allow_nil: true
...
end
decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> nil
Truyền
Tùy chọn ủy quyền cuối cùng mà chúng tôi sẽ xem xét là Casting
của Jim Gay gem, cho phép các nhà phát triển "ủy quyền các phương thức trong Ruby và bảo toàn bản thân". Đây có lẽ là định nghĩa gần nhất với định nghĩa chặt chẽ về ủy quyền, vì nó sử dụng bản chất động của Ruby để tạm thời gắn kết người nhận của một cuộc gọi phương thức, tương tự như sau:
UserDecorator.instance_method(:full_name).bind(user).call
#=> "John Doe"
Khía cạnh thú vị nhất của điều này là các nhà phát triển có thể thêm hành vi vào các đối tượng mà không cần thay đổi cấu trúc phân cấp siêu lớp 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 tôi đã mở rộng user
với Casting::Client
, cung cấp cho chúng tôi quyền truy cập vào delegate
phương pháp. Ngoài ra, chúng tôi có thể đã sử dụng include Casting::Client
bên trong User
để cung cấp khả năng này cho tất cả các phiên bản.
Ngoài ra, Casting
cung cấp các tùy chọn để thêm tạm thời các hành vi trong suốt thời gian tồn tại của một khối hoặc cho đến khi bị xóa thủ công một lần nữa. Để điều này hoạt động, trước tiên chúng ta cần bật ủy quyền các phương thức còn thiếu:
user.delegate_missing_methods
Để thêm hành vi trong khoảng thời gian của một khối, chúng ta có thể sử dụng Casting
của delegate
phương thức lớp:
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">
Ngoài ra, chúng ta có thể thêm hành vi cho đến khi chúng ta gọi uncast
một cách rõ ràng một lần nữa:
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">
Tuy phức tạp hơn một chút so với các giải pháp đã trình bày khác, nhưng Casting
cung cấp nhiều quyền kiểm soát và Jim thể hiện các công dụng khác nhau của nó và hơn thế nữa trong cuốn sách Clean Ruby của anh ấy.
Tóm tắt
Ủy quyền và chuyển tiếp phương thức là những mẫu hữu ích để phân chia trách nhiệm giữa các đối tượng liên quan. Trong các dự án Ruby thuần túy, cả Delegator
và Forwardable
có thể được sử dụng, trong khi mã Rails có xu hướng bị thu hút về phía delegate
của nó phương pháp. Để kiểm soát tối đa những gì được ủy quyền, hãy Casting
gem là một lựa chọn tuyệt vời, mặc dù nó phức tạp hơn một chút so với các giải pháp khác.
Mối tình của Tác giả khách mời Michael Kohl với Ruby bắt đầu vào khoảng năm 2003. Anh ấy cũng thích viết và nói về ngôn ngữ này và đồng tổ chức Bangkok.rb và RubyConf Thailand.