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

เรียนรู้ Class-Level Instance Variables ใน Ruby อย่างเชี่ยวชาญ

ใน Ruby Magic ก่อนหน้านี้ เราได้ค้นพบวิธีการ inject modules เข้าสู่ classes ได้อย่างน่าเชื่อถือ โดยการ overwrite method .new ซึ่งช่วยให้เราสามารถ wrap methods ด้วยพฤติกรรมเพิ่มเติมได้

ครั้งนี้ เราจะก้าวต่อไปอีกขั้นหนึ่ง โดยนำพฤติกรรมนั้นมาสร้างเป็น module ของตัวเอง เพื่อให้สามารถนำกลับมาใช้ใหม่ได้ เราจะสร้าง module Wrappable ที่จัดการการขยาย class ให้อัตโนมัติ และในขณะเดียวกันเราจะได้เรียนรู้เกี่ยวกับ class-level instance variables มาโดยละเอียด มาเริ่มกันเลย!

แนะนำ Module Wrappable

เพื่อ wrap objects ด้วย modules เมื่อถูก initialize เราต้องบอก class ว่าจะใช้ wrapping modules อะไร เริ่มต้นด้วยการสร้าง Wrappable module แบบง่ายๆ ที่มี method wrap เพื่อ push module ที่กำหนดเข้าไปใน array ที่ถูกกำหนดเป็น class attribute นอกจากนี้ เรายัง redefine method 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

เพื่อเพิ่มพฤติกรรมใหม่ให้กับ class เราใช้ extend Method extend จะเพิ่ม module ที่กำหนดเข้าไปใน class ทำให้ methods กลายเป็น class methods เพื่อเพิ่ม module สำหรับ wrap instances ของ class นี้ เราสามารถเรียก method 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

ลองทดสอบโดยสร้าง instance ใหม่ของ Bird และเรียก method make_noise

bird = Bird.new bird.make_noise # Started making noise # Chirp, chirp! # Finished making noise

เยี่ยม! ทำงานตรงตามที่คาดไว้ แต่สิ่งแปลกๆ จะเริ่มเกิดขึ้นเมื่อเรา extend class ที่สองด้วย module 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 จะไม่ได้ถูก wrap ด้วย module Logging แต่ก็ยังแสดงข้อมูล logging อีกด้วย สิ่งที่แย่กว่านั้น—แม้ Bird ก็กลายเป็น powering up และ down ด้วย นี่แน่ใจว่าไม่ถูกต้อง

ต้นเหตุของปัญหานี้อยู่ที่วิธีการเก็บ modules Class variable @@wrappers ถูกกำหนดบน module Wrappable และถูกใช้ทุกครั้งที่เราเพิ่ม module ใหม่ ไม่ว่าจะเป็น class ใดที่ใช้ wrap ก็ตาม

เรื่องนี้จะชัดเจนขึ้นถ้าดู class variables ที่ถูกกำหนดบน module Wrappable และ class Bird กับ Machine ในขณะที่ Wrappable มี class variable ถูกกำหนด สอง class นั้นไม่มี

Wrappable.class_variables # => [:@@wrappers] Bird.class_variables # => [] Machine.class_variables # => []

เพื่อแก้ไข เราต้องปรับ implementation ให้ใช้ instance variables แทน แต่ instance variables เหล่านี้ไม่ใช่บน instances ของ Bird หรือ Machine—แต่เป็น instance variables บน classes เอง

ใน Ruby Classes ก็คือ Objects

แนวคิดนี้อาจทำให้ปวดหัวครั้งแรก แต่เป็นแนวคิดสำคัญมากที่ต้องเข้าใจ Classes เป็น instances ของ Class และการเขียน class Bird; end เทียบเท่ากับ Bird = Class.new ยิ่งไปกว่านั้น Class สืบทอดมาจาก Module ซึ่งสืบทอดมาจาก Object ผลคือ classes และ modules มี methods เหมือน object อื่นๆ ทุกประการ Methods ส่วนใหญ่ที่เราใช้กับ classes (เช่น macro attr_accessor) จริงๆ แล้วเป็น instance methods ของ Module

การใช้ Instance Variables บน Classes

มาปรับ implementation ของ Wrappable ให้ใช้ instance variables แทนกัน เพื่อความสะอาด เราเพิ่ม method wrappers ที่จะสร้าง array ใหม่หรือคืนค่าที่มีอยู่แล้วเมื่อ instance variable มีอยู่แล้ว เรายังปรับ methods wrap และ new ให้ใช้ method ใหม่นี้

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

เมื่อตรวจสอบ instance variables บน module และสอง classes เราจะเห็นว่า Bird และ Machine ตอนนี้แต่ละตัวมี collection ของ wrapping modules ของตัวเอง

Wrappable.instance_variables #=> [] Bird.instance_variables #=> [:@wrappers] Machine.instance_variables #=> [:@wrappers]

ไม่แปลกใจที่ปัญหาก่อนหน้านี้ได้รับการแก้ไข—ตอนนี้ทั้งสอง classes ถูก wrap ด้วย modules ของตัวเองแยกกัน

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

รองรับ Inheritance

ทุกอย่างทำงานได้ดีจนกว่าจะมี inheritance เข้ามา เราคาดหวังว่า classes จะสืบทอด wrapping modules จาก superclass มาดูว่าจริงหรือไม่

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 ก็ยังคงมี collection ของ wrapping modules ของตัวเองอยู่ แม้จะสมเหตุสมผลว่า wrapping modules ที่กำหนดให้ Pigeon ไม่ได้อยู่บน Bird แต่ไม่ใช่สิ่งที่เราต้องการ มาหาวิธีดึง wrappers ทั้งหมดจาก inheritance chain กัน

โชคดีที่ Ruby มี method Module#ancestors สำหรับ list classes และ modules ทั้งหมดที่ class (หรือ module) สืบทอดมา

Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]

ด้วยการเพิ่ม grep เราสามารถเลือกเฉพาะ classes ที่ extended ด้วย Wrappable ได้ เนื่องจากเราต้องการ wrap instances ด้วย wrappers จากระดับสูงก่อน เราจึงเรียก .reverse เพื่อกลับลำดับ

Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]

Method #=== ของ Ruby

บางส่วนของ Ruby magic มาจาก method #=== (หรือ case equality) โดยค่าเริ่มต้นมันทำงานเหมือน #== (หรือ equality) แต่หลาย classes override #=== เพื่อให้พฤติกรรมต่างกันใน case statements นี่คือเหตุผลที่คุณสามารถใช้ regular expressions (#=== เทียบเท่า #match?) หรือ classes (#=== เทียบเท่า #kind_of?) ใน statements เหล่านั้น Methods เช่น Enumerable#grep, Enumerable#all?, หรือ Enumerable#any? ก็พึ่งพา case equality method เช่นกัน

ตอนนี้เราสามารถเรียก flat_map(&:wrappers) เพื่อดึง list ของ wrappers ทั้งหมดใน inheritance chain เป็น array เดียว

Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]

สิ่งที่เหลือคือการ pack logic นี้เข้าไปใน method inherited_wrappers และปรับ method new ให้ใช้ method ใหม่นี้แทน 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

การทดสอบครั้งสุดท้ายยืนยันว่าทุกอย่างทำงานตามที่คาดไว้ Wrapping modules จะถูกนำไปใช้กับ class (และ subclasses) ที่ถูกนำไปใช้เท่านั้น

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

จบแล้วครับ!

承認ว่า นกที่ทำเสียงดังเหล่านี้เป็นเพียงตัวอย่างทฤษฎี (tweet, tweet) แต่ inheritable class instance variables ไม่ได้แค่ cool สำหรับการเข้าใจว่า classes ทำงานอย่างไร พวกมันเป็นตัวอย่างยอดเยี่ยchsthat classes ก็คือ objects ใน Ruby

และเรายอมรับว่า inheritable class instance variables อาจมีประโยชน์ในชีวิตจริงด้วย เช่น การกำหนด attributes และ relationships บน model พร้อมความสามารถในการ introspect ได้ภายหลัง สำหรับเรา magic คือการเล่นกับสิ่งเหล่านี้และเข้าใจลึกขึ้นว่าสิ่งต่างๆ ทำงานอย่างไร และเปิดใจสู่ระดับต่อไปของ solutions 🧙🏼‍♀️

เหมือนเดิม เรารอคอยฟังว่าคุณสร้างอะไรขึ้นมาโดยใช้ pattern นี้หรือ pattern คล้ายๆ กัน แค่ tweet มาที่ @AppSignal บน Twitter