ในบทนี้เราจะได้รู้จักกับศัพท์ 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 ทั้งหมดนะครับ