ในการสำรวจอัญมณีที่ซ่อนอยู่ในไลบรารีมาตรฐานของ 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