description |
---|
🤔 Clean Code - การตั้งชื่อของลุงบ๊อบ |
การทำ Clean Code ตอนที่ 2.2 จากวีดีโอของ 🧓 ลุงบ๊อบ หรือ Robert C. Martin หนึ่งในมหาเทพวงการคอมพิวเตอร์ ซึ่งในรอบนี้เราจะมาดูกันว่า ป๋าแกมีมุมมองใน ตัวชื่อของต่างๆ ยังไงกันบ้างถึงจะคลีนกันฮ๊าฟ 😘 ส่วนใครที่ยังไม่ได้อ่านตอนก่อนหน้าก็กดอ่านได้จากลิงค์นี้เบย 🧓 Uncle Bob - Part 2.1****
{% hint style="success" %}
แนะนำให้อ่าน
บทความนี้เป็นส่วนหนึ่งของบทคอร์ส 👶 Clean Code หากเพื่อนๆแมวน้ำสนใจศึกษาเรียนรู้ว่าการทำ Clean Code ว่ามันคืออะไร? มีอะไรบ้าง? บลาๆ ก็สามารถกดที่ชื่อบทความสีฟ้าๆเข้าไปอ่านได้เลยครัช
{% endhint %}
ลุงบ๊อบได้ลองเอาโปรเจคของแกมา 7 ตัว แล้วเอามาวิเคราะห์หาความสัมพันธต่างๆดู แล้วก็พบจุดที่น่าสนใจที่เกี่ยวกับ ไฟล์ในโปรเจค ตามรูปด้านล่าง
จากรูปด้านบนมันแสดงให้เห็นว่า ไม่ว่าโปรเจคจะง่ายหรือยากก็ตาม ขนาดไฟล์ไม่ได้มีผลเลย ซึ่งหมายความว่า โปรเจคที่เราได้รับมา เราจะเขียนโค้ดเยอะๆหรือน้อยๆก็ได้ ตราบใดที่มันทำงานได้
🧓 ขนาดไฟล์และจำนวนบรรทัด - จะมากหรือน้อยมันขึ้นอยู่กับ รูปแบบในการเขียนโค้ดที่ทีมจะเลือกใช้ (Coding Standard) ก็แค่นั้นแหละ
{% hint style="info" %}
Coding Standard
เป็นมาตรฐานที่บังคับให็ทุกคนในทีมต้องทำตามเวลาเขียนโค้ด เพราะทุกคนจะได้เขียนของต่างๆออกมาเหมือนๆกัน เช่น รูปแบบการตั้งชื่อ รูปแบบการคอมเมนต์ รูปแบบการจัดการไฟล์ บลาๆ ซึ่งถามว่าไม่ใช้ได้ป่ะ? คำตอบคือได้นะ แต่ลองจินตนาการว่าเราต้องไปแก้โค้ดของคนอื่น แล้วต่างคนต่างเขียน ต่างคนก็มีสไตล์เป็นของตัวเอง แล้วเรายังอยากจะไปอ่านโค้ดที่มี 3-4 สไตล์ป่ะ? นั่นแหละข้อเสียของการไม่มี Coding Standard
{% endhint %}
ถัดมาลุงแกก็ค้นพบว่า โปรเจคทั้ง 7 ตัวที่ไม่เกี่ยวข้องอะไรกันแต่กลับมี จำนวนตัวอักษรต่อ 1 บรรทัด ใกล้เคียงกันมาก ตามรูปด้านล่าง
จากรูปด้านบนมันแสดงให้เห็นว่า ไม่มีคนชอบโค้ดยาวๆ และจุดที่เหมาะสมในการเขียนโค้ดควร ไม่เกิน 30-40 ตัวอักษร (เราไม่ได้เขียนโค้ดภาษาไทย ดังนั้นอ้างไม่ได้นะเฟร้ย)
🧓 ขนาดหน้าจอไม่มีผล เพราะต่อให้เป็นหน้าจอยาวแค่ไหนก็ตาม เราก็ไม่ชอบโค้ดยาวๆ และ สำหรับจอปรกติ เราก็ไม่ชอบเลื่อนจอซ้ายขวากลับไปกลับมาอยู่ดี และการทำ Wordwrap ก็ยิ่งน่าเกลียด
ชื่อเกี่ยวข้องกับทุกอย่างในโปรเจค ไม่ว่าจะเป็น ชื่อโปรเจค ชื่อโฟเดอร์ ชื่อไฟล์ ชื่อคลาส ชื่อเมธอด ชื่อตัวแปร บลาๆ ซึ่ง 💖 หนึ่งในหัวใจหลักของการทำ Clean Code คือการตั้งชื่อ นี่แหละ
🧓 ถ้าเราไม่ใส่ใจในการตั้งชื่อ เราจะไม่สามารถเขียนโค้ดที่อธิบายตัวมันเองได้เลย
อะไรก็ตามที่ไม่ชัดเจน เราก็ต้องไปใส่คอมเมนต์อธิบายมันต่ออะดิ ตามโค้ดด้านล่าง
int d; // elapsed time in days
การตั้งชื่อตัวแปรให้ ดูที่ Scope ของมัน ซึ่งถ้า Scope สั้นให้ตั้งชื่อสั้น แต่ถ้า Scope ยาวให้ตั้งชื่อยาว ตามตัวอย่างด้านล่าง
{% hint style="info" %}
อธิบายเพิ่มเติม
Scope ในกรณี้นี้หมายถึง Life Cycle ของสิ่งนั้นๆ โดยปรกติของทุกอย่างมันจะมีชีวิตอยู่ได้แค่ภายในวงเล็บปีกกา { } เท่านั้น ซึ่งมันมีชีวิตอยู่มากหรือน้อย ขึ้นอยู่กับว่ายังมีใครใช้หรืออ้างอิงมันอยู่หรือเปล่า ซึ่งถ้าไม่มีแล้ว โดยปรกติเดี๋ยวมันก็จะมีตัว Garbage collector มาคอยทำลายของพวกนั้นทิ้ง เพื่อคืนหน่วยความจำนั่นเอง
{% endhint %}
ถ้าเราต้องการสร้างตัวแปรมารับค่าซักอย่าง ซึ่งมันจะถูกใช้งานแค่ไม่กี่บรรทัดเท่านั้น หรือพูดง่ายๆคือ scope หรือ life cycle มันน้อย เราก็จะตั้งชื่อสั้นๆได้ เพราะจุดที่เรียกใช้มันอยู่ใกล้ๆกับตัวแปรนั้นเรายังจำที่มาที่ไปของมันได้อยู่
var input = Console.ReadLine();
if(input == "something")
{
// ทำไรซักอย่าง
}
ตัวอย่างตัวแปรที่จบภายในบรรทัดเดียว หรือที่เราเรียกกันว่า inline variable ลุงแกจะตั้งชื่อแค่ 1 ตัวอักษรยังได้เลย
return int.TryParse("1", out int v)? v : 0;
ถ้าเราต้องสร้างตัวแปรมารับค่าซักอย่าง ซึ่งมันจะถูกเอาไปใช้งานในบรรทัดไกลๆ หรือ ภายในเมธอดอื่น เราก็ต้องตั้งชื่อมันให้ยาวขึ้น เพราะชื่อมันต้องชัดเจนว่ามันคืออะไร เพราะในจุดที่เรียกใช้ตัวแปรตัวนั้นเราอาจจะจำไม่ได้แล้วว่าตัวแปรนี้มีที่มาที่ไปยังไง
var rawMoneyText = Console.ReadLine();
if(string.IsNullOrWhiteSpace(rawMoneyText))
{
throw new ArgumentNullException();
}
// ผ่านไปอีกซัก 10 บรรทัด
if(int.TryParse(rawMoneyText, out int money))
{
// ทำไรซักอย่าง
}
🧓 สาเหตุที่ลุงให้ตั้งชื่อตาม scope เพราะตอนที่เรียกใช้ตัวแปร เราต้องชัดเจนว่าตัวแปรนั้นคืออะไร ดังนั้นยิ่งมันมีชีวิตอยู่ยาว นั่นหมายความว่าในจุดที่โค้ดเรียกใช้ตัวแปรนั้น เรามีโอกาสสูงมากที่จะลืมที่มาที่ไปของตัวแปรพวกนั้นแล้ว ดังนั้นถ้าชื่อตัวแปรมันบอกเจตนาของมันว่า มันใช้เก็บอะไร เราก็จะไม่พลาดในตอนที่เรียกใช้ยังไงล่ะ
ของพวกนี้จะ ตรงข้ามกับตัวแปร เพราะถ้า Scope สั้นให้ตั้วชื่อยาว แต่ถ้า Scope ยาวให้ตั้งชื่อสั้น และ เราควรจะ ตั้งชื่อเป็น High Level & Abstraction ด้วย โดยลุงแกให้เหตุผลที่ทำแบบนั้นว่า
{% hint style="info" %}
เกร็ดความรู้
High Level & Abstraction หมายถึง เราจะตั้งชื่อเป็น การทำงานระดับบน ไม่ต้องอธิบายว่าจริงๆมันทำงานยังไง เพราะ เวลาอ่านเราจะได้เข้าใจไอเดียว่าของพวกนี้ใช้ทำอะไร โดยไม่ต้องไปสนใจรายละเอียดการทำงานจริงๆของมัน เช่น CreateUser หากเป็น Low Level มันจะเขียนออกมาประมาณว่า InsertNewUserToMongoDB ไรงี้ ซึ่งมันเป็นการตั้งชื่อที่ลงรายละเอียดถึงระดับการทำงานลึกๆละ
{% endhint %}
🧓 สาเหตุที่ Scope ยาวเราจะต้องตั้งชื่อให้สั้นๆ เพราะ โค้ดที่เรียกใช้ของพวกนี้เขาไม่ต้องการรู้การทำงานจริงๆ ดังนั้นชื่อมันเลยควรจะเป็น High Level ยังไงล่ะ ตามตัวอย่างด้านล่างเบย
คลาส & เมธอด
- public class จะนิยมตั้งชื่อสั้นๆ เพราะคนที่เรียกใช้ ไม่ได้สนใจหรอกว่าจริงๆมันจะต่อกับ printer ประเภทไหน แค่รู้ว่าคลาส Printer ทำงานได้กับเครื่องพิมพ์ได้ทุกตัวก็พอ
- public method จะนิยมตั้งชื่อสั้นๆ เพราะคนที่เรียกใช้ก็ไม่สนใจหรอกว่าจริงๆมันต้องทำงานยังไงถึงจะสั่งพิมพ์ได้ แต่รู้ว่าสั่งผ่านเมธอด Print ก็จะพิมพ์เอกสารได้นั่นเอง
public class Printer
{
public void Print() { ... }
}
🧓 สาเหตุที่ Scope สั้นเราจะตั้งชื่อยาวๆ เพราะ โค้ดที่มาเรียกใช้ของพวกนี้ ส่วนใหญ่จะเป็นของที่อยู่ในวงจำกัด และส่วนใหญ่จะอยู่ในระดับ Low Level เรียกใช้กันเองแล้ว ดังนั้นมันเลยต้องตั้งชื่อให้ละเอียดเพื่อความชัดเจน ตามตัวอย่างด้านล่าง
คลาส
โค้ดด้านล่าง เราจะเห็นว่า inner class ในบรรทัดที่ 5 มันถูกใช้แค่ภายใน class Printer เท่านั้น ดังนั้นมันจะต้องถูกตั้งชื่อให้อ่านแล้วรู้เรื่องว่ามันมีไว้เพื่อกำหนดค่าเพจสำหรับเอาไว้พิมพ์ ไม่ได้มีไว้ใช้อย่างอื่นนะ
public class Printer
{
// โค้ดต่างๆของของคลาสนี้
private class PageSetupInformation { ... }
}
เมธอด
โค้ดด้านล่าง เราจะเห็น private method บรรทัดที่ 11~14 มันถูกใช้แค่ภายใน class Printer เท่านั้น ดังนั้นมันจะถูกตั้งชื่อให้อ่านแล้วรู้ว่ามันแต่ละเมธอดมีหน้าที่อะไรที่ชัดเจน ชื่อมันเลยจะยาวเมื่อเทียบกับ เมธอด Print ในบรรทัดที่ 3 เพราะมันถูกเรียกใช้จากภายนอกได้
public class Printer
{
public void Print()
{
connectToPrinter();
checkInks();
checkPapers();
startPrint();
}
private void connectToPrinter() { ... }
private void checkInks() { ... }
private void checkPapers() { ... }
private void startPrint() { ... }
}
คลาสลูก ยิ่งห่างจากชั้นแม่ชื่อยิ่งยาว เพราะ ลักษณะของคลาสลูกมันจะเจาะจงกับตัวงานมากขึ้น ดังนั้น ยิ่งคลาสลูกยาวเท่าไหร่ มันเลยต้องอธิบายตัวมันเองให้ชัดเจนมากยิ่งขึ้นนั่นเอง ลองดูตัวอย่างของตระกูล Exception ดูก็ได้
หลังจากที่เห็นหลักในการตั้งชื่อกันไปละ คราวนี้เรามาลองเอามันมาใช้งานจริงกันดูบ้าง ว่ามันจะช่วยเราได้ยังไงกัน
จากโค้ดด้านล่าง เราจะพบว่าถ้าเราตั้งชื่อที่ไม่สื่อความหมาย มันจะทำให้เราต้องนั่งไล่อ่านโค้ดทุกบรรทัด ซึ่งเมื่อเข้าใจ Algorithm ของมันหมดแล้ว เราก็จะยังไม่รู้อยู่ดีว่าเมธอดนี้มันเอาไว้ทำอะไร
แต่ถ้าเราตั้งชื่อให้มันสื่อความหมายถึง อย่างน้อยเราก็จะรู้ว่าเมธอดนี้มันจะเอา collection ที่โดนมาร์คไว้กลับมาให้ โดยที่เราไม่ต้องไปไล่อ่านโค้ดทีละบรรทัดเลย หรือต่อให้อ่าน มันก็เข้าใจได้ง่ายขึ้นด้วย
ลองดูตัวแปร 2 ตัวด้านล่างดิ๊ว่ามันต่างกันตรงไหน? แล้วตอนเราจะเรียกใช้งาน เราจะเลือกถูกตัวแปรป่ะตอนใช้งาน? แล้วถ้ามันมีมากกว่า 2 ตัวอ่ะ?
var XYZControllerForEfficientHandlingOfStrings
var XYZControllerForEfficientStorageOfStrings
if(x > 50)
{
a = 1;
}
else
{
a = 100;
}
a = x > 50? 1 : 100;
จุดนี้ลุงแกไม่ได้พูดเรื่องนี้หรอก แต่แกบ่นเรื่องตัว lO มันดูแล้วคล้ายกับเลข 1 กับเลข 0 แต่ IDE สมัยใหม่ๆไม่มีปัญหาพวกนี้แล้ว แมวน้ำเลยแถมเรื่องนี้ให้
เจอบ่อยมากในหมู่ เด็กจบใหม่ หรือ คนที่มีประสบการณ์น้อยๆ จะชอบตั้งชื่อตัวแปรประมาณนี้
int a1, a2, a3;
int x, y, z;
ก็ตั้งชื่อให้มันสื่อไปเลย แมวน้ำชอบโดนสวนกลับว่า ก็มันทำงานได้แล้วจะไปเสียเวลาตั้งทำไม? ผมเข้าใจของผมอยู่แล้ว ฮ่วยยยย (เบิดคำซิเว่า) โปรแกรมเมอร์มันทำงานเป็นทีมเฟร้ย ใช่เอ็งเข้าใจเอ็งแก้ได้ แต่คนในทีมไม่เข้าใจมันก็ maintenance ยากขึ้นเฟร้ย !! คิดแล้วโมโหขอบ่นหน่อย
int age, score, grade;
ลองดูโค้ดด้านล่างจะเห็นการต่อท้ายชื่อตัวแปรที่น่ารำคาญ เพราะบางทีเราก็จำไม่ได้ว่ามันย่อมาจากอะไร และ คำว่า Data กับ Info ในกรณีนี้มันต่างกันตรงไหน? แล้วตัวเอาเข้าใจริงๆทั้ง 3 ตัวนี้มันต่างกันยังไง?
Product product;
ProductData pd;
ProductInfo pi;
โค้ดด้านล่างเป็นการใส่คำขึ้นต้นที่น่ารำคาญ เช่นพวก A กับ The ดังนั้นอย่าไปใส่
Product aProduct;
Product theProduct;
ลองดูเมธอด 3 ตัวด้านล่างดูดิ๊ แล้วบอกหน่อยว่าเมธอดบรรทัดที่ 2 กับ 3 มันต่างกันตรงไหน? แล้วทำไมเมธอดบรรทัดที่ 3 ไม่เติม s ล่ะในเมื่อมันได้ collection กลับมา?
public Account GetActiveAccount() { ... }
public List<Account> GetActiveAccounts() { ... }
public List<Account> GetActiveAccountInfo() { ... }
ถ้าสิ่งที่เราตั้งชื่อมันอ่านออกมาเป็นเสียงไม่ได้ มันจะทำให้เรามีปัญหาเวลาจะบอกคนอื่นว่า สิ่งที่มันมีปัญหาคือจุด !@#$%^&* นี้นะ 😑
string ioactpwd; // ไอ้นี่มันอ่านว่ายังไง? ย่อจากอะไร?
🧓 สรุป 2 บทที่ผ่านมา เราพยายามที่จะทำให้ โค้ดอธิบายตัวมันเองได้ โดยการ ไม่เขียนคอมเมนต์ และ ตั้งชื่อให้สื่อความหมาย ซึ่งเราสามารถ Extract method ออกไปได้เรื่อยๆ ตราบใดที่มันอ่านแล้วไม่เป็นภาษาคน ซึ่ง
- ไฟล์จะใหญ่ จะหลายบรรทัด ก็ช่าง(หัว)มัน เพราะเราต้องการโค้ดที่แก้ไขได้ง่าย
- อย่าเขียนโค้ดยาวๆ เพราะไม่มีคนชอบอ่านโค้ดยาวๆ สมองมนุษย์ไม่ได้เก่งเรื่องจำ แต่เก่งเรื่องวิเคราะห์
🐈 ของบางอย่างแมวน้ำก็ไม่ได้ลงรายละเอียดให้นะ เพราะมันถูกเขียนไว้ใน 👶 Clean Code ตัวหลักอยู่แล้ว ดังนั้นถ้าสนใจก็ไปกดอ่านเอาเองเด้อ
🌊 บทความนี้ยังไม่จบอย่างที่บอกว่ามันจะแบ่งเป็น 6 ส่วน นี่เป็นแค่ตอนที่ 2 เท่านั้นเอง ดังนั้นตามอ่านกันยาวๆต่อได้จากบทความหลัก หรือไม่อยากพลาดก็ติดตามได้จาก Facebook: Mr.Saladpuk ฮั๊ฟ
{% hint style="success" %}
แนะนำให้อ่าน
ในบทความถัดๆไปเราจะเจอหลักในการออกแบบเยอะม๊วก ดังนั้นแมวน้ำแนะนำให้รู้จักหลักในการออกแบบพื้นฐาน 5 ตัว โดยเพื่อนๆสามารถไปอ่านได้จากลิงค์นี้ครัช 👦 SOLID Design Principles
{% endhint %}
{% embed url="https://www.youtube.com/watch?v=2a\_ytyt9sf8" %}