การสืบทอดคลาส ในภาษา Ruby

ในบทนี้ คุณจะได้เรียนรู้เกี่ยวกับคุณสมบัติการสืบทอดในภาษา Ruby เราจะพูดถึงการสร้างคลาสที่สืบทอดมาจากคลาสอื่น และในตอนท้ายของบทเราจะแนะนำให้คุณรู้จักเกี่ยวกับลำดับชั้นของการสืบทอดของคลาสในภาษา Ruby ก่อนที่จะเริ่มนั้น มาทำความเข้าใจกันก่อนว่าการสืบทอดคืออะไร และมันมีความสำคัญอย่างไรในการเขียนโปรแกรม

Inheritance ในภาษา Ruby

การสืบทอดคลาสคืออะไร

การสืบทอด (Inheritance) คือคุณสมบัติของการเขียนโปรแกรมเชิงวัตถุ (OOP) ที่เราสามารถสร้างคลาสที่สืบทอดมาจากคลาสอื่นได้ คลาสที่ถูกสืบทอดนั้นจะเรียกว่าคลาสหลักหรือ Base class ส่วนคลาสที่ได้รับการสืบทอดจะเรียกว่าคลาสย่อยหรือ Sub class การสืบทอดคลาสนั้นเป็นการนำโค้ดกลับมาใช้ใหม่ ในขณะที่เรายังคงสามารถเพิ่มการทำงานให้กับคลาสใหม่ของเราได้ กล่าวคือเมื่อมีการสืบทอดคลาสเกิดขึ้น คลาสย่อยจะมีแอตทริบิวส์และเมธอดเหมือนคลาสหลัก และมากไปกวานั้น มันยังสามารถเพิ่มแอตทริบิวส์และเมธอดของมันเองได้

นี่เป็นรูปแบบของการสืบทอดคลาสในภาษา Ruby

class SubClass < BaseClass

    # Class definitions

end 

ในตัวอย่างนั้นเป็นรูปแบบการสืบทอดคลาสในภาษา Ruby โดยที่ SubClass นั้นเป็นคลาสใหม่ที่เราต้องการสร้าง ตามด้วยเครื่องหมายน้อยกว่า < และตามด้วยชื่อของคลาสหลัก BaseClass ที่เราต้องการสืบทอดมาจาก และสำหรับ BaseClass นั้นต้องเป็นคลาสที่ได้ถูกสร้างไว้ก่อนหน้านี้แล้ว

การสืบทอดคลาส ในภาษา Ruby

หลังจากที่คุณได้ทราบแล้วว่าการสืบทอดคลาสคืออะไร ต่อไปมาดูตัวอย่างการสืบทอดคลาสในภาษา Ruby เราจะสร้างคลาส Animal ที่มีแอตทริบิสต์และเมธอดจำนวนหนึ่งและหลังจากนั้น เราจะสร้างคลาสเพื่อมาสืบทอดจากคลาสนี้ นี่เป็นโค้ดสำหรับตัวอย่างแรกของเรา

animal.rb
class Animal

    attr_accessor(:leg)

    def eat
        puts "I'm eating"
    end

end

class Dog < Animal

    attr_accessor(:color, :breed)

    def bark
        puts "#{@breed} is barking"
    end

    def run
        puts "#{@breed} is running with #{@leg} legs"
    end

end

# Creating objects
d = Dog.new
d.leg = 4
d.color = "Brown"
d.breed = "Husky"

d.eat
d.bark
d.run

ในตัวอย่าง เป็นการสืบทอดคลาสในภาษา Ruby เราได้สร้างคลาส Dog ที่สืบทอดมาจากคลาส Animal ในคลาส Animal เนื่องจากเรารู้ว่าสัตว์จะต้องมีขา ดังนั้นเราประกาศแอตทริบิวต์ leg สำหรับเก็บจำนวนขาของสัตว์ และเนื่องจากสัตว์นั้นสามารถกินอาหารได้ เราจึงมีเมธอด eat สำหรับการกินอาหาร

class Dog < Animal

หลังจากนั้นเราได้สร้างคลาส Dog ที่สืบทอดมาจากคลาส Animal นั่นทำให้คลาสนี้มีแอตทริบิวต์และเมธอดเหมือนกับที่คลาส Animal มี นอกจากนี้ในคลาส Dog ยังมีแอตทริบิวต์เป็นของมันเองได้แก่ color และ breed ซึ่งใช้สำหรับเก็บสายพันธ์ของสุนัข และมีเมธอดสำหรับการเห่าและการวิ่งเพิ่มเข้ามา

d = Dog.new
d.leg = 4
d.color = "Brown"
d.breed = "Husky"

d.eat
d.bark
d.run

ในคำสั่งต่อมาเราได้นำคลาส Dog มาสร้างออบเจ็ค กำหนดแอตทริบิวต์ให้กับออบเจ็ค และหลังจากนั้นเป็นการเรียกใช้งานเมธอดจากออบเจ็ค ซึ่งเมธอด eat นั้นเป็นเมธอดที่คลาส Dog ได้รับการสืบทอดมา ดังนั้นเราสามารถเรียกใช้เหมือนกับว่ามันได้ถูกประกาศอยู่ในคลาส Dog

I'm eating
Husky is barking
Husky is running with 4 legs

นี่เป็นผลลัพธ์การทำงานของโปรแกรม จะเห็นว่าในออบเจ็คของคลาส Dog เราสามารถมีแอตทริบิวต์ leg และเรียกใช้งานเมธอด eat บนออบเจ็คได้ นั่นเป็นเพราะว่านี่เป็นคุณสมบัติที่มันได้รับการสืบทอดนั่นเอง

การใช้งานเมธอด super

ในการสืบทอดคลาสนั้นทั้งแอตทริบิวต์และเมธอดจากคลาสหลักจะถูกสืบทอดมายังคลาสย่อย ยกเว้นคอนสตรัคเตอร์ของคลาสหรือเมธอด initialize ที่จะไม่ถูกสืบทอด ดังนั้นในคลาสใหม่ที่เราสร้างขึ้น เราจะต้องกำหนดคอนสตรัคเตอร์ให้กับคลาสใหม่เสมอ ในตัวอย่างนี้จะแนะนำให้คุณรู้จักกับเมธอด super ซึ่งเป็นเมธอดที่ใช้เรียกเมธอดของคลาสหลัก ที่มีชื่อเดียวกัน

person.rb
class Person

    attr_accessor(:first_name, :last_name)

    def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
    end

    def full_name
        return @first_name + " " + @last_name
    end

end

class Programmer < Person

    attr_accessor(:lang)

    def initialize(first_name, last_name, lang)
        super(first_name, last_name)
        @lang = lang
    end

    def work
        puts "#{full_name} is programming in #{lang}"
    end

end

class Musician < Person

    attr_accessor(:genre)

    def initialize(first_name, last_name, genre)
        super(first_name, last_name)
        @genre = genre
    end

    def play
        puts "#{full_name} is playing #{genre} music"
    end

end

# Creating objects
p1 = Programmer.new("Matteo", "Marcus", "Ruby")
p2 = Programmer.new("Matteo", "Marcus", "C++")
m1 = Musician.new("Taylor", "Swift", "Pop")
m2 = Musician.new("Jonas", "Blue", "Electronic")

# Perform works
p1.work
p2.work
m1.play
m2.play

ในตัวอย่าง เราได้สร้างสามคลาส คลาส Person นั้นเป็นคลาสของคน คลาสนี้มีแอตทริบิวส์สำหรับเก็บชื่อและนามสกุล และเมธอดสำหรับรับชื่อแบบเต็ม เราจะใช้คลาสนี้เป็นคลาสหลักและเราจะสร้างอีกสองคลาสที่จะมาสืบทอดจากคลาสนี้

class Programmer < Person

    attr_accessor(:lang)

    def initialize(first_name, last_name, lang)
        super(first_name, last_name)
        @lang = lang
    end

    def work
        puts "#{full_name} is programming in #{lang}"
    end

end

และนี่เป็นคลาสของโปรแกรมเมอร์ คลาส Programmer นั้นคลาสที่สืบทอดมาจากคลาส Person แอตทริบิวส์และเมธอดที่ประกาศไว้ในคลาสหลักจะถูกสืบทอดมายังคลาสนี้ ยกเว้นสิ่งเดียวที่ไม่สืบทอดมายังคลาส Programmer นั่นคือคอนสตรัคเตอร์หรือเมธอด initialize ในคลาสโปรแกรมเมอร์นั้นเรามีแอตทริบิวส์ lang สำหรับเก็บภาษาที่เขียนโปรแกรม และเมธอด work สำหรับแสดงให้เห็นว่าโปรแกรมเมอร์ทำงานอย่างไร

def initialize(first_name, last_name, lang)
    super(first_name, last_name)
    @lang = lang
end

เนื่องจากคอนสตรัคเตอร์นั้นไม่สืบทอด ดังนั้นเราจึงต้องกำหนดคอนสตรัคเตอร์ให้กับคลาส Programmer ใหม่โดยการรับอาร์กิวเมนต์เป็นชื่อ นามสกุล และภาษาโปรแกรมที่เขียนเข้ามา และภายในคอนสตรัคเตอร์เราต้องการที่จะกำหนดค่าให้กับแอตทริบิวส์ต่างๆ

super(first_name, last_name)

และเนื่องจากการกำหนดแอตทริบิวส์สำหรับชื่อและนามสกุลนั้นเคยถูกทำในคอนสตรัคเตอร์ของคลาสหลักอยู่แล้ว เราสามารถเรียกใช้เมธอด super เพื่อเข้าถึงคอนสตรัคเตอร์ของคลาสหลักได้โดยการส่งอาร์กิวเมนต์ที่สอดคล้องกันเข้าไป สิ่งเดียวที่เราต้องทำในคอนสตรัคเตอร์ของคลาสนี้คือกำหนดค่าให้กับแอตทริบิวส์ lang

class Musician < Person

    attr_accessor(:genre)

    def initialize(first_name, last_name, genre)
        super(first_name, last_name)
        @genre = genre
    end

    def play
        puts "#{full_name} is playing #{genre} music"
    end

end

คลาสที่สองที่สืบทอดมาจากคลาส Person คือคลาส Musician คลาสนี้มีลักษณะการทำงานคล้ายกับคลาสของโปรแกรมเมอร์ ยกเว้นแต่ว่าแอตทริบิวส์ที่เพิ่มเข้ามานั้นจะเป็น genre ที่ใช้เก็บประเภทของเพลงที่นักดนตรีเล่น และเมธอด play เพื่อบอกว่านักดนตรีเล่นเพลงของพวกเขาอย่างไร

# Creating objects
p = Person.new("Simon", "Junior")
p1 = Programmer.new("Matteo", "Marcus", "Ruby")
p2 = Programmer.new("Matteo", "Marcus", "C++")
m1 = Musician.new("Taylor", "Swift", "Pop")
m2 = Musician.new("Jonas", "Blue", "Electronic")

# Perform works
puts "#{p.full_name} is just ordinary person"
p1.work
p2.work
m1.play
m2.play

หลังจากที่เราได้สร้างคลาสเสร็จเรียบร้อยแล้ว เราได้นำคลาสเหล่านั้นมาสร้างออบเจ็คและเรียกใช้งานเมธอดบนออบเจ็คเพื่อดูการทำงานของโปรแกรม สังเกตว่าเรายังคงสามารถสร้างออบเจ็ค p จากคลาส Person ได้แต่ออบเจ็คนี้จะเป็นแค่คนธรรมดาที่ยังไม่มีงานทำนั่นเอง

Simon Junior is just ordinary person
Matteo Marcus is programming in Ruby
Matteo Marcus is programming in C++
Taylor Swift is playing Pop music
Jonas Blue is playing Electronic music

และนี่เป็นผลลัพธ์การทำงานของโปรแกรม ในตัวอย่างนี้ได้แสดงให้คุณเห็นว่าเราสามารถสร้างหลายๆ มาสืบทอดจากคลาส Person คลาสเดียวกันได้ นั่นเป็นเพราะว่าทั้งโปรแกรมเมอร์และนักดนตรีนั้นต่างก็เป็นคนเหมือนกัน ดังนั้นพวกเขาสามารถมีชื่อและนามสกุลเช่นกัน นั่นเป็นเหตุผลว่าทำไมเราได้สืบทอดสองคลาสนี้มาจากคลาส Person

อีกสิ่งหนึ่งเป็นการใช้งานเมธอด super เมธอดนี้ใช้สำหรับเรียกใช้งานเมธอดของคลาสหลัก มันจะเรียกใช้งานเมธอดจากหลักที่มีชื่อเดียวกันกับเมธอดที่เรียกมัน ยกตัวอย่างเช่น ถ้าเราเรียกใช้เมธอด super ในเมธอด work ของคลาส Programmer หมายความว่าเราได้เรียกเมธอด work ของคลาส Person นั่นเอง เราจะพูดเกี่ยวกับการใช้งานเมธอดนี้อีกครั้งในตัวอย่างต่อไป

หมายเหตุ: นอกจากเมธอดคอนสตรัคเตอร์ของคลาสที่ไม่สืบทอดแล้วยังไม่อีกเมธอดประเภทหนึ่งที่ไม่สืบทอดลงมาคลาสย่อย ซึ่งเมธอดที่ว่านั้นเป็นเมธอดที่มีการกำหนดการเข้าถึงเป็น private และเรายังไม่ได้พูดถึงในบทนี้ คุณจะได้เรียนรู้เกี่ยวกับเมธอดที่เป็น private ในบทของ Encapsulation ในภาษา Ruby

Method overriding

การ Override เมธอดคือการกำหนดการทำงานของเมธอดในคลาสย่อยใหม่โดยที่ยังใช้ชื่อเดิมจากคลาสหลัก เมื่อทำเช่นนี้จะทำให้เราไม่สามารถเรียกใช้งานเมธอดจากคลาสหลักจากชื่อที่ถูกเขียนทับได้อีกต่อไป แต่โชคดีที่ในภาษา Ruby เราสามารถใช้เมธอด super สำหรับใช้เรียกเมธอดของคลาสหลัก และเมธอด alias สำหรับกำหนดชื่อเสมือนให้กับเมธอดได้ นี่เป็นตัวอย่างการ การ Override เมธอดในภาษา Ruby

animal2.rb
class Animal

    def move
        puts "Animal is moving"
    end

end

class Dog < Animal

    def move
        super
        puts "Dog is running"
    end 

end

class Bird < Animal

    alias :super_a :move

    def move
        puts "Dog is flying"
    end

    def base_move
        super_a
    end

end

dog = Dog.new
dog.move

bird = Bird.new
bird.base_move
bird.move

ในตัวอย่าง เราได้สร้างคลาสที่ชื่อว่า Animal เช่นเดิม ในคลาสนั้นมีเพียงหนึ่งเมธอดคือเมธอด move ซึ่งเป็นเมธอดสำหรับอธิบายการเคลื่อนที่ของสัตว์ หลังจากนั้นเราสร้างอีกสองคลาสเพื่อสืบทอดจากคลาสนี้

class Dog < Animal

    def move
        super
        puts "Dog is running"
    end 

end

คลาสแรกนั้นเป็นคลาส Dog ซึ่งเป็นคลาสของสุนัขเพื่อเป็นการเจาะจงประเภทของสัตว์ของให้แคบลงมา ในคลาสได้มีการกำหนดการทำงานของเมธอด move ใหม่ เนื่องจากว่าเราต้องการบอกว่าสุนัขนั้นเคลื่อนที่ด้วยการวิ่ง ซึ่งนี่เองเรียกว่าการ override เมธอดในภาษา Ruby หลังจากที่เราได้ทำการ override เมธอดไปแล้ว การเรียกใช้เมธอดนี้จะมีการทำงานใหม่ที่เราได้กำหนดไว้ในคลาสย่อย

def move
    super

อย่างไรก็ตาม ภายในเมธอดของคลาสย่อย เรายังสามารถใช้งานเมธอด super เพื่ออ้างถึงเมธอดจากคลาสหลักที่มันสืบทอดมาจากได้ ในคำสั่งนี้เป็นการเรียกใช้งานเมธอด move ที่อยู่ในคลาส Animal

class Bird < Animal

    alias :super_a :move

    def move
        puts "Dog is flying"
    end

    def base_move
        super_a
    end

end

ต่อมาเราได้สร้างคลาส Bird คลาสนี้ได้รับการสืบทอดมาจากคลาส Animal เช่นเดียวกัน และเราได้ทำการ override เมธอด move ในคลาสนี้ เพื่ออธิบายการเคลื่อนที่ของนกว่ามันเคลื่อนที่ด้วยการบิน

เนื่องจากเราได้ทำการ override เมธอด move ไปแล้วดังนั้นการทำงานของมันจะเปลี่ยนไป อย่างไรก็ตาม ในกรณีที่เรายังต้องการเรียกใช้งานเมธอดจากคลาสหลัก ในขณะที่มีการ override เมธอดใหม่ เราสามารถใช้เมธอด alias เพื่อกำหนดชื่อเสมือนให้กับเมธอดก่อนหน้าได้

alias :super_a :move

...

def base_move
    super_a
end

เนื่องจากเรายังคงต้องการเข้าถึงเมธอด move ของคลาสหลัก ดังนั้นก่อนถึงบรรทัดของการ override เมธอด move เราได้ใช้เมธอด alias เพื่อกำหนดชื่อใหม่ให้กับเมธอด move ในคลาสหลัก ในตอนนี้เราสามารถเรียกใช้งานเมธอดของคลาสหลักได้ด้วยเมธอด base_move แทน ส่วนเมธอด move ในตอนนี้จะหมายถึงเมธอดในคลาสย่อย

dog = Dog.new
dog.move

ต่อมาเราได้สร้างออบเจ็คจากทั้งสองคลาส หลังจากนั้นเรียกใช้งานเมธอด move บนออบเจ็ค Dog จะเห็นว่าเมธอดได้แสดงการแสดงผลของคลาสหลักด้วย เพราะเราได้เรียกใช้เมธอด super ในเมธอดดังกล่าว

bird = Bird.new
bird.base_move
bird.move

สำหรับออบเจ็คของนกจะเห็นว่าเมธอด base_move นั้นเป็นการทำงานจากเมธอดของคลาสหลัก นั่นเป็นเพราะว่าเราได้ตั้งชื่อนี้ซึ่งเป็นชื่อใหม่ให้กับเมธอด move ในคลาส Animal ก่อนที่มันจะถูก override ไปในคลาส Bird

Animal is moving
Dog is running
Animal is moving
Dog is flying

นี่เป็นผลลัพธ์การทำงานของโปรแกรมสำหรับการ override เมธอดในภาษา Ruby และอย่างที่คุณเห็น เราสามารถใช้เมธอด super ในการเรียกใช้เมธอดจากคลาสหลักได้ และเมธอด alias สำหรับกำหนดชื่อเสมือนให้กับเมธอดเพื่อเอาไว้ใช้งานก่อนที่มันจะถูก override ได้

Ruby Object hierarchy

ในภาษา Ruby ทุกคลาสและออบเจ็คนั้นจะได้รับการสืบทอดมาจากคลาส Object โดยปริยาย ซึ่งนี้ถือว่าเป็นสิ่งพื้นฐานในภาษา Ruby เนื่องจากว่า Ruby นั้นเป็นภาษาเขียนโปรแกรมเชิงวัตถุอย่างสมบูรณ์ และทุกอย่างในภาษานั้นแทบจะเป็นออบเจ็คแทบทั้งสิ้น ตัวอย่างต่อไปนี้แสดงให้เห็นว่าทุกคลาสนั้นสืบทอดมาจากคลาส Object ซึ่งเป็นคลาสหลักสูงสุดของทุกคลาส

myclass.rb
class MyClass
end

class A
end

class B < A
end

puts "Parent of MyClass is #{MyClass.superclass}"
puts "Parent of A is #{A.superclass}"
puts "Parent of B is #{B.superclass}"

ในตัวอย่าง เราได้สร้างคลาส MyClass ถึงแม้ว่าในตอนสร้างเราไม่ได้ระบุเฉพาะเจาะจงว่ามันต้องสืบทอดมาจากคลาส Object และถ้าเราไม่ได้กำหนดคลาสที่จะสืบทอด Ruby ทำการสืบทอดจากคลาส Object ให้อัตโนมัติ

ในคลาส A ก็สืบทอดมาจากคลาส Object เช่นเดียวกันเนื่องจากเราไม่ได้กำหนดคล่าที่มันจะต้องสืบทอด ดังนั้นมันจึงสืบทอดมาจากคลาส Object สำหรับคลาส B นั้นสืบทอดจากมาจากคลาส A นั่นก็เท่ากับว่ามันสืบทอดจากคลาส Object ด้วยเช่นกัน นั่นเป็นเพราะว่าคลาสหลักของมันที่เป็นคลาส A นั้นสืบทอดมาจากคลาสดังกล่าว

puts "Parent of MyClass is #{MyClass.superclass}"
puts "Parent of A is #{A.superclass}"
puts "Parent of B is #{B.superclass}"

ในสามคำสั่งต่อมา จะเห็นว่าเราสามารถเรียกใช้งานเมธอด superclass ได้ถึงแม้ว่าเราไม่ได้ประกาศคลาสดังกล่าวในคลาสเหล่านี้ นั่นเป็นเพราะว่าเมธอดนี้เป็นเมธอดที่ถูกประกาศในคลาส Object นั่นเอง มันใช้สำหรับดูว่าคลาสหลักของคลาสปัจจุบันคืออะไร

Parent of MyClass is Object
Parent of A is Object
Parent of B is A

และนี่เป็นผลลัพธ์การทำงานของโปรแกรม ในตัวอย่างนี้แสดงให้เห็นว่าคลาสทั้งในภาษา Ruby นั้นสืบทอดมาจากคลาส Object ซึ่งเป็นคลาสหลักสูงสุดของทุกคลาส

หมายเหตุ: อย่างไรก็ตาม คลาส Object นั้นยังได้สืบทอดมาจากคลาสหลักของมันอีกทีหนึ่ง ซึ่งก็คือคลาส BasicObject และเนื่องจากคลาสนี้มีเมธอดจำนวนไม่มาก และเพื่อให้ง่ายต่อการเข้าใจ เราสมมติว่าคลาสหลักสูงสุดคือคลาส Object นั่นเอง

เราจะมีอีกตัวอย่างเพื่อให้คุณเข้าใจเกี่ยวกับการสืบทอดในภาษา Ruby มากขึ้น

being.rb
class Being
end

b = Being.new

p b.inspect
p b.public_methods

ในตัวอย่าง เราได้สร้างคลาส Being ภายในคลาสนี้มันไม่ได้มีการกำหนดเมธอดใดๆ ไว้เลย จากนั้นเราได้นำคลาสมาสร้างออบเจ็ค จะเห็นว่าเราสามารถเรียกใช้งานเมธอดบนตัวแปรออบเจ็คได้ นั่นเป็นเพราะว่าเมธอดเหล่านี้ถูกประกาศไว้ในคลาส Object ซึ่งเป็นคลาสที่คลาส Being ของเราได้สืบทอดมาจากนั่นเอง

p b.inspect
p b.public_methods

เราเรียกใช้งานเมธอด inspect เพื่อดูคำอธิบายเกี่ยวกับคลาสเป็น String และเมธอด public_methods เพื่อดูว่ามีเมธอดไหนบางที่ออบเจ็คนี้สามารถเรียกใช้งานได้ ซึ่งรายชื่อของเมธอดทั้งหมดที่คุณเห็นนั้นคือเมธอดที่ประกาศไว้ในคลาส Object นั่นเอง ซึ่งจะมีเป็นจำนวนมาก

#<Being:0x0000000004aecb68>
instance_variable_defined?
remove_instance_variable
instance_of?
kind_of?
is_a?
tap
methods
singleton_methods
protected_methods
instance_variables
instance_variable_get
instance_variable_set
...

นี่เป็นผลลัพธ์การทำงานของโปรแกรม และในตอนนี้เราหวังว่าคุณจะเข้าใจเกี่ยวกับลำดับชั้นของการสืบทอดในภาษา Ruby มากขึ้นแล้ว

และนี่เป็นเนื้อหาทั้งหมดสำหรับบทนี้ ในบทนี้ คุณได้เรียนรู้เกี่ยวกับการสืบทอดในภาษา Ruby ซึ่งเป็นคุณสมบัติที่สำคัญในการเขียนโปรแกรมเชิงวัตถุ การสืบทอดคลาสนั้นจะทำให้เราสามารถนำโค้ดกลับมาใช้ใหม่ได้ และนอกจากนี้ คุณได้รู้ว่าทุกคลาสและออบเจ็คในภาษา Ruby นั้นสืบทอดจากคลาส Object ซึ่งเป็นคลาสหลักสูงสุดของทุกคลาส