Immutable ตัวแปรที่เปลี่ยนค่าไม่ได้!

ในบทนี้เราจะได้รู้จักกับศัพท์ 2 คำคือ "Mutable" ซึ่งแปลว่า เปลี่ยนแปลงได้, ไม่แน่นอน, แปรผัน แต่เมื่อมันมาอยู่ในเรื่องการเขียนโปรแกรม มันจะหมายถึงชนิดของตัวแปรที่สามารถเปลี่ยนแปลงค่าได้นั่นเอง

อ่านมาถึงตรงนี้ ถ้าใครเพิ่งหักเขียนโปรแกรมอาจจะมีการ เอ๊ะ!? ขึ้นมาในใจได้ ว่าตัวแปรหรือ variable เนี่ยมันก็ต้องเปลี่ยนค่าได้อยู่แล้วไม่ใช่เหรอยังไง ถ้าเปลี่ยนค่าไม่ได้เราจะคำนวณผลได้ยังไง

ประเด็นก็คือมันมีตัวแปรอีกแบบหนึ่งที่ตรงข้ามกับ mutable นั่นคือตัวแปรประเภท "Immutable" (ภาษาอังกฤษ เติม im- เข้าไปข้ามหน้าส่วนใหญ่จะทำให้ความหมายกลับเป็นตรงกันข้าม) แปลว่าเปลี่ยนแปลงไม่ได้ยังไงล่ะ

ตัวแปรที่เป็น immutable นั้นจะกำหนดค่าได้ครั้งเดียวเท่านั้น หลังจากนั้นตัวแปรจะอยู่ในสถานะ Read-Only ไปจนกว่าตัวแปรนั้นจะถูกทำลาย

ในแต่ละภาษามีอาจจะมีการกำหนดว่าตัวแปรไหนเป็น immutable ที่ต่างกัน เช่น

Language Mutable Immutable
JavaScript let x หรือ var x const x
Java int x final int x
C# int x readonly int x
Kotlin var x val x
Swift var x let x
Dart var x final x

(ใครต้องเขียนหลายภาษาบอกเลยว่ามีงงแน่นอน ฮา)

คำเตือน! ในบางภาษา, ตัวแปร immutalbe นั้นอาจจะถูกแบ่งเป็น 2 ประเภทย่อยอีก นั่นคือ immutable แบบ runtime (ค่าสร้างตอนรันโปรแกรม จะคำนวณมาจากค่าอื่นอีกทีก็ได้) และ compile-time (ค่าสร้างตั้งแต่ตอนคอมไพล์ ต้องเป็นค่า constant เท่านั้น เช่น PI = 3.14) ดังนั้นใช้งานภาษาอะไรอยู่ อ่านdocของภาษานั้นดีๆ ด้วยล่ะ!

ทำไมต้อง Immutable ?

ในมุมมองโปรแกรมเมอร์ทั่วไป เราสามารถเปลี่ยนแปลงค่าตัวแปรยังไงก็ได้

var x = 100
x = x + 1

เช่น สร้าง x มาแล้ว อยากเพิ่มค่าให้ x อีก 1 ก็สั่ง +1 เข้าไป (เคสนี้เจอบ่อยเวลาสร้างลูป ตรง i++ ยังไงล่ะ)

ในเคสนี้ ถ้าเราเปลี่ยนค่าตัวแปรไม่ได้ เราก็ต้องเขียนโค้ดแบบนี้แทน

var x = 100
var y = x + 1

//หรือ
var x = 100
var y = add(x, 1)

ซึ่งดูแล้วหาประโยชน์อะไรไม่ได้เลย! โอเค นี่คงไม่ใช่ตัวอย่างที่ดีนักสำหรับการใช้ immutable งั้นเราลองมาดูตัวอย่าง use case ที่เราควรกำหนดตัวแปรเป็น immutable กันดีกว่า..

1.สร้างตัวแปรค่าคงที่ กันเผลอไปเปลี่ยนโดนไม่ตั้งใจ

class ParkingFee {
    int ratePerHour = 10
    ...
}

สมมุติเราสร้างตัวแปรซึ่งเป็นค่าคงที่ ไม่สามารถเปลี่ยนแปลงได้ขึ้นมาตัวหนึ่ง

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

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

เดี๋ยวนี้โปรแกรมเมอร์ใช้งาน IDE กันเยอะ การมีโค้ดเด้งๆ ขึ้นมาเป็นเรื่องปกติ บางคนเพื่อนๆ คุณอาจจะไม่รู้ แต่เมื่อมีโค้ดเด้งขึ้นมาให้ ฉันก็นึกว่ามันใช้งานได้น่ะสิ!

class ParkingFee {
    final int ratePerHour = 10
    ...
}

ดังนั้น ทางที่ดีเราควรกำหนดความเป็น immutable ให้ตัวแปรที่ไม่ต้องการให้เปลี่ยนค่าลงไปตรงๆ เลยจะดีกว่า

2.ลดการเกิดบั๊กจากการเปลี่ยนค่าตัวแปรโดยไม่ตั้งใจ

ลองดูโค้ดข้างล่างนี้หน่อย แล้วลองเดาดูว่า s.score ที่บรรทัดสุดท้ายจะมีค่าเป็นเท่าไหร่?

Student s = Student()
s.name = "Ann"
s.score = 50

displayStudentScore(s)

s.score = ?

หลายคนน่าจะเดาว่ามันจะได้ 50 เนื่องจากเรากำหนดค่า s.score = 50 ตั้งแต่ข้างบน ถึงแม้มันจะผ่านฟังก์ชันที่เอาค่าเราไปปริ้น แต่ค่าก็ควรเป็น 50 เท่าเดิมสิ?

ใช่ครับ ค่ามันควรจะเป็น 50 เท่าเดิม ถ้าฟังก์ชันปริ้นมันทำอะไรข้างในประมาณนี้

function displayStudentScore(student){
    //Read-Only
    print(`${student.name} get score ${student.score}.`)
}

แต่ถ้ามันมี requirement เข้ามาเพิ่มว่าเราต้องการ "เพิ่มคะแนนให้นักเรียนทุกคน คนละ 10 คะแนน" ล่ะ ... พอดีเหลือเกิ๊น! ที่วันนั้นคุณไม่ว่าง เลยเป็นเพื่อนคุณที่เข้ามาแก้โค้ดนี้ให้แทน ซึ่งเขาก็หาไปหามา แล้วก็เจอว่า ถ้าอยากให้คะแนนเพิ่ม งั้นก็บวกมันเพิ่มไปตรงนี้ละกัน

แบบนี้...

function displayStudentScore(student){

    //Mutable!: bonus score 10 point
    student.score += 10

    print(`${student.name} get score ${student.score}.`)
}

เนื่องจาก object ทุกตัวเป็น reference type ดังนั้นการแก้ค่าของอ็อบเจคในฟังก์ชันก็จะส่งผลออกมาถึงด้านนอกด้วย นั่นแหละ ทำให้ค่า s.score ของเราอาจจะไม่ใช่ 50 แล้วก็ได้เมื่อมันโดนส่งค่าผ่านฟังก์ชันเข้าไป

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

3.Shared Memory

หัวข้อนี้ไม่ได้เป็นประโยชน์ทำให้เราเขียนโค้ดดีขึ้น แต่ทำให้เราประหยัดการใช้เมโมรี่มากขึ้น

นั่นคือถ้าเรามีตัวแปร immutable อยู่ แล้วเราสร้างตัวแปรใหม่ขึ้นมาอีกตัวโดยให้มันถือตัวแปร immutable ของเราไว้ด้วย บางภาษาที่ตัว compiler ฉลาดๆ หน่อยจะมองออกว่าไม่ต้อง "copy" ตัวแปรออกมาใหม่อีกตัว สามารถใช้งานแชร์กันได้เลย เพราะถึงยังไงตัวแปรพวกนี้ก็เปลี่ยนค่าไม่ได้อยู่แล้ว

หรืออีกตัวอย่างหนึ่ง เช่นคลาส String ในภาษา Java (String ใน Java เป็นคลาสแบบ immutable นะ)

ถ้าเรากำหนดค่าตัวแปรขึ้นมา 2 ตัวในหัวเราจะคิดว่ามันต้องสร้าง instance ขึ้นมา 2 ชุด แต่ด้วยความเป็น immutable นั้นทำให้คอมไพเลอร์จะมีการเช็กว่าถ้า instance 2 ตัวนั้นมีค่าเหมือนกัน มันจะจัดตัวแปร 2 ตัวนั้นแชร์ค่ากันทันที

ทำการทดลองได้ด้วยลองสร้าง String 2ตัวที่มีค่าเหมือนกัน แล้วลองเช็กว่ามันเป็นตัวเดียวกันมั้ยด้วย == ที่ปกติแล้วจะได้ค่า false เสมอ (ปกติ Java ต้องเทียบสตริงด้วย equals() เท่านั้น)

ในตัวอย่างต่อไปขออธิบายด้วยภาษา Java นะ .. เพราะมันเป็นภาษาที่เคร่งแนวคิด OOP ประมาณนึงเลย

OOP และการใช้ getter/setter

ตามปกติแล้ว สำหรับคนที่เรียน OOP มาจะมีคอนเซ็ปหนึ่งที่เราต้องทำกันตลอดเวลา นั่นคือ getter และ setter

  • เพราะตามหลัก OOP เราจะต้องป้องกันไม่ให้คนอื่นเข้ามายุ่งกับตัวแปรภายในของเรา (กันคนแอบเปลี่ยนค่า) = เราเลยมักเซ็ต properties ทั้งหมดให้เป็น private
  • แล้วก็มีปัญหาตามมา นั่นคือถ้าเราล็อกไม่ให้คนอื่นเข้ามาใช้ค่าภายใน แล้วมันจะติดต่อกับอ็อบเจคตัวอื่นได้ยังไงล่ะ
  • วิธีแก้คือสร้างเมธอดขึ้นมา เป็นตัวกลางในการเข้าถึงตัวแปรระหว่างคลาสเรากับคลาสอื่นๆ
class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getScore() {
        return score;
    }
}

เช่นตัวอย่างโค้ดข้างบน เรามี properties 2 ตัวคือ name และ score เราก็ต้องสร้างเมธอดที่เรียกว่า getter (เอาไว้ดึงค่าออกมา) และ setter (เอาไว้เซ็ตค่ากลับเข้าไป)

คนเรียน OOP ทุกคนจะต้องเขียนฟังก์ชันอันน่าเบื่อพวกนี้เยอะมาก เขียนจนชิน และโดยส่วนใหญ่จะเลิกเขียนเองแล้วใช้ IDE สร้างให้ก็ตาม (ฮา)

แต่จริงๆ แล้วนี่เป็นวิธีที่ไม่ดีเท่าไหร่! (อ้าว! แล้วฉันเรียนอะไรมาเนี่ย)

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

ดังนั้น FP จึงแนะนำให้คุณสร้างอ็อบเจคแบบ immutable ซะ

class Student {
    public final String name;
    public final int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

เราประกาศ final ที่ properties ทุกตัวเลย ทีนี้คนอื่นก็จะแก้ค่าพวกนี้ไม่ได้แล้ว

สังเกตต่ออีกอย่างคือเราเอา getter/setter ออกไปด้วย แล้วเปิดให้ properties เป็น public แทน ... ก็ไม่เห็นต้องกลัวใครเปลี่ยนค่าแล้วไง มันแก้ค่าไม่ได้อยู่แล้ว

หลังๆ มานี่เวลาเราสร้าง class เราชอบสร้างในรูปแบบนี้เสมอเลยนะ เลิกเขียนกันทีกับ getter/setter โค้ดดูสะอาดขึ้น แถมเขียนสั้นลงและลดบั๊กอีกด้วย

แต่ก็ไม่จำเป็นนะว่าจะต้องรับค่าเข้ามาตรงๆ เราอาจจะรับค่าเข้ามาแล้วโปรเซสอะไรบางอย่างก่อนก็ยังไง

class Employee {
    public final String email;
    public final String name;

    public Employee(String firstname, String lastname) {
        this.name = firstname + " " + lastname;
        this.email = firstname + "." + lastname.substring(0,2) + "@tamemo.com"
    }
}

Builder Pattern

ปัญหาจริงๆ ของการสร้างอ็อบเจคให้เป็น immutable คือเวลาเราจะแก้ค่าอะไร เราจะต้องสร้างอ็อบเจคใหม่ขึ้นมาเลย

เช่นต้องการ +10 คะแนนให้ Student หนึ่งคน

Student stu1 = new Student("Ann", 50);
Student stu2 = new Student(stu1.name, stu1.score + 10);

ก็ต้องสร้างอ็อบเจคตัวใหม่ แล้ว copy ค่าจากอ็อบเจคตัวเก่ามาใส่ตัวใหม่ให้หมดเลย

เอาจริงมันก็ไม่ใช่ปัญหาหรอก แค่มันเขียนยาววววขึ้น 55

ดังนั้นเราอาจจะเอา "Builder Pattern" มาช่วย ก็จะทำให้การ copy ค่าทำได้ง่ายขึ้น

Builder ซึ่งเป็นหนึ่งใน Design Pattern ซึ่งยังไม่ขอพูดถึงในบทนี้ ในอนาคตจะมาเขียนเกี๋ยวกับเรื่องนี้ต่ออีกทีหนึ่งนะ

class Data {
    int x, y, z, p, q, r ;
}

Data data1 = new Data(1, 2, 3, 4, 5, 6);

// Standard
Data data2 = new Data(
    10, data1.y + 100, data1.z * 2, data1.p, data1.q, data1.r
);

// Builder Pattern
Data data2 = new Data.Builder()
    .from(data1)
    .setX(10)
    .setY(data1.y + 100)
    .setZ(data1.z * 2)
    .build();
}

สรุป

การทำให้ตัวแปรเปลี่ยนค่าไม่ได้ อาจจะทำให้คนที่เพิ่งเรียนเขียนโปรแกรมแปลกใจกับแนวคิดนี้ (เพราะที่มหาลัยส่วนใหญ่จะไม่ได้สอนข้อเสียของ OOP) แต่เชื่อเถอะว่าการที่ตัวแปรเปลี่ยน state ได้น่ะ ทำให้เกิดบั๊กมากมายตามมา

ไม่อย่างนั้นภาษายุคใหม่ที่ออกมาแต่ละตัว คงไม่สร้างชนิดตัวแปรแบบ immutable ออกมาให้เราใช้กันหรอก บางภาษาถ้าตัวแปรนั้นเป็น mutable แล้วคอมไพเลอร์เช็กเจอว่าเราไม่ได้เปลี่ยนค่าอะไร บางทีเจอ warning ด้วยซ้ำ (มันจะบอกว่าเปลี่ยนเป็น immutable เถอะ)

ถ้าเป็นไปได้พยายามใช้ตัวแปรแบบ immutable ทั้งหมดนะครับ

231 Total Views 3 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...