หนึ่งในสิ่งที่ทำให้ Ruby ยอดเยี่ยมคือเราสามารถ customize เกือบทุกอย่างตามความต้องการได้ นี่ทั้ง useful และ dangerous มันง่ายที่จะยิงขาเอง แต่เมื่อใช้อย่างระมัดระวัง จะผลลัพธ์เป็น powerful solutions
ที่ Ruby Magic เราคิดว่า useful และ dangerous เป็น combination ที่ยอดเยี่ยม มาดูว่า Ruby สร้างและ initialize objects อย่างไร และเราสามารถปรับ default behavior ได้อย่างไร
The Basics of Creating New Objects from Classes
เริ่มต้นด้วยการดูว่าสร้าง objects ใน Ruby อย่างไร เพื่อสร้าง object (หรือ instance) ใหม่ เราเรียก new บน class ต่างจากภาษาอื่น new ไม่ใช่ keyword ของภาษาเอง แตเป็น method ที่ถูกเรียกเหมือน method อื่นๆ
class Dog end object = Dog.newเพื่อ customize newly created object สามารถส่ง arguments ให้ method new ได้ สิ่งใดก็ตามที่ส่งเป็น arguments จะถูกส่งไปที่ initializer
class Dog def initialize(name) @name = name end end object = Dog.new('Good boy')อีกครั้ง ต่างจากภาษาอื่น initializer ใน Ruby ก็เป็นแค่ method แทน special syntax หรือ keyword
ด้วยความคิดนี้ ควรจะสามารถ mess around กับ methods เหล่านี้ได้ เหมือน method Ruby อื่นๆ 당연 ไม่ใช่ไหม?
Modifying the Behavior of a Single Object
สมมติเราอยาก ensure ว่า objects ทั้งหมดของ class เฉพาะจะ print log statements เสมอ แม้ method จะถูก override ใน subclasses หนึ่งในวิธีคือเพิ่ม module ให้ object's singleton class
module Logging def make_noise puts "Started making noise" super puts "Finished making noise" end end class Bird def make_noise puts "Chirp, chirp!" end end object = Bird.new object.singleton_class.include(Logging) object.make_noise # Started making noise # Chirp, chirp! # Finished making noiseในตัวอย่างนี้ Bird object ถูกสร้างด้วย Bird.new และ module Logging ถูก include ใน resulting object โดยใช้ singleton class ของมัน
What's a Singleton Class?
Ruby อนุญาต methods ที่ unique สำหรับ single object เพื่อ support นี้ Ruby เพิ่ม anonymous class ระหว่าง object และ actual class ของมัน เมื่อ methods ถูกเรียก methods ที่ defined บน singleton class จะได้ precedence over methods ใน actual class Singleton classes เหล่านี้ unique สำหรับทุก object ดังนั้นการเพิ่ม methods ให้พวกมันไม่ affect objects อื่นๆ ของ actual class เรียนรู้เพิ่มเติมเกี่ยวกับ classes และ objects ใน Programming Ruby guide
มีอะไรมากเกินไปที่จะ modify singleton class ของแต่ละ object ทุกครั้งที่สร้าง มา move inclusion ของ Logging class ไปที่ initializer เพื่อเพิ่มให้ทุก object ที่สร้าง
module Logging def make_noise puts "Started making noise" super puts "Finished making noise" end end class Bird def initialize singleton_class.include(Logging) end def make_noise puts "Chirp, chirp!" end end object = Bird.new object.make_noise # Started making noise # Chirp, chirp! # Finished making noiseแม้จะทำงานดี แต่ถ้าเราสร้าง subclass ของ Bird เช่น Duck initializer ของมันต้องเรียก super เพื่อ retain Logging behavior แม้จะ argue ได้ว่าเป็น good idea ที่จะ call super properly ทุกครั้งที่ override method มาลองหาวิธีที่ไม่ require มันดู
ถ้าเราไม่เรียก super จาก subclass เราจะสูญเสีย inclusion ของ Logger class:
class Duck < Bird def initialize(name) @name = name end def make_noise puts "#{@name}: Quack, quack!" end end object = Duck.new('Felix') object.make_noise # Felix: Quack, quack!แทนนั้น มา override Bird.new ตามที่กล่าวไว้ new เป็นแค่ method ที่ implemented บน classes ดังนั้นเราสามารถ override มัน เรียก super และ modify newly created object ตามต้องการ
class Bird def self.new(*arguments, &block) instance = super instance.singleton_class.include(Logging) instance end end object = Duck.new('Felix') object.make_noise # Started making noise # Felix: Quack, quack! # Finished making noiseแต่เกิดอะไรขึ้นถ้าเราเรียก make_noise ใน initializer? น่าเสียดายเพราะ singleton class ยังไม่ include Logging module เราจะไม่ได้ desired output
โชคดีที่มี solution: สามารถสร้าง default .new behavior จากศูนย์โดยเรียก allocate
class Bird def self.new(*arguments, &block) instance = allocate instance.singleton_class.include(Logging) instance.send(:initialize, *arguments, &block) instance end endการเรียก allocate คืน object ใหม่ uninitialized ของ class ดังนั้นหลังจากนั้นเราสามารถ include additional behavior และหลังจากนั้นถึงเรียก method initialize บน object นั้น (เพราะ initialize เป็น private โดยค่าเริ่มต้น เราต้อง resort ไปใช้ send สำหรับสิ่งนี้)
The Truth About Class#allocate
ต่างจาก methods อื่นๆ ไม่สามารถ override allocate ได้ Ruby ไม่ใช้ conventional way ของ dispatching methods สำหรับ allocate ภายใน ผลคือแค่ override allocate โดยไม่ override new ไม่ได้ แต่ถ้าเราเรียก allocate โดยตรง Ruby จะเรียก redefined method เรียนรู้เพิ่มเติมเกี่ยวกับ Class#new และ Class#allocate ใน Ruby documentation
Why Would We Do This?
เหมือนหลายสิ่ง การ modify ว่า Ruby สร้าง objects จาก classes อย่างไร อาจ dangerous และอาจ break ใน unexpected ways
อย่างไรก็ตาม มี valid use cases สำหรับการเปลี่ยน object creation ตัวอย่าง ActiveRecord ใช้ allocate กับ init_from_db method ที่ต่างกันเพื่อเปลี่ยน initialization process เมื่อสร้าง objects จาก database เทียบกับ building unsaved objects มันยังใช้ allocate เพื่อ convert records ระหว่าง different single-table inheritance types ด้วย becomes
สำคัญที่สุด ด้วยการเล่นกับ object creation คุณได้ deeper insight ไปยังวิธีที่มันทำงานใน Ruby และเปิดใจสู่ different solutions เราหวังว่าคุณเพลิดเพลินกับบทความ
เราอยากฟังเรื่องสิ่งที่คุณ implement ด้วยการเปลี่ยน Ruby's default way ของการสร้าง objects โปรดอย่าลังเล tweet ความคิดของคุณไปที่ @AppSignal