Split Variable
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”ตัวแปรหนึ่งตัวควรแทนของสิ่งเดียว พอชื่อเดียวถูกกำหนดค่าใหม่กลางทาง ตอนแรกเก็บเส้นรอบรูป ตอนหลังเก็บพื้นที่ นั่นคือตัวแปรสองตัวที่ใส่เสื้อตัวเดียวกันอยู่ Split Variable แยกให้แต่ละความหมายมีชื่อของตัวเองที่กำหนดค่าครั้งเดียว ผู้อ่านจึงเชื่อได้ว่าชื่อหนึ่งหมายถึงของสิ่งเดียวกันทุกจุดที่เจอ และการกำหนดค่าครั้งที่สองที่เคยเปลี่ยนความหมายก็หายไป
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”smell คือ ตัวแปรที่ถูกกำหนดค่ามากกว่าหนึ่งครั้งเพื่อจุดประสงค์ที่ไม่เกี่ยวกัน ไม่ใช่ตัวสะสมจริง ๆ อย่างตัวนับใน loop หรือผลรวมที่ทยอยบวก แต่เป็นชื่อที่ถูกหยิบมาใช้ซ้ำเป็นช่องเขียนชั่วคราวเพราะสะดวก เบาะแสมีสองอย่าง คือตัวแปรนั้นไม่ใช่ตัวเก็บสะสม เพราะไม่ได้บวกหรือสร้างอะไรข้ามรอบวน และความหมายเปลี่ยนไประหว่างการกำหนดค่าแต่ละครั้ง ทุกครั้งที่คุณต้องเลื่อนขึ้นไปดูแล้วถามว่า “ตอนนี้ตัวนี้เก็บอะไรอยู่” แปลว่าตัวแปรนั้นถูกใช้เกินหน้าที่และควรแยกออก
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”การคำนวณทางฟิสิกส์นำตัวแปร temp ตัวเดียวมาใช้ซ้ำ ทีแรกสำหรับเส้นรอบรูป แล้วจึงสำหรับพื้นที่ ตอนหลัง ผลลัพธ์แต่ละอันได้ตัวแปรที่ตั้งชื่ออย่างชัดเจน กำหนดค่าครั้งเดียว เป็นของตัวเอง
// Beforefunction describe(height: number, width: number): string { let temp = 2 * (height + width); const out: string[] = [`Perimeter: ${temp}`]; temp = height * width; out.push(`Area: ${temp}`); return out.join('\n');}
// Afterfunction describe(height: number, width: number): string { const perimeter = 2 * (height + width); const area = height * width; return [`Perimeter: ${perimeter}`, `Area: ${area}`].join('\n');}# Beforedef describe(height, width): temp = 2 * (height + width) out = [f"Perimeter: {temp}"] temp = height * width out.append(f"Area: {temp}") return "\n".join(out)
# Afterdef describe(height, width): perimeter = 2 * (height + width) area = height * width return "\n".join([f"Perimeter: {perimeter}", f"Area: {area}"])// Beforefunc Describe(height, width float64) string { temp := 2 * (height + width) out := fmt.Sprintf("Perimeter: %g", temp) temp = height * width out += fmt.Sprintf("\nArea: %g", temp) return out}
// Afterfunc Describe(height, width float64) string { perimeter := 2 * (height + width) area := height * width return fmt.Sprintf("Perimeter: %g\nArea: %g", perimeter, area)}// Beforefn describe(height: f64, width: f64) -> String { let mut temp = 2.0 * (height + width); let mut out = format!("Perimeter: {temp}"); temp = height * width; out.push_str(&format!("\nArea: {temp}")); out}
// Afterfn describe(height: f64, width: f64) -> String { let perimeter = 2.0 * (height + width); let area = height * width; format!("Perimeter: {perimeter}\nArea: {area}")}กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- หาการกำหนดค่าครั้งแรก แล้วเปลี่ยนชื่อตัวแปรให้อธิบาย เฉพาะความหมายแรก นั้น ถ้าภาษาอนุญาต ให้ทำเป็นค่าคงที่ไปเลย (
const,letที่ไม่ reassign หรือ:=ที่ไม่เคยกำหนดค่าใหม่) - เปลี่ยนทุกการอ้างอิงระหว่างการกำหนดค่าครั้งแรกและครั้งที่สองให้ใช้ชื่อใหม่
- รัน test
- ทำซ้ำที่การกำหนดค่าครั้งที่สอง: ประกาศตัวแปรใหม่ด้วยชื่อสำหรับความหมายที่สอง แล้วชี้การอ้างอิงหลังจากนั้นไปยังชื่อใหม่
- ทำต่อไปจนกระทั่งแต่ละจุดประสงค์ที่แตกต่างเป็นเจ้าของตัวแปรที่แยกกัน กำหนดค่าครั้งเดียว
- เมื่อตัวแปรที่แก้ค่าได้ตัวเดิมไม่เหลือคนอ่านแล้ว ให้ลบทิ้ง ส่วนใน Rust มักตัด
mutออกได้หมดเลย
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้ Split Variable ทุกครั้งที่ชื่อหนึ่งถูกกำหนดค่าใหม่เพื่อแทนของคนละอย่าง เพราะจะกำจัดบั๊กทั้งตระกูล “ตอนนี้ค่านี้คืออะไร” ออกไป และทำให้แต่ละบรรทัดเข้าใจได้ด้วยตัวเอง ท่านี้ยังเป็นจุดตั้งต้นของ Extract Function ที่พบบ่อย เพราะพอแต่ละค่าอยู่ในตัวแปรที่กำหนดค่าครั้งเดียวของตัวเอง ส่วนที่คำนวณค่านั้นก็ยกออกมาได้สะอาด
ระวังอย่าแยกตัวสะสมที่แท้จริง ตัวนับ loop ผลรวมที่กำลังดำเนินอยู่ หรือตัวสร้างที่เติบโตข้ามรอบการวน ถูกตั้งใจ ให้กำหนดค่าใหม่ และการให้แต่ละขั้นมีชื่อของตัวเองจะไร้สาระ จงแยกเฉพาะเมื่อค่าที่ต่อเนื่องกันหมายถึง สิ่งที่ต่างกัน ต้นทุนของการแยกคือตัวแปรที่ตั้งชื่อเพิ่มอีกหนึ่งตัว ซึ่งแทบจะถูกกว่าความสับสนจากตัวแปรที่ใช้งานเกินหน้าที่เสมอ
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Split Variable เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| variable ถูก reassign เพื่อใช้งานที่ต่างกัน | variable เป็น loop counter ที่ชัดเจน (i, j) |
| ชื่อ variable ไม่สามารถสื่อ intent ได้ชัดเจน | ทั้งสองค่าเกี่ยวข้องกันแน่นและ split แล้วสับสนกว่า |
| tracking bug พบว่า variable มีหลายความหมาย | split จะเพิ่ม variable ที่ used-once และทิ้งทันที |
⚠️ ไม่ควร Split Variable เมื่อ:
- loop variable ที่ทุกคนรู้ว่า
iหมายถึงอะไร- accumulator ที่ชัดเจนว่าสะสมค่าเดียวกันตลอด
- split แล้วต้องส่งผ่านหลาย scope ทำให้ซับซ้อนขึ้น