👶 Clean Code

เคยทำความสะอาดโค้ดกันบ้างหรือเปล่า ?

😢 ปัญหา

เคยอ่านโค้ดแล้วกร่นด่าในใจกันไหม "นี่มันโค้ดบ้าอะไร?", "ทำไมเขียนแบบนี้ฟร๊า?", "จะเริ่มจากตรงไหนดี?" บลาๆ ซึ่งถ้าเกิดเหตุการณ์แบบนี้ขึ้นนั่นแสดงว่า เราจะทำงานได้ช้าลง เพราะต้องเสียเวลานั่งทำความเข้าใจเจ้าโค้ดพวกนั้น และเลวร้ายที่สุดคือการที่พบว่า ลบโค้ดพวกนั้นทิ้งแล้วเขียนใหม่ดีกว่า นั่นแสดงว่าทีมต้องเสียเวลาไปทำงานเดิมซ้ำสอง

โค้ด !@#$%^&* พวกนั้นเราเรียกมันว่า Bad Code ซึ่งเราควรจะเปลี่ยนมาเขียน Good Code กัน

❓ ทำไมต้องแก้ Bad Code ?

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

สมมุติว่าเราต้องเพิ่มความสามารถใหม่ให้โค้ดหรือต้องไปแก้ bug กับงานที่มี Bad Code เยอะๆ จะได้อารมณ์ประมาณภาพด้านล่างนี้

เส้นสีเขียว - คือเราต้องเดินไปไหนบ้างถึงจะไปเจอจุดที่ต้องไปเขียนโค้ดจริงๆ โซนสีส้ม - คือจุดที่ต้องเข้าไปเขียนโค้ด

ภาพจาก ronjeffries.com

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

คราวนี้มาดูงานที่มีแต่ Good Code ที่ทุกอย่างเป็นระเบียบเรียบร้อยกันบ้าง

ภาพจาก ronjeffries.com

จะเห็นว่า Good Code ทำให้เราเข้าถึงจุดของงานได้เร็ว และ ลดโอกาสเกิดข้อผิดพลาดลง

🤔 ดูยังไงว่าโค้ดเราดีหรือยัง ?

การดูว่าโค้ดเราดีหรือเปล่าให้ดูจาก คำด่าต่อนาที ตอนทำ Code Review

Code Review คือชั่วโมงที่เราเอาโค้ดทั้งของเราและของคนอื่นๆมากางออก แล้วไล่ดูว่ามีโค้ดจุดไหนที่ต้องปรับแก้ ตรงไหนควรสร้าง code standard บลาๆ ซึ่งวัตถุประสงค์ของมันคือช่วยให้ทีมได้เรียนรู้และไม่ทำโค้ดกากๆออกมา

😄 วิธีแก้ปัญหา

ก็อย่าเขียน Bad Code แต่แรกดิ !! (ไม่บอกก็รู้เฟร้ย 🤣)

การเขียน Good Code นั้นเป็นเรื่องของความเอาใจใส่ในคุณภาพของโค้ด ซึ่งการเขียนโค้ดมันก็เหมือนกับคุณเขียนหนังสือซักเล่มนั่นแหละ ถ้าคุณเขียนแล้วคนอื่นเอาไปอ่านแล้วไม่เข้าใจ มันก็หมายถึงคุณเขียนได้ไม่ดี #โค้ดก็เช่นกัน

งั้นเดี๋ยวเราไปดูมาตรฐานสากลของ Good Code แล้วเทียบกับ Bad Code กันเรย

🔥 การเขียนเงื่อนไข

  • อย่าเขียนเงื่อนไขที่ให้คนอ่านต้องแปลความหมาย เพราะมันจะเสียเวลาทำความเข้าใจ และอาจะเข้าใจผิดได้

ลองดูโค้ดด้านล่างแล้วทำความเข้าใจดูครับว่ามันจะสื่อว่าอะไร ? แล้วลองเปรียบเทียบ Bad Code กับ Good Code ดู

Bad Code

if(Math.Pow(banaCa.Extana + 3 / 1.2, 2) >= Policy && (FullName.StartWith("LUNA") || LastName == "Covan" ))
{
// เข้าผับได้
}
else
{
// เข้าผับไม่ได้
}

Good Code

var canEnterThePub = Math.Pow(banaCa.Extana + 3 / 1.2, 2) >= Policy && (FullName.StartWith("LUNA") || LastName == "Covan" );
if(canEnterThePub)
{
// เข้าผับได้
}
else
{
// เข้าผับไม่ได้
}

ใจความของตัวอย่างคือ เราเข้าใจเงื่อนไขได้เร็วขนาดไหน * Bad Code เราต้องไล่ดูทีละเงื่อนไขทีละตัวใน if เพื่อจะได้รู้ว่ามันกำลังตรวจเรื่องอะไร * Good Code เราไม่ต้องไล่ดูเงื่อนไขเลย เพราะอ่านใน if ปุ๊ปเราก็เข้าใจได้ทันที

🔥 คอมเมนต์

  • อย่าเขียนคอมเมนต์!! ถ้าเขียนคอมเมนต์แสดงว่าโค้ดคุณไม่ได้เข้าใจง่ายพอ คุณเลยต้องเขียนคอมเมนต์ทิ้งไว้ให้ชนรุ่นหลังได้เข้าใจ วิธีแก้คือสร้าง method ที่ทำให้เข้าใจได้ง่ายแทน

  • อย่าคอมเมนต์โค้ดที่เผื่อจะได่ใช้ เพราะคนอื่นไม่รู้สาเหตุหรอกว่าคุณคอมมเมนต์มันไว้ทำไม และเขาจะไม่กล้าลบมันแล้วมันจะกลายเป็นขยะค้างไว้แบบนั้น ลบมันทิ้งซะ เพราะยังไงก็อยู่ใน commit อยู่แล้ว

Bad Code

/// <summary>
/// เปลี่ยนแปลงที่อยู่ผู้ใช้ใหม่
/// </summary>
/// <param name="email">อีเมล์ผู้ใช้</param>
/// <param name="address">ข้อมูลที่อยู่ใหม่</param>
public void UpsertAddress(string email, Address address)
{
// ตรวจสอบหาข้อผิดพลาด
if(address == null)
{
throw new Exception("Address เป็น null");
}
// ค้นหาผู้ใช้และตรวจว่ามีผู้ใช้ในระบบหรือไม่
var user = Database.User.Get(it => it.Email == email);
if(user == null)
{
throw new Exception("หาผู้ใช้ไม่พบ");
}
else
{
address.UserId = user.Id;
}
// ดึงข้อมูลที่อยู่ผู้ใช้มาทำการแก้ไข
if(Database.Address.Get(it => it.UserId == user.Id) == null)
{
Database.Address.Insert(address);
}
else
{
Database.Address.Update(address);
}
// เก็บเอาไว้ก่อนเผื่อได้ใช้ในอนาคต
//if(user.Setting.SendNotificationWhenAddressChanged)
//{
// Email.Send(email, address, "คุณได้ทำการเปลี่ยนที่อยู่ใหม่");
//}
}

Good Code

public void UpsertAddress(string email, Address address)
{
var user = getUserByEmail(email);
updateAddress(user, address);
}
private UserProfile getUserByEmail(string email) ...
private void updateAddress(UserProfile user, Address address) ...

ลดการเสียเวลาในการอ่านโค้ด โดยการสร้าง method เพื่อให้คนอ่านเห็นเป็นภาษามนุษย์มากขึ้น ไม่ใช่ต้องไปไล่อ่านโค้ดเพื่อทำความเข้าใจว่าเจ้า 10 บรรทัดนี้คืออะไร

จากตัวอย่างจะสังเกตุได้ว่า ใน Good Code บรรทัด 3 มันแทนที่ 14-19 ใน Bad Code ได้หมดเลย (และอ่านเป็นภาษามนุษย์ด้วย) ใน Good Code บรรทัด 4 มันแทนที่ 25-33 ใน Bad Code ได้หมดเลย (และอ่านเป็นภาษามนุษย์ด้วย) ส่วน 8-12 และ 20-23 ใน Bad Code มันถูกย้ายไปจัดการใน updateAddress() เรียบร้อยแล้ว และ 35-39 ถ้าไม่ได้ใช้ก็ลบทิ้งซะอย่างเก็บมันไว้ คนอื่นจะไม่กล้ายุ่งกับโค้ดคุณ

🔥 การตั้งชื่อทั่วไป

  • เวลาจะตั้งชื่ออะไรก็แล้ว ต้องตั้งให้มันสื่อความหมาย อ่านแล้วเข้าใจว่ามันมีไว้เพื่ออะไร

  • ชื่อต้อง อ่านออกเสียงได้ ถ้ามันออกเสียงไม่ได้เวลาจะบอกคนอื่นว่ามันผิดที่ไหน คุณจะบอกเขาว่ายังไง?

  • หลีกเลี่ยงคำย่อ เพราะทุกคนย่อคำไม่เหมือนกัน เช่น Student คุณจะย่อว่าอะไร (std? โรคติดต่อทางเพศงั้นเหรอ?)

  • ชื่อควรตั้งชื่อตามพฤติกรรม เช่น ชื่อคลาสควรเป็นคำนาม, ชื่อ method ควรขึ้นด้วยคำกิริยา

  • อย่าตั้งชื่อยาว เพราะมันจะเสียเวลาทำความเข้าใจ

  • อย่าตั้งชื่อเป็นนิเสธ เพราะคนอ่านจะ งง (แค่คำว่านิเสธก็ งง แล้ว)

  • อย่างตั้งชื่อกำกวม เพราะมันจะสับสนว่าตัวไหนเป็นตัวไหนกันแน่

Bad Code

double m; // ตัวแปรตัวนี้ใช้ทำอะไร ?
string actpwd; // ไอ้นี่มันอ่านว่ายังไง? ย่อจากอะไร?
string generatedSaltValueForEncryptTheAccountPasswordForThisUserOnly; // เสียเวลาอ่านจุง
string data; // กำกวมเกิน สรุปมันคือ data เรื่องอะไร?

Good Code

double money; // อ่านแล้วรู้เลยว่าใช้เก็บเรื่องเงิน
string accountPassword; // อ่านแล้วรู้เลยว่าเก็บรหัสผ่านของผู้ใช้
string salt4Password; // อ่านแล้วรู้เลยว่าเก็บรหัสผ่าน
string userData; // อ่านแล้วรู้เลยว่าเก็บข้อมูลผู้ใช้

เกร็ดความรู้ คลีนโค้ดบางสำนักไม่แนะนำให้ตั้งชื่อแบบใส่ตัวเลขแทนคำอ่านนะ เช่น Good Code บรรทัดที่ 3 อาจจะเขียนเต็มๆไปเลยก็ได้ saltForPassword

🔥 การตั้งชื่อ Methods & Functions

กฏในการตั้งชื่อทั่วไปทั้งหมดก็ใช้กับการตั้งชื่อ Methods & Functions ด้วยนะจ๊ะ

  • ขึ้นต้นด้วยคำกิริยา เพราะคนเรียกใช้อ่านแล้วรู้เลยว่าใช้ทำอะไร (คนไทยอาจะไม่มีผลเท่าไหร่แต่ฝรั่งนี่มีสูงเลย)

  • อย่ารับ arguments เยอะ เพราะคนเรียกใช้ method อาจ งง ใส่ผิด และเวลาเพิ่ม/ลดละก็อวกแตก

  • ตั้งชื่อให้สื่อว่างานของมันคืออะไร ไม่งั้นคนเรียกใช้ method จะมีคำถาม

Bad Code

AccountNameChanger(string name) // ต้องอ่านทั้งหมดถึงจะเข้าใจ
CreateAccount(string uName, string pwd, ...) // Arguments จะเยอะไปไหน
ChangeAccountPasswordAndDeleteIfNotValid() // เอ็งจะเปลี่ยนรหัสผ่านหรือจะลบผู้ใช้กันแน่ ?

Good Code

ChangeAccountName(string name) // อ่านแล้วเข้าใจเลยเพราะเห็นคำกิริยาก่อนเพื่อน
CreateAccount(AccountInfo account) // คนเรียกไม่สับสน
ChangeAccountPassword() // อ่านแล้วรู้เลยว่าแค่เปลี่ยนรหัสผ่าน
DeleteAccountIfNoBirthDate() // อ่านแล้วรู้เลยว่าจะลบผู้ใช้กรณีไหน

🔥 Layout

  • ควรจัดวาง Layout ให้ตรงกับมาตรฐานของภาษาที่ใช้ เพราะโค้ดของทั้งทีมจะได้เหมือนกัน และเวลาทำงานกับ Git ก็จะไม่เกิด conflict ด้วย

Bad Code

if( conditionA ){
// Do something-1
}
else if(conditionB)
{
// Do something-2
}
else
// Do something-3
// สรุปเอ็งจะใช้ layout ของ { } และการเว้นวรรคแบบไหนกันแน่?

Good Code

if(conditionA)
{
// Do something-1
}
else if(conditionB)
{
// Do something-2
}
else
{
// Do something-3
}

🎯 บทสรุป

เรื่อง Clean Code มันเป็นเรื่องความเอาใจใส่ในผลงานของตัวเอง ซึ่งผลลัพท์ที่ได้มันจะทำให้เราไม่เสียเวลาหลงทางในเขาวงกต และมันไม่ได้มีแค่เท่านี้หรอก อันนี้เป็นเพียงน้ำจิ้มส่วนนึงเท่านั้น และแต่ละภาษาก็มีหลักในการทำ clean code ในรูปแบบของเขาอยู่เช่นกัน เลยขอดึงเฉพาะตัวที่เป็นกลางๆมาให้ดูก่อน ไว้มีโอกาสจะเอามาเพิ่มให้อ่านนะจุ๊

ทำทันที เรื่องการ Clean Code ห้ามปล่อยทิ้งไว้ เพราะ คุณไม่กลับมาแก้หรอก!! ดังนั้นจงจำไว้ "ทำ-ทัน-ที"

เกร็ดความรู้ มาตรฐานทั้งหมดที่ว่านี้ ในแต่ละภาษามันจะมี Coding Standard ของเขาเองอยู่นะ ลองไปหาอ่านได้ เช่น มาตรฐานของฝั่ง .NET สามารถอ่านได้จากลิงค์นี้ครับ Microsoft document