Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Ruby's Hidden Gems -Delegator และ Forwardable

ในการสำรวจอัญมณีที่ซ่อนอยู่ในไลบรารีมาตรฐานของ Ruby ในวันนี้ เราจะมาดูที่คณะผู้แทน

น่าเสียดายที่คำนี้—เหมือนกับคำอื่นๆ—ค่อนข้างสับสนในช่วงหลายปีที่ผ่านมาและมีความหมายที่แตกต่างกันไปสำหรับแต่ละคน ตามวิกิพีเดีย:

การมอบหมายหมายถึงการประเมินสมาชิก (คุณสมบัติหรือวิธีการ) ของวัตถุหนึ่ง (ผู้รับ) ในบริบทของวัตถุดั้งเดิมอื่น (ผู้ส่ง) การมอบหมายสามารถทำได้อย่างชัดเจน โดยส่งวัตถุที่ส่งไปยังวัตถุที่ได้รับ ซึ่งสามารถทำได้ในภาษาเชิงวัตถุใดๆ หรือโดยปริยายโดยกฎการค้นหาสมาชิกของภาษาซึ่งต้องการการสนับสนุนภาษาสำหรับคุณลักษณะนี้

อย่างไรก็ตาม บ่อยครั้ง ผู้คนยังใช้คำนี้เพื่ออธิบายอ็อบเจ็กต์ที่เรียกเมธอดที่สอดคล้องกันของอ็อบเจกต์อื่นโดยไม่ส่งผ่านตัวมันเองมาเป็นอาร์กิวเมนต์ ซึ่งสามารถเรียกได้ว่า "การส่งต่อ" ได้แม่นยำยิ่งขึ้น

ด้วยวิธีนี้ เราจะใช้ "การมอบหมาย" เพื่ออธิบายรูปแบบทั้งสองนี้สำหรับส่วนที่เหลือของบทความ

ตัวแทน

มาเริ่มสำรวจการมอบสิทธิ์ใน Ruby โดยดูที่ Delegator ของไลบรารีมาตรฐาน คลาสซึ่งมีรูปแบบการมอบหมายหลายแบบ

SimpleDelegator

สิ่งที่ง่ายที่สุดและสิ่งที่ฉันพบมากที่สุดในป่าคือ SimpleDelegator ซึ่งล้อมอ็อบเจ็กต์ที่ให้ไว้ผ่านตัวเริ่มต้น แล้วมอบหมายวิธีที่ขาดหายไปทั้งหมดให้กับอ็อบเจ็กต์ มาดูสิ่งนี้กัน:

require 'delegate'
 
User = Struct.new(:first_name, :last_name)
 
class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

ก่อนอื่น เราต้อง require 'delegate' เพื่อสร้าง SimpleDelegator มีอยู่ในรหัสของเรา นอกจากนี้เรายังใช้ Struct เพื่อสร้าง User . อย่างง่าย คลาสที่มี first_name และ last_name อุปกรณ์เสริม จากนั้นเราได้เพิ่ม UserDecorator ซึ่งกำหนด full_name วิธีการรวมแต่ละส่วนของชื่อเป็นสตริงเดียว นี่คือที่ที่ SimpleDelegator เข้ามาเล่น:เนื่องจากไม่ใช่ first_name หรือ last_name ถูกกำหนดในคลาสปัจจุบัน พวกมันจะถูกเรียกบนอ็อบเจ็กต์ที่ถูกห่อแทน:

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

SimpleDelegator ยังให้เราแทนที่เมธอดที่ได้รับมอบหมายด้วย super เรียกเมธอดที่สอดคล้องกันบนอ็อบเจ็กต์ที่ห่อ เราสามารถใช้สิ่งนี้ในตัวอย่างของเราเพื่อแสดงเฉพาะชื่อย่อแทนชื่อเต็ม:

class UserDecorator < SimpleDelegator
  def first_name
    "#{super[0]}."
  end
end
decorated_user.first_name
#=> "J."
decorated_user.full_name
#=> "J. Doe"

ตัวแทน

ขณะที่อ่านตัวอย่างข้างต้น คุณสงสัยหรือไม่ว่า UserDecorator . ของเราเป็นอย่างไร รู้ว่าวัตถุที่จะมอบหมายให้? คำตอบนั้นอยู่ใน SimpleDelegator คลาสพาเรนต์ของ—Delegator . นี่คือคลาสพื้นฐานที่เป็นนามธรรมสำหรับการกำหนดรูปแบบการมอบหมายแบบกำหนดเองโดยจัดเตรียมการใช้งานสำหรับ __getobj__ และ __setobj__ เพื่อรับและกำหนดเป้าหมายการมอบหมายตามลำดับ การใช้ความรู้นี้ทำให้เราสามารถสร้าง SimpleDelegator . เวอร์ชันของเราเองได้อย่างง่ายดาย เพื่อวัตถุประสงค์ในการสาธิต:

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

สิ่งนี้แตกต่างเล็กน้อยจาก SimpleDelegator การใช้งานจริงซึ่งเรียก __setobj__ ใน initialize กระบวนการ. เนื่องจากคลาส Delegator แบบกำหนดเองของเราไม่จำเป็นต้องใช้มัน เราจึงละทิ้งวิธีการนั้นไปโดยสิ้นเชิง

สิ่งนี้ควรทำงานเหมือนกับตัวอย่างก่อนหน้านี้ของเรา และมันก็เป็นเช่นนั้น:

UserDecorator.superclass
#=> MyDelegator < Delegator
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

วิธีการมอบหมาย

รูปแบบการมอบสิทธิ์ครั้งสุดท้าย Delegate ให้เราเป็นชื่อที่ค่อนข้างแปลก Object.DelegateClass กระบวนการ. สิ่งนี้จะสร้างและส่งคืนคลาส delegator สำหรับคลาสเฉพาะ ซึ่งเราสามารถสืบทอดจาก:

  class MyClass < DelegateClass(ClassToDelegateTo)
    def initialize
      super(obj_of_ClassToDelegateTo)
    end
  end

แม้ว่าสิ่งนี้อาจดูสับสนในตอนแรก—โดยเฉพาะอย่างยิ่งความจริงที่ว่าทางด้านขวาของมรดกสามารถมีรหัส Ruby ได้ตามอำเภอใจ—จริง ๆ แล้วเป็นไปตามรูปแบบที่เราสำรวจก่อนหน้านี้ นั่นคือคล้ายกับการสืบทอดจาก SimpleDelegator .

ไลบรารีมาตรฐานของ Ruby ใช้คุณลักษณะนี้เพื่อกำหนด Tempfile คลาสที่มอบหมายงานส่วนใหญ่ให้กับ File ขณะตั้งค่ากฎพิเศษบางอย่างเกี่ยวกับตำแหน่งที่จัดเก็บและการลบไฟล์ เราสามารถใช้กลไกเดียวกันนี้ในการตั้งค่า Logfile . ที่กำหนดเองได้ คลาสแบบนี้:

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

ส่งต่อได้

ที่น่าสนใจคือ ไลบรารีมาตรฐานของ Ruby ได้จัดเตรียมไลบรารีอื่นสำหรับการมอบหมายให้เราในรูปแบบของ Forwardable โมดูลและ def_delegator และ def_delegators วิธีการ

มาเขียน UserDecorator . เดิมของเรากัน ตัวอย่างที่มี 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"

ความแตกต่างที่เห็นได้ชัดเจนที่สุดคือการมอบหมายไม่ได้ให้โดยอัตโนมัติผ่าน method_missing แต่จำเป็นต้องประกาศอย่างชัดเจนสำหรับแต่ละวิธีที่เราต้องการส่งต่อ วิธีนี้ช่วยให้เราสามารถ "ซ่อน" วิธีการใดๆ ของอ็อบเจ็กต์ที่ห่อไว้ที่เราไม่ต้องการเปิดเผยต่อลูกค้า ซึ่งช่วยให้เราควบคุมอินเทอร์เฟซสาธารณะได้มากขึ้น และเป็นเหตุผลหลักที่โดยทั่วไปแล้วฉันชอบ Forwardable เหนือ SimpleDelegator .

คุณสมบัติที่ดีอีกอย่างของ Forwardable คือความสามารถในการเปลี่ยนชื่อเมธอดที่ได้รับมอบหมายผ่าน def_delegator ซึ่งยอมรับอาร์กิวเมนต์ที่สามซึ่งระบุชื่อแทนที่ต้องการ:

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 ด้านบน เปิดเผยเฉพาะ personal_name . นามแฝง และ family_name เมธอดในขณะที่ยังคงส่งต่อไปยัง first_name และ last_name ของ User . ที่หุ้มไว้ วัตถุ:

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"

คุณลักษณะนี้อาจมีประโยชน์มากในบางครั้ง ฉันเคยใช้สำเร็จมาแล้วสำหรับสิ่งต่างๆ เช่น การย้ายโค้ดระหว่างไลบรารีที่มีอินเทอร์เฟซคล้ายกัน แต่มีความคาดหวังที่แตกต่างกันเกี่ยวกับชื่อเมธอด

นอกไลบรารีมาตรฐาน

แม้จะมีโซลูชันการมอบหมายงานในไลบรารีมาตรฐาน แต่ชุมชน Ruby ได้พัฒนาทางเลือกหลายทางในช่วงหลายปีที่ผ่านมา และเราจะสำรวจสองทางเลือกต่อไป

ตัวแทน

เมื่อพิจารณาถึงความนิยมของ Rails แล้ว delegate วิธีอาจเป็นรูปแบบการมอบหมายที่ใช้บ่อยที่สุดที่ใช้โดยนักพัฒนา Ruby ต่อไปนี้คือวิธีที่เราใช้เขียนใหม่ UserDecorator . เก่าที่เชื่อถือได้ของเรา :

# 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"

ซึ่งค่อนข้างคล้ายกับ Forwardable แต่เราไม่จำเป็นต้องใช้ extend ตั้งแต่ delegate ถูกกำหนดโดยตรงบน Module และดังนั้นจึงมีให้ในทุกคลาสหรือโมดูล (คุณเป็นคนตัดสินใจเองว่าจะดีขึ้นหรือแย่ลง) อย่างไรก็ตาม delegate มีทริคเล็กๆ น้อยๆ ที่แขนเสื้อ อย่างแรกคือ :prefix ตัวเลือกที่จะนำหน้าชื่อเมธอดที่ได้รับมอบสิทธิ์ด้วยชื่อของอ็อบเจ็กต์ที่เรามอบหมายให้ ดังนั้น

delegate :first_name, :last_name, to: :user, prefix: true

จะสร้าง user_first_name และ user_last_name วิธีการ หรือเราสามารถระบุคำนำหน้าแบบกำหนดเอง:

delegate :first_name, :last_name, to: :user, prefix: :account

ขณะนี้เราสามารถเข้าถึงส่วนต่างๆ ของชื่อผู้ใช้เป็น account_first_name และ account_last_name .

อีกตัวเลือกที่น่าสนใจของ delegate คือ :allow_nil ตัวเลือก. หากวัตถุที่เรามอบหมายให้ขณะนี้ nil —ตัวอย่างเช่น เนื่องจากไม่ได้ตั้งค่า ActiveRecord ความสัมพันธ์—เรามักจะจบลงด้วย NoMethodError :

decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil

อย่างไรก็ตาม ด้วย :allow_nil ตัวเลือกการโทรนี้จะสำเร็จและส่งคืน nil แทน:

class UserDecorator
  delegate :first_name, :last_name, to: :user, allow_nil: true
 
  ...
end
 
decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> nil

แคสติ้ง

ตัวเลือกการมอบหมายสุดท้ายที่เราจะดูคือ Casting ของ Jim Gay gem ซึ่งช่วยให้นักพัฒนาสามารถ "มอบหมายวิธีใน Ruby และรักษาตัวเอง" นี่อาจเป็นคำจำกัดความที่เข้มงวดที่สุดของการมอบหมาย เนื่องจากใช้ลักษณะไดนามิกของ Ruby เพื่อเชื่อมผู้รับการเรียกเมธอดอีกครั้งชั่วคราว ซึ่งคล้ายกับสิ่งนี้:

UserDecorator.instance_method(:full_name).bind(user).call
#=> "John Doe"

สิ่งที่น่าสนใจที่สุดคือนักพัฒนาสามารถเพิ่มพฤติกรรมให้กับออบเจ็กต์ได้โดยไม่ต้องเปลี่ยนลำดับชั้นของซูเปอร์คลาส

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)

เราขยาย user ด้วย Casting::Client ซึ่งทำให้เราสามารถเข้าถึง delegate กระบวนการ. หรือเราอาจใช้ include Casting::Client ภายใน User class เพื่อให้ความสามารถนี้กับทุกกรณี

นอกจากนี้ Casting มีตัวเลือกสำหรับการเพิ่มลักษณะการทำงานชั่วคราวตลอดอายุของบล็อกหรือจนกว่าจะนำออกด้วยตนเองอีกครั้ง เพื่อให้สิ่งนี้ใช้งานได้ เราต้องเปิดใช้งานการมอบหมายของวิธีการที่ขาดหายไปก่อน:

user.delegate_missing_methods

หากต้องการเพิ่มพฤติกรรมในช่วงระยะเวลาหนึ่งบล็อก เราก็สามารถใช้ Casting delegating วิธีการเรียน:

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">

อีกทางหนึ่ง เราสามารถเพิ่มพฤติกรรมได้จนกว่าเราจะเรียก 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">

แม้ว่าจะซับซ้อนกว่าโซลูชันอื่นๆ เล็กน้อย Casting ให้การควบคุมมากมาย และจิมก็สาธิตการใช้งานที่หลากหลายและอื่นๆ ในหนังสือ Clean Ruby ของเขา

สรุป

การมอบหมายและการส่งต่อวิธีการเป็นรูปแบบที่เป็นประโยชน์สำหรับการแบ่งความรับผิดชอบระหว่างอ็อบเจ็กต์ที่เกี่ยวข้อง ในโปรเจ็กต์ Ruby ธรรมดา ทั้ง Delegator และ Forwardable สามารถใช้ได้ในขณะที่โค้ด Rails มีแนวโน้มที่จะดึงดูดไปยัง delegate กระบวนการ. เพื่อการควบคุมสูงสุดในสิ่งที่ได้รับมอบหมาย Casting gem เป็นตัวเลือกที่ยอดเยี่ยม แม้ว่าจะซับซ้อนกว่าโซลูชันอื่นๆ เล็กน้อย

เรื่องรัก ๆ ใคร่ ๆ ของ Michael Kohl กับ Ruby ของนักเขียนรับเชิญเริ่มต้นเมื่อราวปี 2546 นอกจากนี้ เขายังสนุกกับการเขียนและพูดเกี่ยวกับภาษานี้ และร่วมจัดงาน Bangkok.rb และ RubyConf Thailand