Computer >> Máy Tính >  >> Lập trình >> Ruby

Ruby's Hidden Gems -Delegator và Forwardable

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_namelast_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____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_namefamily_name , trong khi vẫn chuyển tiếp đến first_namelast_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_nameuser_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_nameaccount_last_name .

Một tùy chọn thú vị khác của delegate: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ả DelegatorForwardable 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.