ใน Ruby Magic ก่อนหน้านี้ เราพบวิธีแทรกโมดูลลงในคลาสอย่างน่าเชื่อถือโดยเขียนทับ .new
method ทำให้เราสามารถห่อ method ที่มีพฤติกรรมเพิ่มเติมได้
ครั้งนี้ เรากำลังก้าวไปอีกขั้นโดยแยกพฤติกรรมนั้นออกเป็นโมดูลของตัวเอง เพื่อให้เราสามารถนำกลับมาใช้ใหม่ได้ เราจะสร้าง Wrappable
โมดูลที่จัดการส่วนขยายคลาสสำหรับเรา และเราจะเรียนรู้ทั้งหมดเกี่ยวกับตัวแปรอินสแตนซ์ระดับคลาสไปพร้อมกัน มาดำดิ่งกันเลย!
แนะนำ Wrappable
โมดูล
ในการห่ออ็อบเจ็กต์ด้วยโมดูลเมื่อเริ่มต้น เราต้องแจ้งให้คลาสทราบว่าควรใช้โมเดลการห่อแบบใด เริ่มต้นด้วยการสร้าง Wrappable
. อย่างง่าย โมดูลที่ให้ wrap
เมธอดที่ผลักโมดูลที่กำหนดเข้าไปในอาร์เรย์ที่กำหนดเป็นแอตทริบิวต์คลาส นอกจากนี้เรายังกำหนด new
วิธีการตามที่กล่าวในโพสต์ที่แล้ว
module Wrappable
@@wrappers = []
def wrap(mod)
@@wrappers << mod
end
def new(*arguments, &block)
instance = allocate
@@wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
ในการเพิ่มพฤติกรรมใหม่ให้กับชั้นเรียน เราใช้ extend
. extend
วิธีการเพิ่มโมดูลที่กำหนดให้กับคลาส เมธอดจะกลายเป็นเมธอดของคลาส ในการเพิ่มโมดูลเพื่อห่ออินสแตนซ์ของคลาสนี้ด้วย ตอนนี้เราสามารถเรียก wrap
วิธีการ
module Logging
def make_noise
puts "Started making noise"
super
puts "Finished making noise"
end
end
class Bird
extend Wrappable
wrap Logging
def make_noise
puts "Chirp, chirp!"
end
end
มาลองดูกันโดยสร้างอินสแตนซ์ใหม่ของ Bird
และเรียก make_noise
วิธีการ
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
ยอดเยี่ยม! มันทำงานได้ตามที่คาดไว้ อย่างไรก็ตาม สิ่งต่าง ๆ เริ่มมีพฤติกรรมแปลก ๆ เมื่อเราขยายคลาสที่สองด้วย Wrappable
โมดูล
module Powered
def make_noise
puts "Powering up"
super
puts "Shutting down"
end
end
class Machine
extend Wrappable
wrap Powered
def make_noise
puts "Buzzzzzz"
end
end
machine = Machine.new
machine.make_noise
# Powering up
# Started making noise
# Buzzzzzz
# Finished making noise
# Shutting down
bird = Bird.new
bird.make_noise
# Powering up
# Started making noise
# Chirp, chirp!
# Finished making noise
# Shutting down
แม้ว่า Machine
ไม่ถูกห่อด้วย Logging
โมดูล มันยังคงส่งออกข้อมูลการบันทึก ที่แย่ไปกว่านั้น แม้แต่นกก็ยังเปิดเครื่องขึ้นและลง ไม่ถูกต้องใช่ไหม
สาเหตุของปัญหานี้อยู่ที่วิธีการจัดเก็บโมดูล ตัวแปรคลาส @@wrappables
ถูกกำหนดไว้ที่ Wrappable
และใช้เมื่อใดก็ตามที่เราเพิ่มโมดูลใหม่ โดยไม่คำนึงถึงคลาสที่ wrap
ถูกนำมาใช้
สิ่งนี้จะชัดเจนยิ่งขึ้นเมื่อดูตัวแปรคลาสที่กำหนดไว้ใน Wrappable
โมดูลและ Bird
และ Machine
ชั้นเรียน ในขณะที่ Wrappable
มีวิธีคลาสที่กำหนดไว้ ทั้งสองคลาสไม่มี
Wrappable.class_variables # => [:@@wrappers]
Bird.class_variables # => []
Machine.class_variables # => []
ในการแก้ไขปัญหานี้ เราต้องแก้ไขการใช้งานเพื่อให้ใช้ตัวแปรอินสแตนซ์ อย่างไรก็ตาม สิ่งเหล่านี้ไม่ใช่ตัวแปรในอินสแตนซ์ของ Bird
หรือ Machine
แต่ตัวแปรอินสแตนซ์ในคลาสเอง
ใน Ruby คลาสเป็นเพียงวัตถุ
นี่เป็นเรื่องเล็กน้อยที่เชื่อได้ในตอนแรก แต่ก็ยังเป็นแนวคิดที่สำคัญมากที่ต้องทำความเข้าใจ คลาสคือตัวอย่างของ Class
และเขียน class Bird; end
เทียบเท่ากับการเขียน Bird = Class.new
. เพื่อให้ Class
สับสนมากขึ้น สืบทอดมาจาก Module
ซึ่งสืบทอดมาจาก Object
. เป็นผลให้คลาสและโมดูลมีวิธีการเดียวกันกับอ็อบเจ็กต์อื่น วิธีการส่วนใหญ่ที่เราใช้ในคลาส (เช่น attr_accessor
มาโคร) เป็นวิธีการอินสแตนซ์ของ Module
.
การใช้ตัวแปรอินสแตนซ์ในคลาส
มาเปลี่ยน Wrappable
. กันเถอะ การใช้งานเพื่อใช้ตัวแปรอินสแตนซ์ เพื่อให้ทุกอย่างสะอาดยิ่งขึ้น เราขอแนะนำ wrappers
เมธอดที่ตั้งค่าอาร์เรย์หรือส่งคืนอาร์เรย์ที่มีอยู่เมื่อตัวแปรอินสแตนซ์มีอยู่แล้ว นอกจากนี้เรายังแก้ไข wrap
และ new
เพื่อที่จะได้ใช้วิธีการใหม่นั้น
module Wrappable
def wrap(mod)
wrappers << mod
end
def wrappers
@wrappers ||= []
end
def new(*arguments, &block)
instance = allocate
wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
เมื่อเราตรวจสอบตัวแปรอินสแตนซ์ในโมดูลและในสองคลาส เราจะเห็นว่าทั้ง Bird
และ Machine
ตอนนี้รักษาคอลเล็กชันโมดูลการห่อของตัวเองไว้
Wrappable.instance_variables #=> []
Bird.instance_variables #=> [:@wrappers]
Machine.instance_variables #=> [:@wrappers]
ไม่น่าแปลกใจเลย วิธีนี้ช่วยแก้ปัญหาที่เราสังเกตเห็นก่อนหน้านี้ด้วย ตอนนี้ทั้งสองคลาสถูกรวมเข้ากับโมดูลของตนเอง
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzzz
# Shutting down
สนับสนุนมรดก
ทั้งหมดนี้ใช้งานได้ดีจนกระทั่งมีการสืบทอด เราคาดหวังว่าคลาสจะสืบทอดโมดูลการห่อจากซูเปอร์คลาส มาดูกันว่าใช่หรือเปล่า
module Flying
def make_noise
super
puts "Is flying away"
end
end
class Pigeon < Bird
wrap Flying
def make_noise
puts "Coo!"
end
end
pigeon = Pigeon.new
pigeon.make_noise
# Coo!
# Is flying away
อย่างที่คุณเห็น มันใช้งานไม่ได้อย่างที่คาดไว้ เพราะ Pigeon
ยังรักษาคอลเล็กชันโมดูลการห่อของตัวเองไว้ด้วย แม้ว่าการห่อโมดูลที่กำหนดไว้สำหรับ Pigeon
ไม่ได้กำหนดไว้บน Bird
มันไม่ใช่สิ่งที่เราต้องการอย่างแน่นอน มาคิดกันว่าจะหา wrappers ทั้งหมดจากห่วงโซ่การสืบทอดทั้งหมดกัน
โชคดีสำหรับเรา Ruby มี Module#ancestors
วิธีการแสดงรายการคลาสและโมดูลทั้งหมดที่คลาส (หรือโมดูล) สืบทอดมา
Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]
โดยการเพิ่ม grep
เราสามารถเลือกอันที่ขยายได้จริงด้วย Wrappable
. เนื่องจากเราต้องการห่ออินสแตนซ์ด้วย wrapper จากที่สูงกว่าใน chain ก่อน เราจึงเรียก .reverse
เพื่อพลิกคำสั่ง
Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]
ทับทิม #===
วิธีการ
เวทมนตร์บางอย่างของ Ruby ลงมาที่ #===
(หรือ ความเท่าเทียมกันของกรณี ) กระบวนการ. โดยค่าเริ่มต้น มันจะทำงานเหมือนกับ #==
(หรือ ความเท่าเทียมกัน ) กระบวนการ. อย่างไรก็ตาม หลายคลาสแทนที่ #===
วิธีการให้พฤติกรรมที่แตกต่างกันใน case
งบ. นี่คือวิธีที่คุณสามารถใช้นิพจน์ทั่วไป (#===
เทียบเท่ากับ #match?
) หรือคลาส (#===
เทียบเท่ากับ #kind_of?
) ในข้อความเหล่านั้น เมธอดเช่น Enumerable#grep
, Enumerable#all?
, หรือ Enumerable#any?
ก็อาศัยวิธีความเท่าเทียมของเคสด้วย
ตอนนี้เราสามารถเรียก flat_map(&:wrappers)
เพื่อรับรายการของ wrapper ทั้งหมดที่กำหนดไว้ใน inheritance chain เป็นอาร์เรย์เดียว
Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]
ที่เหลือก็แค่บรรจุลงใน inherited_wrappers
โมดูลและปรับเปลี่ยนวิธีการใหม่เล็กน้อยเพื่อให้ใช้สิ่งนั้นแทน wrappers
วิธีการ
module Wrappable
def inherited_wrappers
ancestors
.grep(Wrappable)
.reverse
.flat_map(&:wrappers)
end
def new(*arguments, &block)
instance = allocate
inherited_wrappers.each { |mod|instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
การทดสอบครั้งสุดท้ายยืนยันว่าทุกอย่างทำงานได้ตามที่คาดไว้ โมดูลการห่อใช้กับคลาสเท่านั้น (และคลาสย่อยของคลาส) ที่ปรับใช้
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzz
# Shutting down
pigeon = Pigeon.new
pigeon.make_noise
# Started making noise
# Coo!
# Finished making noise
# Is flying away
จบ!
เป็นที่ยอมรับว่านกที่มีเสียงดังเหล่านี้เป็นตัวอย่างทางทฤษฎีเล็กน้อย (ทวีต, ทวีต) แต่ตัวแปรอินสแตนซ์ของคลาสที่สืบทอดมาไม่ได้เป็นเพียงการเข้าใจวิธีการทำงานของคลาสที่ยอดเยี่ยมเท่านั้น นี่เป็นตัวอย่างที่ดีที่คลาสเป็นเพียงวัตถุใน Ruby
และเราจะยอมรับว่าตัวแปรอินสแตนซ์ของคลาสที่สืบทอดได้อาจมีประโยชน์มากในชีวิตจริงด้วยซ้ำ ตัวอย่างเช่น ลองนึกถึงการกำหนดคุณลักษณะและความสัมพันธ์บนแบบจำลองที่มีความสามารถในการไตร่ตรองในภายหลัง สำหรับเราแล้ว ความมหัศจรรย์คือการเล่นกับสิ่งนี้และทำความเข้าใจวิธีการทำงานของสิ่งต่างๆ ให้ดียิ่งขึ้น และเปิดใจของคุณสำหรับการแก้ปัญหาในระดับต่อไป 🧙🏼♀️
และเช่นเคย เรารอคอยที่จะได้ยินสิ่งที่คุณสร้างโดยใช้รูปแบบนี้หรือรูปแบบที่คล้ายคลึงกัน เพียงส่งเสียงร้องไปที่ @AppSignal บน Twitter