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