Modules ในภาษา Ruby

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

Modules คืออะไร

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

ในภาษา Ruby เรามักจะใช้โมดูลเพื่อวัตถุประสงค์สองอย่างคือ ใช้ในการจัดกลุ่มของโค้ดเข้าไว้ด้วยกัน และใช้เป็น Mix-in โมดูลเพื่อประกาศเมธอดและนำไปใช้กับคลาส ต่อไปมาดูรูปแบบการประกาศโมดูลในภาษา Ruby

module ModuleName

    # Definition

end

เราจะใช้คำสั่ง module ในการประกาศ ตามด้วยชื่อของโมดูลและจบการประกาศด้วยคำสั่ง end ชื่อของโมดูลควรจะขึ้นต้นด้วยตัวพิมพ์ใหญ่ในรูปแบบของ Camel Case และภายในโมดูลนั้นเราสามารถประกาศเมธอด ค่าคงที่ และคลาสไว้ข้างในได้ นอกจากนี้ เรายังสามารถประกาศโมดูลซ้อนกันได้

Creating modules

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

module.rb
module Company

    class Employee

        attr_accessor(:name, :salary, :department)

        def initialize(name, salary, department)
            @name = name
            @salary = salary
            @department = department
        end

    end

    class Department

        attr_accessor(:name)

        def initialize(name)
            @name = name
        end

    end

end

emp = Company::Employee.new("Mateo", 98000, "Engineering")
print "#{emp.name} works in #{emp.department} department"
puts " and his salary is #{emp.salary}"

departments = [
    Company::Department.new("Engineering"),
    Company::Department.new("Marketing"),
    Company::Department.new("Customer Service")
]

puts "There are #{departments.length} departments in our company"
departments.each { |d|
    puts d.name
}

ในตัวอย่าง เราได้ประกาศโมดูลที่ชื่อว่า Company และเราได้ประกาศสองคลาสอยู่ข้างในโมดูล ได้แก่ คลาส Employee เป็นคลาสของพนักงานในบริษัท และคลาส Department เป็นคลาสของแผนกในบริษัท จะเห็นว่าเนื่องจากทั้งสองคลาสนั้นเป็นคลาสที่ใช้จัดการข้อมูลเกี่ยวกับบริษัท ดังนั้นเราจึงใช้โมดูลในการจัดเก็บทั้งสองคลาสนี้เอาไว้ด้วยกัน

emp = Company::Employee.new("Mateo", 98000, "Engineering")

...

departments = [
    Company::Department.new("Engineering"),
    Company::Department.new("Marketing"),
    Company::Department.new("Customer Service")
]

หลังจากประกาศโมดูลเรียบร้อยแล้ว เรานำคลาสในโมดูลมาใช้งานในการสร้างออบเจ็ค เราสามารถเข้าถึงคลาสในโมดูลด้วยชื่อของโมดูล ตามด้วยเครื่องหมายโคลอน (::) และตามด้วยชื่อของคลาส ในรูปแบบ ModuleName::ClassName

Mateo works in Engineering department and his salary is 98000
There are 3 departments in our company
Engineering
Marketing
Customer Service

นี่เป็นผลลัพธ์การทำงานของโปรแกรม ในการประกาศและใช้งานโมดูลในภาษา Ruby

การประกาศเมธอดในโมดูล

อย่างที่เราได้บอกไปในตอนต้นว่านอกจากคลาสแล้ว คุณยังสามารถประกาศเมธอดและค่าคงที่ไว้ในโมดูลได้ แต่สำหรับการเรียกใช้งานเมธอดในโมดูลจะแตกต่างจากคลาส เราจะต้องเข้าถึงเมธอดในโมดูลในรูปแบบ ModuleName.method_name แทน และเราจะต้องนำเข้าโมดูลด้วยเมธอด include ก่อนจึงจะสามารถใช้งานเมธอดในโมดูลได้ นี่เป็นตัวอย่าง

module M
    def f
        puts "method f"
    end

    def g
        puts "method g"
    end
end

include M
M.f
M.g

ในตัวอย่าง เราได้ประกาศโมดูล M ภายในโมดูลนั้นมีสองเมธอดอยู่ภายในคือเมธอด f และ g ในการใช้งานเมธอดในโมดูลนั้น เราจะใช้เมธอด include ในการโหลดโมดูลเข้ามาในโปรแกรมก่อนที่เราจะสามารถเรียกใช้งานเมธอดทั้งสองได้

method f
method g

นี่เป็นผลลัพธ์การทำงานของโปรแกรม ในการประกาศและเรียกใช้งานเมธอดที่ถูกประกาศไว้ในโมดูล

การนำเข้าโมดูลจากไฟล์อื่นมาใช้งาน

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

ตัวอย่างต่อไป เราจะสร้างโมดูลที่เก็บคลาสเกี่ยวกับรูปร่าง นอกจากนี้เรายังจะประกาศเมธอดและค่าคงที่ภายในโมดูลอีกด้วย นี่เป็นโค้ดตัวอย่าง

shape.rb
module Shape

    DRAW_CHAR = "#"

    class Rectangle

        attr_accessor(:width, :height)

        def initialize(width, height)
            @width = width
            @height = height
        end

        def area
            return @width * @height
        end

    end

    class Triangle

        attr_accessor(:width, :height)

        def initialize(width, height)
            @width = width
            @height = height
        end

        def area
            return @width * @height / 2
        end

    end

    def draw_rectangle(object)
        for i in (0...object.height)
            for j in (0...object.width)
                print DRAW_CHAR
            end
            puts
        end
    end

    def draw_triangle(object)
        w = object.width.to_f
        h = object.height.to_f
        for i in (0...h)
            for j in (0...w)
                if j >= (w / 2) - (w / h / 2 * i) - 1 && 
                    j <= w / 2 + (w / h / 2 * i)
                    print DRAW_CHAR
                else
                    print "-"
                end
            end
            puts
        end
    end

end

ในตัวอย่าง เราได้สร้างโมดูลที่ชื่อว่า Shape ซึ่งโมดูลนี้มีสองคลาสอยู่ภายในคือคลาส Rectangle เป็นคลาสของรูปสี่เหลียม และคลาส Tritangle เป็นคลาสของรูปสามเหลี่ยมหน้าจั่วซึ่งเป็นรูปสามเหลี่ยมที่มีด้านสองด้านที่เท่ากัน

DRAW_CHAR = "#"

สำหรับโค้ดบรรทัดแรกในโมดูล เราได้ประกาศค่าคงที่ DRAW_CHAR ซึ่งเป็นค่าคงที่สำหรับเก็บตัวอักษรที่เอาไว้ใช้วาดรูปสี่เหลี่ยมและรูปสามเหลี่ยมที่สร้างไปจากคลาสที่เราได้บอกไปก่อนหน้า

def draw_rectangle(object)
    ...
end

def draw_tritangle(object)
    ...
end

ในตอนท้ายของโมดูล เราได้ประกาศเมธอดสองเมธอดสำหรับวาดรูป เมธอด draw_rectangle ใช้สำหรับวาดรูปของออบเจ็คที่สร้างจากคลาส Rectangle ส่วนเมธอด draw_tritangle ใช้สำหรับวาดรูปของออบเจ็คที่สร้างจากคลาส Tritangle ภายในเมธอดเราใช้คำสั่ง for loop เพื่อวนวาดรูปและใช้ตัวอักษรจากค่าคงที่ DRAW_CHAR ในการวาด

หลังจากที่เราประกาศโมดูลเสร็จเรียบร้อยแล้ว ต่อไปเราจะสร้างไฟล์ใหม่เพื่อมาเรียกใช้งานโมดูล Shape ของเรา และนี่เป็นโค้ดของโปรแกรม

use_shape.rb
require "./shape"
include Shape

r1 = Shape::Rectangle.new(8, 4)
r2 = Shape::Rectangle.new(4, 4)

t1 = Shape::Triangle.new(11, 5)
t2 = Shape::Triangle.new(8, 8)

puts "Area of a rectangle: #{r1.area}"
Shape.draw_rectangle(r1)
puts "Area of a rectangle: #{r2.area}"
Shape.draw_rectangle(r2)

puts "Area of a triangle: #{t1.area}"
Shape.draw_triangle(t1)
puts "Area of a triangle: #{t2.area}"
Shape.draw_triangle(t2)

puts "All shapes are drawn with #{Shape::DRAW_CHAR} character"

ในตัวอย่าง เราได้สร้างไฟล์ใหม่ที่มีชื่อว่า use_shape.rb เพื่อเรียกใช้งานโมดูลก่อนหน้าที่เราได้ประกาศไปในไฟล์ shape.rb และสมมติว่าเราวางไฟล์ทั้งสองนี้ไว้ในโฟล์เดอร์เดียวกัน

require "./shape"
include Shape

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

r1 = Shape::Rectangle.new(8, 4)
r2 = Shape::Rectangle.new(4, 4)

t1 = Shape::Triangle.new(11, 5)
t2 = Shape::Triangle.new(8, 8)

จากนั้นเราได้นำคลาส Rectangle และ Triangle ในโมดูลมาสร้างออบเจ็คของรูปสี่เหลี่ยมและรูปสามเหลี่ยมหน้าจั่ว เราเข้าถึงคลาสเหมือนกับที่เราทำในตัวอย่างก่อนหน้านั่นคือ MouleName::ClassName นั่นเอง

puts "Area of a rectangle: #{r1.area}"
Shape.draw_rectangle(r1)
puts "Area of a rectangle: #{r2.area}"
Shape.draw_rectangle(r2)

puts "Area of a triangle: #{t1.area}"
Shape.draw_triangle(t1)
puts "Area of a triangle: #{t2.area}"
Shape.draw_triangle(t2)

puts "All shapes are drawn with #{Shape::DRAW_CHAR} character"

สิ่งที่เพิ่มเติมเข้ามาในตัวอย่างนี้ก็คือเราได้ประกาศค่าคงนี้ไว้ในโมดูลด้วย เราสามารถเข้าถึงค่าคงที่ภายในโมดูลดังในรูปแบบ MouleName::Constant ซึ่งจะเหมือนกันกับการเข้าถึงคลาสภายในโมดูล

Area of a rectangle: 32
########
########
########
########
Area of a rectangle: 16
####
####
####
####
Area of a triangle: 27
-----#-----
----###----
---#####---
--#######--
-#########-
Area of a triangle: 32
---##---
---##---
--####--
--####--
-######-
-######-
########
########
All shapes are drawn with # character

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

Nested modules

ในภาษา Ruby เราสามารถประกาศโมดูลที่ซ้อนกันได้ การซ้อนกันของโมดูลจะทำให้เราสามารถจัดกลุ่มของโค้ดย่อยลงไปได้อีก ยกตัวอย่างเช่น

nested_module.rb
module M
    module SUB1

        class A
        end

        class B
        end

    end

    module SUB2

        C = 1

        def f
        end

    end
end

ในตัวอย่าง เราได้ประกาศโมดูล M ซึ่งมีสองโมดูลย่อยอยู่ข้างในคือโมดูล SUB1 และ SUB2 นี่จะทำให้เราสามารถแยกระหว่างการประกาศคลาสและเมธอดออกจากกันได้ ในขณะที่ยังแชร์โมดูลหลักร่วมกันคือโมดูล M นั่นเอง และจากโมดูลของเรา เราสามารถเข้าถึงสิ่งที่ประกาศไว้ในโมดูลได้ดังนี้

# Using class
M::SUB1::A
M::SUB1::B
# Using constant
M::SUB2::C
# Using method
include M::SUB2
M::SUB2.f

และอย่างที่คุณรู้ ในการใช้งานคลาสและค่าคงที่นั้นสามารถเข้าถึงได้โดยตรง ส่วนการใช้งานเมธอดภายในโมดูลเราจะต้องนำเข้ามาด้วยเมธอด include ก่อน

Mix-in modules

Mix-in โมดูลคือการประกาศเมธอดไว้ในโมดูลและเรียกใช้งานโดยคลาส นั่นจะทำให้เมธอดในโมดูลเป็นเหมือนกับว่ามันถูกประกาศไว้ในคลาส การใช้ Mix-in โมดูลจะทำให้คลาสสามารถใช้เมธอดบางอย่างร่วมกันได้ และเมธอดเหล่านั้นจะถูกเปลี่ยนบริษทการทำงานให้เข้ากับคลาสนั้นๆ ในตอนที่โปรแกรมทำงาน

ตัวอย่างของ Mix-in โมดูลในภาษา Ruby ก็คือ Enumerable โมดูล โมดูลนี้ประกอบไปด้วยเมธอดสำหรับวนและค้นหาข้อมูลเป็นจำนวนมาก และมีหลายคลาสที่นำโมดูลนี้เข้าไปใช้งาน เช่น คลาส Array Hash และ Range ยกตัวอย่างเช่น

a = [1, 2, 3, 4, 5]
h = { "th": "Thailand" , 'jp': "Japanse" }
r = (1..10)

a.each { |n|
    puts n
}
h.each_value { |v|
    puts v
}

puts a.min
puts a.max
puts r.min
puts r.max

เราสามารถเรียกใช้งานเมธอดสำหรับวนซ้ำ each และ each_value และเมธอดสำหรับหาค่าน้อยสุดและมากสุดภายในออบเจ็คของอาเรย์ แฮช และ Range ซึ่งเมธอดเหล่านี้เป็นเมธอดที่กำหนดใน Enumerable โมดูล

ต่อไปมาดูตัวอย่างการใช้งาน Mix-in โมดูลในการเขียนโปรแกรม เราจะสร้างคลาสของวิดีโอและเสียงโดยการใช้ประโยชน์จาก Mix-in โมดูล นี่เป็นโค้ดของโปรแกรม

mixin_module.rb
module Controllable

    def play
        puts "#{self.class.name} is playing" 
    end

    def pause
        puts "#{self.class.name} has paused" 
    end

    def stop
        puts "#{self.class.name} has stopped" 
    end

end

class Video

    include Controllable

    def render_image
        puts "Rendering video image"
    end

end

class Audio

    include Controllable

end

v = Video.new
v.play
v.render_image
v.pause
v.stop

a = Audio.new
a.play
a.pause
a.stop

ในตัวอย่าง เราได้สร้างโมดูลที่มีชื่อว่า Controllable โดยโมดูลนี้ประกอบไปด้วยสามเมธอดคือ play pause และ stop สำหรับควบคุมการเล่น หยุดพัก และหยุดเล่นตามลำดับ หลังจากนั้นเราได้สร้างสองคลาสที่นำโมดูลนี้ไปใช้งาน สิ่งที่สามารถสังเกตได้ในตัวอย่างนี้ก็คือแทนที่เราจะประกาศเมธอดเหล่านี้ไว้ในแต่ละคลาส แต่เราประกาศไว้ในโมดูลและนำมันมาเรียกใช้งานแทน นี่จะทำให้เราสามารถสร้างเมธอดเพียงครั้งเดียว และนำไปใช้กับกี่คลาสก็ได้ที่ต้องการมีเมธอดดังกล่าว

def play
    puts "#{self.class.name} is playing" 
end

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

class Video

    include Controllable

    def render_image
        puts "Rendering video image"
    end

end

class Audio

    include Controllable

end

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

การทำเช่นนี้เป็นการผูกเมธอดภายในโมดูลเข้ากับคลาส นั่นทำให้ทั้งคลาส Video และ Audio มีเมธอด play pause และ stop เหมือนกับว่าเราประกาศเมธอดภายในคลาสเหล่านี้ นอกจากนี้ในคลาส Video ก็ยังมีเมธอด render_image เป็นของมันเอง เนื่องจากวิดีโอนั้นต้องมีการแสดงผลภาพด้วย

Video is playing
Rendering video image
Video has paused
Video has stopped
Audio is playing
Audio has paused
Audio has stopped

นี่เป็นผลลัพธ์การทำงานของโปรแกรมในการประกาศและนำ Mix-in โมดูลมาใช้งานกับคลาสในภาษา Ruby

def render_image
    puts "Rendering video image"
end

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

ในบทนี้ คุณได้เรียนรู้เกี่ยวกับโมดูลในภาษา Ruby เราได้ประกาศและนำโมดูลมาใช้งาน และแนะนำการใช้งาน Mix-in โมดูล ซึ่งคือความสามารถในการนำเมธอดไปใช้งานกับหลายๆ คลาสได้โดยการประกาศไว้เพียงทีเดียว