สิ่งที่ได้เรียนรู้จากการทำ Composite Refactoring
ช่วงเดือนที่ผ่านมาได้มีโอกาสเข้าไปฟัง session เกี่ยวกับการ refactoring มา 2-3 session พบว่าจริง ๆ แล้วแนวทางการทำ refactoring เป็นทักษะมากกว่าเครื่องมือ แล้วมันมีกระบวนการคิดอย่างไร มาดูกัน
Refactoring เป็นทักษะมากกว่าเครื่องมือ
เมื่อ refactoring คือการปรับปรุงโครงสร้างของ code ให้ดีขึ้นโดยที่ไม่เปลี่ยนแปลงพฤติกรรมภายนอกของมัน ปัจจุบัน technology ในการ coding มันไปไกลมากแล้ว ตั้งแต่ program ต่าง ๆ ที่เราใช้เขียน code (IDE, editor) ไปจนถึง Generative AI ต่าง ๆ ที่มาช่วยให้การ refactoring ง่ายดายขึ้น คำถามคือแล้วต่อไปนี้ refactoring มันจะยากตรงไหนล่ะ
ที่ technology มันสามารถช่วยได้คือการ refactoring ในระดับล่างสุดของ code ซึ่งหลังจากทำไป 1 ครั้งแล้วเราก็จะพบว่า
- มันไม่เพียงพอที่จะไปถึงภาพที่เราอยากจะให้มันเป็นจริง ๆ
- มันแสดงภาพที่เราอยากจะให้มันเป็นจริง ๆ แต่ไม่ได้บอกว่าจะไปถึงจุดนั้นต้องทำอย่างไรบ้างเป็นทีละขั้นทีละตอน
การที่จะไปให้ถึงนั้นจะต้องอาศัยกระบวนท่าหลาย ๆ อันผสมผสานกัน หลังจากจบแต่ละขั้นตอนจะต้องไม่เปลี่ยนพฤติกรรมภายนอกและไม่ควรใช้เวลานานเกิน 5 นาที เพื่อให้ได้ code ที่สามารถแก้ไขได้ง่ายเมื่อมี requirement จาก business เข้ามา เมื่อต้องไปพูดคุยกับ business ในการซื้อแนวคิด refactoring เราสามารถขายได้เลยว่า “เนี่ย เราใช้เวลาปรับปรุง code ให้พร้อมต่อ requirement ใหม่ภายใน 5 นาที”
ยกตัวอย่างเช่นเรามี code ของ program เครื่องคิดเลขอยู่โดยที่แต่ละตัวอักษรในเครื่องคิดเลขจะเรียกว่า Node
โดยมี properties คือ Node
ซ้ายกับขวาเพื่อเก็บตัวเลขหรือเครื่องหมาย
จะพบว่า code ส่วนนี้สามารถปรับปรุงได้หลายส่วนเลย ยกตัวอย่างเช่น
- Duplicate code ใน method
display()
- การทำ error handling เมื่อเครื่องหมายไม่ใช่
+
,-
หรือ*
- Class นี้มันทำงานหลายอย่างเพราะรวม logic ของ
Node
ที่เป็นเครื่องหมายกับตัวเลขไว้ด้วยกัน เมื่อมี requirement ในการเพิ่มเครื่องหมายใหม่เข้ามาเราก็จะต้องแก้ code class นี้แน่ ๆ ทำให้ต่อมามันจะเริ่มโตขึ้นไปเรื่อย ๆ อ่านยากเข้าใจยาก (Single Responsibility Principle)
ดังนั้นวิธีการ refactoring คร่าว ๆ คือเราจะต้องแยกส่วน logic ของ Node
ที่เป็นเครื่องหมายกับตัวเลขออกจากกันก่อน ก็จะได้หน้าตาประมาณนี้
พอลองเอาไปเข้า ChatGPT ให้ลอง refactoring ก็พบว่าได้หน้าตามาประมาณนี้
แต่การจะไปถึงจุดที่แยกกันได้จะต้องทำขั้นตอนอะไรบ้างล่ะ แน่นอนว่ามีหลายทางแต่เราควรจะเลือกกินคำเล็ก ๆ ที่แต่ละคำต้องไม่เปลี่ยนพฤติกรรมภายนอกและไม่ควรใช้เวลานานเกิน 5 นาที มีดังนี้
-
เราต้องมีชุดการทดสอบก่อนครับ เพื่อให้แน่ใจว่าหลังจาก refactoring เราจะไม่เปลี่ยนพฤติกรรมภายนอก หากไม่มีแล้วการ refactoring จะยากขึ้นมาก ๆ นะ และหลังจากที่ลงมือ refactor ในแต่ละขั้นตอนแล้วให้ทำการ run ชุดการทดสอบทุกครั้ง เมื่อผ่านแล้วเราสามารถ commit code ได้ถือว่าจบขั้นตอนการ refactoring ทีนี้ก็แล้วแต่เราละว่าอยากให้มันจบขั้นตอนไหนก็อิงตาม requirement ได้เลย
-
สังเกตว่าใน
Node
จะมี constructor อยู่ 2 อันสำหรับเครื่องหมายกับตัวเลข เพื่อจะแยกออกจากกัน ในเมื่อเราไม่สามารถเปลี่ยน constructor ให้ return อีก class นึงได้ เราสามารถเอา logic ของตัวเลขแยกเป็น class ใหม่ชื่อValueNode
แล้วเปลี่ยนชื่อNode
เป็นOperatorNode
สำหรับ logic ของเครื่องหมาย แต่ถ้าลงมือทำปุ๊บ constructor จะถูกย้ายไปด้วยชุดการทดสอบจะต้องแก้เยอะ ดังนั้นขั้นตอนนี้เราแค่ทำการ Replace constructor with factory method ก็พอ หากทำผ่าน IDE แล้วมันก็จะแก้ชุดการทดสอบให้ไปใช้ factory method ด้วย ทีนี้เราก็สามารถโยก constructor ได้ง่ายขึ้นละ -
แยก interface ออกมารวมพฤติกรรมของ
Node
ทั้ง 2 รูปแบบคือจะต้องมีcompute()
และdisplay()
จะเป็นประโยชน์สำหรับNode
ของเครื่องหมายที่มันไม่ต้องสนใจว่าข้างซ้าย-ขวาเป็นNode
ประเภทไหน ใช้แค่ inteface ก็จบ ทีนี้ถ้า interface มีชื่อว่าNode
ก็ต้องแก้ชื่อ class เดิมด้วย (ในตัวอย่างจะเรียกว่าGodNode
ละ) แต่การเอาไปใช้ในชุดการทดสอบให้ใช้Node
เหมือนเดิม -
ถึงเวลาแยก logic ของ
Node
ส่วนตัวเลขออกมา (ในตัวอย่างจะเรียกว่าValueNode
) แต่ว่าตอนนี้GodNode
ยังต้องใช้ propertiesvalue
อยู่ เราเลยต้องทำValueNode
ให้เป็น superclass ของGodNode
ก่อน โดยที่ดึง propertiesvalue
ของGodNode
ออกมาแล้วก็ให้ValueNode
ไปใช้Node
interface ในการย้าย logiccompute()
และdisplay()
ส่วนของตัวเลขออกมาอีกที -
จะพบว่า factory method
valueNode
เราสามารถแทนGodNode
ด้วยValueNode
ได้ ทำให้เราสามารถลบ private constructor 1 ตัวที่เกี่ยวกับตัวเลขในGodNode
ออกไปได้ แยก inheritance ของGodNode
ออกจากValueNode
ได้ และลบ logiccompute()
และdisplay()
ส่วนของValueNode
ออกจากGodNode
ได้ -
เมื่อ
GodNode
มีแต่ logic ของเครื่องหมายอย่างเดียวแล้ว เราก็ปิดท้ายด้วยการเปลี่ยนชื่อ class จากGodNode
เป็นOperatorNode
เป็นอันเสร็จ
สรุป
จากตัวอย่างของ Ultimate calculator จะเห็นได้ว่า
- การ refactor เพื่อเปลี่ยนแปลงโครงสร้างนั้นจะต้องใช้ทักษะในการสังเกต วิเคราะห์ และเลือกใช้กระบวนท่าหลาย ๆ อันผสมผสานกัน (composite) โดยที่ไม่เปลี่ยนแปลงพฤติกรรมภายนอกของมัน เครื่องมือเป็นแค่ตัวช่วยที่จะพาเราไปถึงโครงสร้างใหม่ที่เราอยากให่้เป็นได้เร็วขึ้น
- เราต่อยอดการ refactor ขึ้นไปในระดับ scale ที่ใหญ่ได้ง่ายขึ้น เพราะเราสามารถเลือกที่จะหยุด refactor ได้ทันท่วงทีเมื่อเกิดการเปลี่ยนแปลงด้าน business หลีกเลี่ยงการทำ design เผื่ออนาคตมากจนเกินไป (over-engineering)
- ชุดการทดสอบเป็นหัวใจหลักของการทำ refactoring หากขาดมันไปแล้วเราก็ขาดความมั่นใจไปด้วยว่าหลัง refactor แล้วพฤติกรรมมันยังเป็นแบบเดิมอยู่ไหม
ปล. เรายังสามารถต่อยอดการ refactor ต่อได้ด้วยการกำจัด switch statement ใน method compute()
และ display()
ด้วย Replace Conditional with Polymorphism ได้อีกนะ ไปลองดูกันฮะ