ใน 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