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

ความมหัศจรรย์ของตัวแปรอินสแตนซ์ระดับคลาส

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