DRY Right way part 2: Wrong abstraction
หมายเหตุ: ตอนแรก Part 2 ผมตั้งใจจะเขียนเรื่องการ Refactor การแก้ไขเลย แต่พอเขียนไปเขียนมาซักพักพบว่าผมเกริ่นเรื่อง Wrong abstraction ก่อนน่าจะดีกว่า เนื่องจากมันเป็นฐานปัญหาที่ผมอยากจะกล่าวถึง แล้วน้อยคนจริงๆ จะเขียนเรื่องนี้ในภาษาไทย
Part 1:https://medium.com/@chrisza/dry-right-way-part-1-understanding-dry-3610ef192f84#.fpbf7d46t
จาก Part 1 เรากล่าวถึงปัญหาการพยายามลด Duplication โดยผูกเอาโค้ดที่ไม่ควรจะถูกแก้ร่วมกัน แต่หน้าตาเหมือนกัน ให้มัน Reuse กัน
ผมเชื่อว่าปัญหานี้คงพบทั่วไปใน Legacy code ไม่มากก็น้อย
Developer ที่สร้างโค้ดแบบนี้ขึ้นมา อาจจะทำด้วยเจตนาที่ดี หรืออาจจะเป็นสิ่งที่ดีที่สุดที่เขาทำได้ในเวลานั้นแล้ว
แต่บางทีด้วย Requirement ที่เปลี่ยนแปลงไป หรือสถานการณ์ทาง Business ที่เปลี่ยน ทำให้ไอ้ของสิ่งเดิมที่เราคิดว่ามันควรจะ “อยู่ร่วมกัน” และ “แก้ร่วมกัน” มันกลับมากลายเป็นว่าไม่ควรจะถูกแก้ไขด้วยกันอีกต่อไปแล้ว
ปัญหาแบบนี้ทั่วไปจะเรียกว่า Wrong abstraction
ซึ่งการมี Wrong abstraction แบบนี้อยู่ แล้วไม่แก้ไข ส่งผลให้เวลาแก้ไขโค้ดแล้วมันไปกระทบในจุดที่เราคิดไม่ถึง เพราะโค้ดถูกแชร์กันในส่วนที่เราคาดไม่ถึงว่ามันแชร์กันอยู่
ปัญหา Wrong abstraction เป็นปัญหาที่ใหญ่แต่น้อยคนที่จะพูดถึงเรื่องนี้ จริงๆ ผมว่าเรื่องนี้แม้แต่ใน Global เราก็พึ่งมาตื่นตัวกันช่วงที่ Sandi Metz พูดถึงในคลิปนี้เมื่อปี 2014
“duplication is far cheaper than the wrong abstraction”
Sandi Metz เขียนไว้ว่าหลังจากที่เขาทำงานเป็น Consult ในเรื่อง Coding มาซักพัก นี่เป็น Pattern ที่เขาสังเกตเห็น
- โปรแกรมเมอร์ A เห็นโค้ดซ้ำ
- โปรแกรมเมอร์ A แก้ไขโค้ดซ้ำนั้นออกมาเป็น Class / Function แล้วตั้งชื่อ
ตอนนี้แหละที่เรียกว่าเราสร้าง Abstraction ขึ้นมาแล้ว คือเราสร้างชื่อและคอนเซปต์ขึ้นมาใน Codebase แล้วเราบอกว่าให้ทุกคนอย่าทำตรงๆ ให้มาใช้งาน Abstraction ชื่อนี้แทน
- โปรแกรมเมอร์ A เปลี่ยนโค้ดซ้ำมาใช้ Abstraction นี้
- โปรแกรมเมอร์ A มีความสุข โค้ดนี้สวยงาม ไม่ยาวเกินไป
เวลาผ่านไป
- Requirement ใหม่เข้ามา ที่เกือบจะใช้ Abstraction เดิมที่เคยทำไว้ได้ แค่ต้องแก้ไขนิดหน่อย
- โปรแกรมเมอร์ B ได้รับงานให้เขียนโค้ดตาม Requirement นี้
- โปรแกรมเมอร์ B มีความรู้สึกไม่อยากสร้างโค้ดใหม่ อยาก Reuse Abstraction เดิม แต่เนื่องจากมันแตกต่างกันนิดหน่อย โปรแกรมเมอร์ B เลยใส่ Option parameter เข้าไปใน Abstraction เดิม หรือ Subclass ของเดิมลงมาแล้วแก้ไข
ถึงจุดนี้ Abstraction เดิมที่เป็น Concept ที่ควรถูก Reuse ได้ทุกที่ใน Codebase แล้วได้ผลเหมือนเดิม กลายเป็นว่าทำงานแต่ละที่ให้ผลไม่เหมือนกันแล้ว
เราจะได้โค้ดประมาณนี้
เวลาผ่านไป เกิดเหตุการณ์นี้ซ้ำ 7 รอบ
- Requirement ใหม่เข้ามา เกือบใช้ Abstraction เดิมที่เคยทำไว้ได้ แค่ต้องแก้ไขนิดหน่อย
- โปรแกรมเมอร์คนที่ X รับงานให้เขียนโค้ดตาม Requirement นี้
โค้ดที่ได้ตอนนี้หน้าตาจะเป็นประมาณนี้
ปัญหาของโค้ดนี้คือ เราพยายามมองว่าทุกๆ อย่างเป็น “ใบเสร็จรับเงิน” เหมือนๆ กัน ทั้งๆ ที่จริงๆ ใบเสร็จรับเงินสำหรับแผนก X สำหรับแผนก Y มันแตกต่างกันในการใช้งาน
แล้วพอเราพยายามมองแบบแถให้มัน “เป็น Class เดียวกัน” ให้ได้ เพียงเพราะไม่อยากประกาศ Class ใหม่ อยาก Reuse ของเก่า เราก็จะได้โค้ดแบบนี้ ซึ่งผลที่ได้คือ Public Method ที่ชื่อ calculateSumPrice จะมีความซับซ้อนเต็มไปหมดจนเกิดอาจจะเกิดป่า IF ขึ้นมาได้
แล้วการแก้โค้ดแบบนี้ให้สวยงาม มันยากเสียยิ่งกว่า Refactor โค้ดที่ซ้ำเข้าด้วยกันซะอีก
ถ้าเราย้อนกลับไปคิดดูว่าโค้ดแบบนี้เกิดขึ้นได้อย่างไร
- Programmer A ก็เขียน Invoice class ตามปกติ
- Programmer B ก็เขียนแบบแก้ใส่ IF เข้าไปเล็กน้อยเพราะมันเกือบจะใช้ของเดิมได้เลยอยู่แล้วแค่แก้นิดหน่อยเอง
- Programmer C เห็นว่า นาย Programmer B มองว่าของสองอย่างนี้มันอยู่ที่เดียวกัน ก็เลยเลียนแบบสิ่งที่นาย B ทำ คือ ใส่ IF ไปอีกอันนึง
- Programmer D — X ก็ทำแบบเดียวกันหมด
สัญญาณและความเสียหายของการมี Wrong abstraction
บางท่านถึงจุดนี้อาจจะงงว่าแล้วโค้ดที่ผมให้ดูมันมีปัญหายังไงกันนะ ก็ปกตินี่ ก็ควรใช้ IF กันไป
โค้ดที่มี Wrong abstraction จะมีอาการประมาณนี้
- แก้โค้ดแล้วเทสที่ไม่ควรพังดันพังไปด้วย
- แก้โค้ดแล้วเกิด Bug ในจุดที่เราคิดไม่ถึงไม่น่าเกี่ยวข้องกันได้เกิดขึ้นมา
- มี IF มากเกินไปในโค้ด ทำให้ไล่อ่านยาก
- มี Abstraction ชื่อประหลาดๆ ที่ต้องอธิบายอยู่เยอะมากเวลาจะ Onboarding โปรแกรมเมอร์ใหม่เข้ามา
ทั้งสี่ปัญหานี้เป็นปัญหาที่ใหญ่มากนะครับ ลองคิดดูว่าถ้าเราแก้โค้ดแล้วเรา Unit test ของคลาสที่ไม่น่าจะเกี่ยวข้องกับเราเลยพัง เราจะกล้าแก้ไขโค้ดขนาดไหน
ความ Productive ของโปรแกรมเมอร์มันขึ้นอยู่กับว่าเรามั่นใจจะแก้โค้ดเยอะแค่ไหน
แล้วเราจะมั่นใจได้อย่างไรถ้าแก้โค้ด 1 บรรทัดแล้วเรารู้สึกว่าฉันต้องทดสอบระบบใหม่ทุกเคสทั้งระบบ แบบนี้ไม่ว่าโปรแกรมเมอร์จะเทพขนาดไหนก็ขยับตัวยากครับ ระบบมันทับซ้อนกันมากเกินไป
ยังไม่นับเรื่องที่ว่า Abstraction ประหลาดที่เราพยายามแถตั้งชื่อมันขึ้นมา ทำให้บริบทที่โปรแกรมเมอร์ต้องเรียนรู้มีมากขึ้น
ซึ่งถ้ามันเรียนแล้วเพิ่ม Productivity ได้จริง ผมว่าก็สมควรเรียน (เหมือนผมก็เรียน React, Redux, MVC, Design pattern ผมมั่นใจว่าของพวกนี้ผมเรียนแล้วเขียนโค้ดได้ดีขึ้น เร็วขึ้น ผิดพลาดน้อยลง)
แต่ถ้าต้องเรียนเพียงเพื่อให้ได้เข้าใจโค้ด ผมก็ตั้งคำถามว่า คุ้มมั้ย? บาง Codebase ก็อาจจะคุ้ม บาง Codebase อาจจะตอบว่าไม่คุ้ม ก็คงต้องว่าเคสบายเคสครับ
ผมขอแค่ว่าอยากให้เห็น Trade-off ของมัน ก่อนลงมือสร้าง Abstraction ครับ อย่าเห็นว่ามันเป็น Golden rule ก็พอ
สรุป
บล็อกนี้ผมอยากย้ำเรื่อง Wrong abstraction และการ Reuse โค้ดแบบที่ทำให้ทุกอย่างวุ่นวายยิ่งขึ้นอีกครั้งนึง ว่าฐานของปัญหามันเกิดมาได้อย่างไร
จริงๆ มันก็เกิดจากการที่เราคิดว่าโค้ดมันซ้ำ ไม่อยากเขียนใหม่ อยากใช้ของเดิมเพิ่มเติมเล็กน้อย โดยที่ไม่ได้คำนึงให้ดีว่าจริงๆ ของสองอย่างนี้มัน Represent ชุดความรู้ที่เหมือนกันหรือไม่
ผมเขียนบล็อกชุดนี้ 3 Part (จากเดิม 2 Part) เพื่อกระตุ้นให้เราตระหนักกันมากขึ้นว่า การเห็นโค้ดซ้ำแล้วแยกออกมาเป็น Function / Class มันไม่ใช่เรื่องที่ดีเสมอไป มันมีต้นทุนของมันซึ่งอาจจะทำให้โค้ดเราเละยิ่งกว่าปล่อยให้มันซ้ำกันเสียอีก
duplication is far cheaper than the wrong abstraction
ทริกส่วนตัวที่ผมใช้คือ ถ้าผมไม่แน่ใจ ผมจะรอให้มันซ้ำกันอย่างน้อย 3 ที่ก่อน ถึงจะเริ่ม Abstract ออกไปเป็น Class / Function
การรอให้เกิดซ้ำหลายๆ ที่ ทำให้ความน่าจะเป็นที่ Pattern ที่เราเจอแล้วเราสร้าง Abstraction ขึ้นมา จะรองรับความเปลี่ยนแปลงในอนาคตได้ดี จะยิ่งมีมากขึ้นเรื่อยๆ
เอาล่ะ รอบหน้าจะกลับมาเขียนแล้วว่าเราจะแก้ไข Wrong abstraction ที่ว่าแก้ไขได้ยากได้เย็นเหลือเกินได้อย่างไร ซึ่งวิธีนี้ผมได้แรงบันดาลใจจาก Sandi Metz ซึ่งผมตั้งชื่อว่า Re-duplication หรือพูดบ้านๆ คือ
“กลับไปทำให้โค้ดมัน Duplicate กันใหม่เถอะ”
แล้วหลังจากกลับไปทำให้มัน Duplicate แล้วเราจะ Refactor ใหม่อีกรอบเป็น Abstraction ที่ตรงกับ Pattern ซ้ำๆ ที่เกิดขึ้นจริงๆ หลังจากเรากางโค้ดทั้งหมดมาดูแล้วว่าจริงๆ ตรงไหนบางที่มันเกิดขึ้นซ้ำจริงๆ
ไว้ต่อกันใน Part 3 ครับ
ปลง ถ้าใครสนใจเรื่องนี้อยากดูที่ละเอียดและโปรกว่าผม แนะนำคลิปนี้ครับ