“Loop” ตอนที่ 2 Primitive Obsession — Code Smell

Posted on:April 29, 2025

“คันหลังนะหนุ่ม ลุงจะไปส่งแก๊ส” เห็นได้ชัด ผมไม่ได้โชดดีเหมือนวันก่อน เมื่อถึงตึกผมได้แต่ถอนหายใจเบาๆ อีกครั้งและเดินไปแลกบัตรขึ้นตึกที่ฝ่ายต้อนรับ

“สวัสดีครับ”

“หวัดดีลูป เป็นยังไงบ้างครับ”

“เมื่อวานผม Setup เครื่อง Mac เสร็จแล้วนะครับ”

“ห่ะ ๆ พอดีเลย งั้นเดี๋ยว ลอง Clone Project นี้ไปนั่งอ่านโค้ดดูก่อนแล้วกัน ลองทำให้เครื่องตัวเองรันโปรเจ็คให้ได้ ลองอ่านในไฟล์ README น่าจะมีวิธีอยู่ เดี๋ยวพี่เดินกลับมา ขอแป๊บเดียว”

TWO HOURS LATER ..

“เป็นไง ๆ รันโปรเจกต์ได้ไหม ? ติดปัญหาอีหยังบ่ ?”

“ห้ะ อะไรนะ? อ่ออ รันได้แล้วครับ ตอนนี้ยังไม่ติดปัญหาอะไรนะครับ”

ผมได้แต่คิดในใจว่า ไฟล์ README ที่พี่บอก มีแค่ชื่อ Project ไม่ได้ช่วยอะไรเลย พี่ซอร์สเล่าให้ฟังคร่าวๆ ว่าโปรเจกต์นี้ชื่อ Order-Taking System เป็นระบบที่มีหน้าที่ในการ รับคำสั่งซื้อจากลูกค้า, ตรวจสอบความถูกต้องของข้อมูล, และสร้างรายการคำสั่งซื้อที่พร้อมจะนำไปดำเนินการในขั้นตอนถัดไป เช่น การขนส่ง (Shipping) หรือการวางบิล (Billing) ซึ่งอยากให้ผมเข้ามาช่วยดูแลโปรเจกต์นี้ในช่วงแรก ๆ ความหล่อของโปรเจกต์นี้คือพี่ซอร์สเลือกใช้ Architectural Styles แบบ DDD และใช้ Architectural Pattern เป็น Hexagonal ผมเคยได้ยินมาบ้างๆ ถึงจะยังไม่ค่อยสนิทกันเท่าไรก็ตาม

ระหว่างที่เราสองคนกำลังคุยกัน พี่ซอร์สก็หันไปทักทายผู้หญิงที่กำลังวางกระเป๋าลงบนโต๊ะข้างๆ ที่เราสองคนยืนคุยกัน

“พอดีเลย!! คนนี้ชื่อ กิ๊ฟ ไม่แน่ใจว่าเป็นพี่ เพื่อนหรือเป็นน้องค่อยไปถามกันเองนะ”

“กิ๊ฟ คนนี้ชื่อลูป เป็นน้องใหม่ พี่คิดว่าน่าจะได้อยู่ทีมเรา แต่ขอคิดก่อนนะ วันนี้ฝากเรา Pair กับลูปหน่อยนะ เดี๋ยวพี่ต้องไปแล้ว”

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

“เคย Pair programming มาก่อนไหมคะ”

“ไม่เคยเลยครับ พึ่งเคยได้ยินคำนี้เป็นครั้งแรกจากพี่นี่แหละ”

หลังผมตอบคำถาม นัยน์ตาของกิ๊ฟก็เบิกกว้าง มีสีหน้าจริงจังขึ้นมาทันที กิ๊ฟมองมาที่ผม ใบหน้าของเธอเปล่งประกายระยิบระยับราวกับสปอตไลต์ของโลกใบนี้สาดส่องลงมาที่คนคนนี้เพียงคนเดียว

“Pair Programming ก็คือ การเขียนโปรแกรมเป็นคู่รักนั่นเอง ห่ะๆ หยอก ๆ ง่าย ๆ เลยคือการให้คนสองคนนั่งเขียน Code ด้วยกัน ช่วยได้มากในกรณีพึ่งเข้ามา Join กับทีม ถือเป็นการ On-boarding ไปในตัว เวลามีคำถามหรือติดปัญหาอะไร จะได้มีคนอยู่ข้าง ๆ ค่อยช่วยตอบได้เร็ว ๆ”

“อ่อครับ … กำลังจะถามเลยว่า ทำไมเราต้อง Pair กัน แต่ได้คำตอบแล้ว ฮ่า ๆ”

กิ๊ฟเล่าต่อว่าการ Pair Programming มีหลายรูปแบบนะ แต่วันนี้รูปแบบที่เราจะใช้คือ Driver กับ Navigator เธออธิบายว่า รูปแบบนี้มักจะใช้ในกรณีที่ความชำนาญ ด้าน Technical หรือความรู้ ความเข้าใจเกี่ยวโปรเจกต์ของคู่ Pair ต่างกัน เราจะให้คนที่ชำนาญมากกว่ารับบทบาทเป็น Navigator ค่อยนำทางให้คนอีกคนที่รับหน้าที่เป็นคนจับ Keyboard

“เดี๋ยวกิ๊ฟจะจับเวลาเป็นรอบๆ ตอนที่เรา Pair กัน สักรอบละ 25 นาที เพื่อที่จะเป็นจุด Check Point ให้เราไม่หลงทางระหว่างขับขี่ งั้นเริ่มเลยนะ ลูปค้นหาไฟล์ที่ชื่อว่า Person ให้หน่อย”

class Person {
id: number;
firstName: string;
lastName: string;
landlinePhone: number;
mobilePhone: number;
email: string;
height: number;
weight: number
countryCode: string;
constructor(
id: number,
firstName: string,
...
) {
...
}
}

new Person( 30217, “Boonsong”, “Srithong”, 023745001, 0873712503, “[email protected]”, 165, 61, “BG” );


“ลูปลองดู Code ตรง Class Person ในนี้มี Code Smell ที่ชื่อว่า Primitive Obsession ลูปรู้จัก Code Smell ไหม?”

“Code Smell จากที่ผมเคยอ่านผ่าน ๆ ตามาบ้าง มันคือสัญญาณที่บอกว่าโค้ดชุดนี้อาจจะมีปัญหาได้ในอนาคต อย่างเช่น Dupilcation Code, Long method, Large Class แต่ Primitive Obsession พึ่งเคยได้ยินครับ คืออะไรหรอ?”

“ลูปเห็น Class Person ไหม จะมี Attributes อยู่เนอะ เช่น Email, MobilePhone ส่วนมากจะเป็น Primitive type พื้นฐานอย่าง String หรือ Number ปัญหาคือ ถ้าเราใช้ Type พื้นฐานกับทุก Attributes เราจะเริ่มได้กลิ่นของความยุ่งยากในอนาคตละ ตัวอย่างเช่น”

“ส่วนที่ 1 ค่า landlinePhone ที่มีโอกาสเป็นค่าว่างได้ เนื่องจากไม่ใช่ทุกคนจะมีโทรศัพท์บ้าน การนำค่าไปใช้งาน เราจำเป็นต้องตรวจสอบก่อนว่าเป็นค่าว่างไหม เงื่อนไขในการตรวจสอบจะกระจัดกระจายตามจำนวนการเรียกใช้งาน เพราะว่าเราไม่ได้ Encapsulation พฤติกรรมของข้อมูลเอาไว้”

if(person.landlinePhone != null){ … }


“ส่วนที่ 2 String ก็คือ String ทำให้ Compiler ไม่รู้ว่าเรากำลังอ้างอิงถึง String ของ firstName หรือของ email มีโอกาสจะกำหนดค่าที่ไม่ถูกต้องให้กับ Field ที่ Type เป็น String เหมือนกันได้ เช่น เราสามารถกำหนดค่า email ให้เป็น firstName ได้ ปัญหาตามมาคือ ถ้าเราขยันทำบุญ สวดมนต์ก่อนนอน เราอาจะรู้ตัวตอน Runtest แต่โชคร้าย เป็นคนเทา ๆ อาจจะรู้ตัวตอน Runtime เจอว่าบน Production ลูกค้าโทรแจ้งว่าทำไม Email ของฉันถึงเอาชื่อมาโชว์อ่ะ”

String firstName = “Boonsong”; String email = “[email protected]” email = firstName;


“และส่วนสุดท้ายคือจะมี Business logic และ Business rules กระจัดกระจายไปทั่วโค้ด เช่น เงื่อนไขในการตรวจสอบ countryCode หรือ วิธีการกำหนดค่าให้กับ fullName พอขาดการ Encapsulation การตรวจสอบเงื่อนไขต่าง ๆ มีโอกาสเกิด Duplication Code ตามจำนวนการเรียกใช้งาน”

if (countryCode.includes(person.countryCode) && person.countryCode == “BG”) { … }

const fullName = person.firstName + person.lastName;


“จากที่ฟังกิ๊ฟอธิบายมา ลูปของฟันธงว่าเรามา Refactor Code ส่วนนี้กันเถอะ”

“มุขบอกอายุเลยนะ!! ได้ งั้นเริ่มด้วยการแก้ Primitive type กันก่อนเลย พี่ซอร์สเล่าให้ฟังแล้วเนอะว่าโปรเจคนี้เราใช้ DDD เป็นแนวทางในการออกแบบ ในโลกของ DDD มีสิ่งที่เรียกว่า Value Object เป็นอีกหนึ่งทางเลือกที่ใช้แก้ปัญหานี้ได้ หลักการคือการย้าย Business logic, Business rules และ Validation ต่าง ๆ เข้าไปที่ Object แล้วนำไปแทน Primitive type อย่าง String หรือ Number จริง ๆ จะมีเรื่อง Aggregates และ Entities ที่ลูปต้องเข้าใจอีก เดี๋ยวค่อย ๆ ทำความรู้จักไปทีละอันนะ”

class Person { id: PersonId; name: Name; landline: PhoneNumber; mobile: PhoneNumber; email: EmailAddress; height: Height; weight: Weight; country: CountryCode;

constructor( id: PersonId, name: Name, …) }

new Person(
id:       new PersonId(30217),
name:     new Name("Boonsong", "Srithong"),
landline: PhoneNumber.Parse("023745001"),
mobile:   PhoneNumber.Parse("0873712503"),
email:    EmailAddress.Parse("[email protected]"),
height:   Height.FromMetric(180),
weight:   Weight.FromMetric(180),
country:  CountryCode.Parse("BG")
);

เราเริ่มด้วยการสร้าง Value Object ให้ landline และ mobile เปลี่ยนจาก String type เป็น PhoneNumber แทน จากนั่นก็เพิ่ม Methods ต่าง ๆ ใน Object เช่น การแสดงประเทศที่หมายเลขนั้นอยู่ แสดงประเภทของหมายเลขโทรศัพท์ หมายเลขนี้เป็นโทรศัพท์บ้านหรือโทรศัพท์มือถือ

var phone = PhoneNumber.Parse("+359877123503");
var country = phone.Country;  // "BG"
var phoneType = phone.PhoneType; // "MOBILE"
var isValid = PhoneNumber.IsValid("+972120266680");

อีกตัวอย่างคือ Height เราสามารถเพิ่ม Methods ที่เกี่ยวข้องสำหรับแยกความหน่วยของ Metric (กรัม , กิโลกรัม, ลิตร ฯลฯ) และ Imperial (ปอนด์, ออนซ์, นิ้ว ฯลฯ) เช่น การแปลงจากหน่วยหนึ่งเป็นอีกหน่วยหนึ่ง การนำค่าไปแสดงผล หรือเปรียบเทียบค่าของหน่วยที่แตกต่างกันได้

var heightMetric = Height.Metric(180);
var heightImperial = Height.Imperial(5, 3);
var string1 = heightMetric.ToString();   // "180cm"
var string2 = heightImperial.ToString();            // "5 feet 3 inches"
var string3 = heightMetric.ToImperial().ToString(); // "5 feet 11 inches"
var firstIsHigher = heightMetric > heightImperial;

“งั้นเราควรจะเปลี่ยน Primitive type ให้เป็น Value Object ทั้งหมดเลยไหม ที่ฟังมาดูมีแต่ข้อดี ง่ายต่อการทำความเข้าใจโค้ด ลดโอกาสในการเกิด Duplication Code และอีกอย่างคือเวลาเขียน Test ก็ง่าย ไม่ต้อง Mock อะไร”

“ไม่เสมอไปค่ะ ไม่มีกฎเกณฑ์ตายตัว แต่สำหรับกิ๊ฟจะสังเกตจาก Code ให้โค้ดเป็นคนเล่าเรื่องและค่อย ๆ พาเราไป”

กิ๊ฟเล่าว่าส่วนตัวเธอมักจะสังเกตจาก Pattens ประมาณนี้

อีกอย่างที่เธอเล่าให้ฟังคือ ต้องคำนึงถึงความซับซ้อนของ Project ด้วย ถ้าเป็นระบบที่ทำหน้าที่แค่ CRUD ง่าย ๆ การใช้งาน Primitive type แบบเดิมๆ อาจจะสมเหตุสมผลมากกว่า ไม่ต้องยุ่งยาก อย่าลืมว่าทุกอย่างคือ Trade off ไม่มีทางได้อะไรมา โดยที่ไม่เสียอะไรไป

ผมขอบคุณกิ๊ฟหลังจากที่เรา Pair กันเสร็จ เป็นอีกหนึ่งวันที่ดีของผมเลย อีกเรื่องที่ไม่พูดถึงไม่ได้เลย ถึงจะไม่อยากยอมรับก็เถอะ แต่ท่าทางตอนที่กิ๊ฟอธิบายเกี่ยวกับอะไรสักมันช่างน่ารักจริง ๆ ยิ่งเวลาเธอยิ้มทำให้เนื้อหาเข้าใจง่ายมาก ๆ อีกอย่างเธอเท่ห์เกินนแล้วววววววว!!!

Primitive Obsession — Code Smells

References: