ช่วงเดือนที่ผ่านมาได้มีโอกาสเข้าไปฟัง 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 ซ้ายกับขวาเพื่อเก็บตัวเลขหรือเครื่องหมาย

Ultimate Calculator example

จะพบว่า code ส่วนนี้สามารถปรับปรุงได้หลายส่วนเลย ยกตัวอย่างเช่น

  • Duplicate code ใน method display()
  • การทำ error handling เมื่อเครื่องหมายไม่ใช่ +, - หรือ *
  • Class นี้มันทำงานหลายอย่างเพราะรวม logic ของ Node ที่เป็นเครื่องหมายกับตัวเลขไว้ด้วยกัน เมื่อมี requirement ในการเพิ่มเครื่องหมายใหม่เข้ามาเราก็จะต้องแก้ code class นี้แน่ ๆ ทำให้ต่อมามันจะเริ่มโตขึ้นไปเรื่อย ๆ อ่านยากเข้าใจยาก (Single Responsibility Principle)

ดังนั้นวิธีการ refactoring คร่าว ๆ คือเราจะต้องแยกส่วน logic ของ Node ที่เป็นเครื่องหมายกับตัวเลขออกจากกันก่อน ก็จะได้หน้าตาประมาณนี้

Ultimate Calculator design

พอลองเอาไปเข้า ChatGPT ให้ลอง refactoring ก็พบว่าได้หน้าตามาประมาณนี้

แต่การจะไปถึงจุดที่แยกกันได้จะต้องทำขั้นตอนอะไรบ้างล่ะ แน่นอนว่ามีหลายทางแต่เราควรจะเลือกกินคำเล็ก ๆ ที่แต่ละคำต้องไม่เปลี่ยนพฤติกรรมภายนอกและไม่ควรใช้เวลานานเกิน 5 นาที มีดังนี้

  1. เราต้องมีชุดการทดสอบก่อนครับ เพื่อให้แน่ใจว่าหลังจาก refactoring เราจะไม่เปลี่ยนพฤติกรรมภายนอก หากไม่มีแล้วการ refactoring จะยากขึ้นมาก ๆ นะ และหลังจากที่ลงมือ refactor ในแต่ละขั้นตอนแล้วให้ทำการ run ชุดการทดสอบทุกครั้ง เมื่อผ่านแล้วเราสามารถ commit code ได้ถือว่าจบขั้นตอนการ refactoring ทีนี้ก็แล้วแต่เราละว่าอยากให้มันจบขั้นตอนไหนก็อิงตาม requirement ได้เลย

  2. สังเกตว่าใน Node จะมี constructor อยู่ 2 อันสำหรับเครื่องหมายกับตัวเลข เพื่อจะแยกออกจากกัน ในเมื่อเราไม่สามารถเปลี่ยน constructor ให้ return อีก class นึงได้ เราสามารถเอา logic ของตัวเลขแยกเป็น class ใหม่ชื่อ ValueNode แล้วเปลี่ยนชื่อ Node เป็น OperatorNode สำหรับ logic ของเครื่องหมาย แต่ถ้าลงมือทำปุ๊บ constructor จะถูกย้ายไปด้วยชุดการทดสอบจะต้องแก้เยอะ ดังนั้นขั้นตอนนี้เราแค่ทำการ Replace constructor with factory method ก็พอ หากทำผ่าน IDE แล้วมันก็จะแก้ชุดการทดสอบให้ไปใช้ factory method ด้วย ทีนี้เราก็สามารถโยก constructor ได้ง่ายขึ้นละ

  3. แยก interface ออกมารวมพฤติกรรมของ Node ทั้ง 2 รูปแบบคือจะต้องมี compute() และ display() จะเป็นประโยชน์สำหรับ Node ของเครื่องหมายที่มันไม่ต้องสนใจว่าข้างซ้าย-ขวาเป็น Node ประเภทไหน ใช้แค่ inteface ก็จบ ทีนี้ถ้า interface มีชื่อว่า Node ก็ต้องแก้ชื่อ class เดิมด้วย (ในตัวอย่างจะเรียกว่า GodNode ละ) แต่การเอาไปใช้ในชุดการทดสอบให้ใช้ Node เหมือนเดิม

    Extract interface

  4. ถึงเวลาแยก logic ของ Node ส่วนตัวเลขออกมา (ในตัวอย่างจะเรียกว่า ValueNode) แต่ว่าตอนนี้ GodNode ยังต้องใช้ properties value อยู่ เราเลยต้องทำ ValueNode ให้เป็น superclass ของ GodNode ก่อน โดยที่ดึง properties value ของ GodNode ออกมาแล้วก็ให้ ValueNode ไปใช้ Node interface ในการย้าย logic compute() และ display() ส่วนของตัวเลขออกมาอีกที

    Extract superclass

  5. จะพบว่า factory method valueNode เราสามารถแทน GodNode ด้วย ValueNode ได้ ทำให้เราสามารถลบ private constructor 1 ตัวที่เกี่ยวกับตัวเลขใน GodNode ออกไปได้ แยก inheritance ของ GodNode ออกจาก ValueNode ได้ และลบ logic compute() และ display() ส่วนของ ValueNode ออกจาก GodNode ได้

  6. เมื่อ GodNode มีแต่ logic ของเครื่องหมายอย่างเดียวแล้ว เราก็ปิดท้ายด้วยการเปลี่ยนชื่อ class จาก GodNode เป็น OperatorNode เป็นอันเสร็จ

สรุป

Ultimate Calculator transition

จากตัวอย่างของ Ultimate calculator จะเห็นได้ว่า

  • การ refactor เพื่อเปลี่ยนแปลงโครงสร้างนั้นจะต้องใช้ทักษะในการสังเกต วิเคราะห์ และเลือกใช้กระบวนท่าหลาย ๆ อันผสมผสานกัน (composite) โดยที่ไม่เปลี่ยนแปลงพฤติกรรมภายนอกของมัน เครื่องมือเป็นแค่ตัวช่วยที่จะพาเราไปถึงโครงสร้างใหม่ที่เราอยากให่้เป็นได้เร็วขึ้น
  • เราต่อยอดการ refactor ขึ้นไปในระดับ scale ที่ใหญ่ได้ง่ายขึ้น เพราะเราสามารถเลือกที่จะหยุด refactor ได้ทันท่วงทีเมื่อเกิดการเปลี่ยนแปลงด้าน business หลีกเลี่ยงการทำ design เผื่ออนาคตมากจนเกินไป (over-engineering)
  • ชุดการทดสอบเป็นหัวใจหลักของการทำ refactoring หากขาดมันไปแล้วเราก็ขาดความมั่นใจไปด้วยว่าหลัง refactor แล้วพฤติกรรมมันยังเป็นแบบเดิมอยู่ไหม

ปล. เรายังสามารถต่อยอดการ refactor ต่อได้ด้วยการกำจัด switch statement ใน method compute() และ display() ด้วย Replace Conditional with Polymorphism ได้อีกนะ ไปลองดูกันฮะ