การสืบทอดคลาส (Class inheritance) เป็นฟีเจอร์พื้นฐานของ OOP (Object-Oriented Programming) ที่ช่วยให้คุณสร้างเวอร์ชันที่เฉพาะเจาะจงและเฉพาะทางมากขึ้นของคลาสใดๆ
ตัวอย่าง:
Food -> Fruit -> Orange
มีความสัมพันธ์ระหว่างคลาสเหล่านี้!
เราสามารถพูดได้ว่าส้มคือผลไม้ แต่นอกจากนี้ ผลไม้ก็เป็นอาหาร
คลาสพาเรนต์ (เรียกอีกอย่างว่า super class หรือ base class) จะเป็นแบบทั่วไป (generic) มากกว่าคลาสลูก (subclasses) เสมอ
Fruit (ทั่วไปกว่า) เป็นคลาสพาเรนต์ของ Orange (เฉพาะเจาะจงกว่า)
ใน Ruby จะเขียนแบบนี้:
class Food end class Fruit < Food end class Orange < Fruit end
หนึ่งในนัยสำคัญของ การสืบทอด ใน Ruby คือ ทุกเมธอดและทุกค่าคงที่ (constant) ที่กำหนดบน Food จะพร้อมใช้งานบน Fruit และ Orange ด้วย
เมธอดถูกส่งผ่านมาจากคลาสฐาน แต่ไม่ใช่ทางกลับกัน
วัตถุประสงค์ของการสืบทอดคืออะไร?
คุณอาจคิดว่าการสร้างลำดับชั้นของออบเจ็กต์ให้การจัดระเบียบโค้ดที่ "รู้สึกดี" แบบใดๆ แต่นั่นมีมากกว่านั้น
การสืบทอดถูกใช้เพื่อสร้างเวอร์ชันที่แตกต่างของคลาสพาเรนต์ซึ่งเหมาะสมสำหรับวัตถุประสงค์เฉพาะ อาจต้องการฟีเจอร์หรือเมธอดเพิ่มเติมที่ไม่ควรอยู่ในคลาสพาเรนต์
ตัวอย่าง:
ผลไม้ทุกชนิดมีสี น้ำหนัก และชื่อ
บางผลไม้อาจมีลักษณะพิเศษที่ไม่ได้แบ่งปันกับผลไม้อื่น ดังนั้นคุณสร้างคลาสใหม่ที่สืบทอดลักษณะของผลไม้ทุกชนิด (สี น้ำหนัก เป็นต้น) แล้วเพิ่มลักษณะพิเศษเข้าไป
นั่นคือสิ่งที่ผมหมายถึงโดย "การเฉพาะเจาะจง" (specialization)
ตัวอย่างอีกอย่าง:
คุณมีคลาสที่เขียนลงฐานข้อมูล แตคุณต้องการเวอร์ชันอื่นของคลาส (อาจเพื่อจุดประสงค์ดีบัก) ที่บันทึก (log) ทุกการดำเนินการฐานข้อมูล
ในกรณีนี้ คุณต้องการ Decorator pattern
คุณสามารถอ่านคำอธิบายโดยละเอียดในบทความนี้ แตแนวคิดพื้นฐานคือคุณใช้การสืบทอดเพื่อห่อหุ้มคลาสอื่น และเพิ่มบางอย่างใหม่เข้าไป
โดยไม่ต้องเปลี่ยนแปลงคลาสต้นฉบับ!
การสืบทอดในโลกจริง
โอเค
เราเรียนรู้เกี่ยวกับการสืบทอดแล้ว แต่คุณรู้ไหมว่าคุณกำลังใช้มันทุกวันในฐานะนักพัฒนา Ruby?
Ruby เองใช้ การสืบทอด เพื่อเปิดใช้งานเมธอดเช่น:
putsclasssuper
นี่เป็นเพราะออบเจ็กต์ Ruby ทุกตัวสืบทอดจากคลาส Object โดยค่าเริ่มต้น
ดังนั้น หากคุณสร้างคลาสแบบนี้:
class Apple end
คลาสพาเรนต์ของมันคือ Object:
Apple.superclass # Object
นั่นคือเหตุผลที่คุณสามารถใช้เมธอดเช่นที่กล่าวมาได้
ตัวอย่างเช่น เมื่อคุณเรียก puts Ruby จะค้นหาเมธอดนี้ในคลาสของคุณ
จากนั้น:
- มันค้นหาเมธอดในหนึ่งในคลาสพาเรนต์
- หากไม่เจอ มันจะเริ่มจากออบเจ็กต์อีกครั้งและพยายามหา
method_missing - หากไม่เจอ จะยกเว้น
NoMethodErrorหรือNameErrorหากเมธอดถูกเรียกโดยไม่มีออบเจ็กต์ระบุชัดเจน (a.sizevssizeโดยที่aคือออบเจ็กต์ระบุชัดเจน)
คุณสามารถหาตัวอย่างการสืบทอดเพิ่มเติมใน Rails
ที่นี่:
class ApplicationController < ActionController::Base end
Controller:
class SessionsController < ApplicationController end
Model:
class Comment < ApplicationRecord belongs_to :article end
การสืบทอดอยู่ทุกที่ใน Ruby แตบางครั้งมันไม่ใช่คำตอบที่ถูกต้อง
Composition: ทางเลือกแทนการสืบทอด
การสืบทอดมีข้อจำกัดบางอย่าง
ตัวอย่าง:
คุณต้องการสร้างคอมพิวเตอร์จากชิ้นส่วน
เราพูดว่าคอมพิวเตอร์ *มี* ชิ้นส่วน แตชิ้นส่วนแต่ละชิ้นไม่ใช่คอมพิวเตอร์ด้วยตัวมันเอง
หากคุณแยกพวกมันออก พวกมันไม่สามารถทำหน้าที่ได้
เราต้องการสิ่งอื่น...
เราต้องการ Composition!
Composition สร้างคลาสที่ชิ้นส่วนต่างๆ มารวมกันเพื่อทำหน้าที่
เหมือนคอมพิวเตอร์
ตัวอย่าง Composition ในการทำงาน:
class Computer
def initialize(memory, disk, cpu)
@memory = memory
@disk = disk
@cpu = cpu
end
end
คอมพิวเตอร์ได้รับชิ้นส่วนที่มันต้องการเพื่อทำงาน
นี่คือ Composition
หลักการ Liskov Substitution Principle
การสืบทอดมีพลังเมื่อใช้ในสถานการณ์ที่ถูกต้อง
แตเหมือนเครื่องมือทุกอย่าง มันอาจถูกใช้ผิด!
จริงๆ แล้วมีคำพูดดังจากหนังสือ Design Patterns ต้นฉบับว่า:
"Prefer composition over inheritance."
Design Patterns: Elements of Reusable Object-Oriented Software
เพื่อให้แน่ใจว่าคุณ ใช้การสืบทอดอย่างถูกต้อง มีหลักการหนึ่งที่คุณสามารถปฏิบัติตามได้ คือ L จาก SOLID
มันย่อมาจาก "Liskov Substitution Principle" (หลักการแทนที่ลิสคอฟ)
หลักการนี้กล่าวว่า คลาสลูกของคุณต้องสามารถใช้แทนคลาสฐานของคุณได้
กล่าวอีกนัยหนึ่ง:
หากคุณสืบทอดจาก Fruit และ color เป็นสตริง คุณไม่ต้องการเปลี่ยน color ให้ส่งคืนสัญลักษณ์ (symbol) ในคลาสลูก
ตัวอย่าง:
ผู้ใช้ของ Fruit ขึ้นอยู่กับ color ที่ส่งคืนสตริง
class Fruit
def color
"orange"
end
end
สิ่งนี้ละเมิด LSP:
class Orange < Fruit
def color
:orange
end
end
หากคุณเปลี่ยน color ให้ส่งคืนสัญลักษณ์ แล้วคุณไม่สามารถแทนที่ออบเจ็กต์ Fruit ด้วย Orange ได้
ทำไม?
เพราะหากคุณเรียกเมธอดเช่น split บนสัญลักษณ์ คุณจะได้ข้อผิดพลาด
เป็นเมธอดที่สัญลักษณ์ไม่มี
แตสตริงมี
สัญญาณเตือนอีกอย่างคือเมื่อคลาสลูกของคุณไม่ใช่การเฉพาะเจาะจงที่แท้จริงของคลาสพาเรนต์ และคุณเพียงแค่ใช้คลาสพาเรนต์เพื่อแบ่งปันเมธอดยูทิลิตี้
สรุป
คุณได้เรียนรู้เกี่ยวกับการสืบทอดและ Composition ใน Ruby แล้ว!
คุณสามารถใช้ การสืบทอด เพื่อสร้างเวอร์ชันเฉพาะเจาะจงของคลาสพาเรนต์ และ Composition เพื่อรวมคอมโพเนนต์เข้าด้วยกันเป็นหนึ่งเดียว จำไว้ว่าต้องปฏิบัติตามหลักการ LSP หากไม่อยากทำให้เป็นเรื่องราว
ตอนนี้คุณสามารถเขียนโค้ด Object-Oriented ที่ดีกว่าได้แล้ว 🙂
ขอบคุณที่อ่านครับ