คู่มือเชิงปฏิบัติสำหรับการทำ Refactoring โค้ด Legacy ครอบคลุมการระบุ จัดลำดับความสำคัญ เทคนิค และแนวทางปฏิบัติที่ดีที่สุดเพื่อความทันสมัยและการบำรุงรักษา
ปราบพยศโค้ด Legacy: กลยุทธ์การทำ Refactoring สำหรับโค้ดเก่า
โค้ด Legacy คำนี้มักจะทำให้นึกถึงภาพของระบบที่กว้างใหญ่ซับซ้อน ไม่มีเอกสารประกอบ มี dependency ที่เปราะบาง และความรู้สึกน่าหวาดหวั่นอย่างท่วมท้น นักพัฒนาจำนวนมากทั่วโลกต้องเผชิญกับความท้าทายในการบำรุงรักษาและพัฒนาระบบเหล่านี้ ซึ่งมักมีความสำคัญอย่างยิ่งต่อการดำเนินธุรกิจ คู่มือฉบับสมบูรณ์นี้จะนำเสนอกลยุทธ์เชิงปฏิบัติสำหรับการทำ Refactoring โค้ด Legacy เปลี่ยนแหล่งที่มาของความหงุดหงิดให้เป็นโอกาสในการปรับปรุงให้ทันสมัยและพัฒนาให้ดียิ่งขึ้น
โค้ด Legacy คืออะไร?
ก่อนที่จะลงลึกในเทคนิคการทำ Refactoring เราจำเป็นต้องนิยามความหมายของ "โค้ด Legacy" ก่อน แม้ว่าคำนี้อาจหมายถึงโค้ดที่เก่า แต่คำนิยามที่ละเอียดยิ่งขึ้นจะเน้นไปที่ความสามารถในการบำรุงรักษา Michael Feathers ในหนังสือเล่มสำคัญของเขา "Working Effectively with Legacy Code" นิยามโค้ด Legacy ว่าเป็นโค้ดที่ไม่มีเทส การขาดเทสนี้ทำให้การแก้ไขโค้ดอย่างปลอดภัยโดยไม่ทำให้เกิดข้อผิดพลาดใหม่ (regression) เป็นเรื่องยาก อย่างไรก็ตาม โค้ด Legacy ยังอาจมีลักษณะอื่นๆ ดังนี้:
- ขาดเอกสารประกอบ: นักพัฒนาเดิมอาจย้ายออกไปแล้ว ทิ้งไว้เพียงเอกสารประกอบเพียงเล็กน้อยหรือไม่มีเลย ซึ่งอธิบายถึงสถาปัตยกรรมของระบบ การตัดสินใจในการออกแบบ หรือแม้แต่ฟังก์ชันการทำงานพื้นฐาน
- ความเชื่อมโยงที่ซับซ้อน (Complex Dependencies): โค้ดอาจมีการผูกมัดกันอย่างแน่นหนา (tightly coupled) ทำให้ยากต่อการแยกและแก้ไขส่วนประกอบแต่ละส่วนโดยไม่ส่งผลกระทบต่อส่วนอื่นๆ ของระบบ
- เทคโนโลยีที่ล้าสมัย: โค้ดอาจเขียนด้วยภาษาโปรแกรม เฟรมเวิร์ก หรือไลบรารีที่เก่ากว่าซึ่งไม่ได้รับการสนับสนุนอีกต่อไป ทำให้เกิดความเสี่ยงด้านความปลอดภัยและจำกัดการเข้าถึงเครื่องมือที่ทันสมัย
- คุณภาพโค้ดที่ไม่ดี: โค้ดอาจมีโค้ดที่ซ้ำซ้อน (duplicated code) เมธอดที่ยาวเกินไป (long methods) และ code smells อื่นๆ ที่ทำให้เข้าใจและบำรุงรักษาได้ยาก
- การออกแบบที่เปราะบาง: การเปลี่ยนแปลงที่ดูเหมือนเล็กน้อยอาจส่งผลกระทบที่ไม่คาดคิดและเป็นวงกว้าง
สิ่งสำคัญที่ควรทราบคือโค้ด Legacy ไม่ได้แย่เสมอไป บ่อยครั้งมันแสดงถึงการลงทุนที่สำคัญและรวบรวมความรู้เฉพาะทางที่มีค่าเอาไว้ เป้าหมายของการทำ Refactoring คือการรักษาคุณค่านี้ไว้พร้อมกับปรับปรุงความสามารถในการบำรุงรักษา ความน่าเชื่อถือ และประสิทธิภาพของโค้ด
ทำไมต้องทำ Refactoring โค้ด Legacy?
การทำ Refactoring โค้ด Legacy อาจเป็นงานที่น่ากลัว แต่ประโยชน์ที่ได้รับมักจะมากกว่าความท้าทาย นี่คือเหตุผลสำคัญบางประการที่ควรลงทุนในการทำ Refactoring:
- ปรับปรุงความสามารถในการบำรุงรักษา: การทำ Refactoring ทำให้โค้ดเข้าใจ แก้ไข และดีบักได้ง่ายขึ้น ลดต้นทุนและความพยายามที่ต้องใช้ในการบำรุงรักษาอย่างต่อเนื่อง สำหรับทีมงานระดับโลก สิ่งนี้มีความสำคัญอย่างยิ่ง เนื่องจากช่วยลดการพึ่งพาบุคคลใดบุคคลหนึ่งและส่งเสริมการแบ่งปันความรู้
- ลดหนี้ทางเทคนิค (Technical Debt): หนี้ทางเทคนิคหมายถึงต้นทุนโดยนัยของการทำงานซ้ำที่เกิดจากการเลือกวิธีแก้ปัญหาง่ายๆ ในปัจจุบันแทนที่จะใช้วิธีที่ดีกว่าซึ่งใช้เวลานานกว่า การทำ Refactoring ช่วยชำระหนี้สินนี้ ปรับปรุงสุขภาพโดยรวมของโค้ดเบส
- เพิ่มความน่าเชื่อถือ: ด้วยการจัดการกับ code smells และปรับปรุงโครงสร้างของโค้ด การทำ Refactoring สามารถลดความเสี่ยงของข้อบกพร่อง (bugs) และปรับปรุงความน่าเชื่อถือโดยรวมของระบบได้
- เพิ่มประสิทธิภาพ: การทำ Refactoring สามารถระบุและแก้ไขปัญหาคอขวดด้านประสิทธิภาพ ส่งผลให้เวลาในการประมวลผลเร็วขึ้นและการตอบสนองดีขึ้น
- การเชื่อมต่อที่ง่ายขึ้น: การทำ Refactoring สามารถทำให้การเชื่อมต่อระบบ Legacy กับระบบและเทคโนโลยีใหม่ๆ ง่ายขึ้น ช่วยให้เกิดนวัตกรรมและความทันสมัย ตัวอย่างเช่น แพลตฟอร์มอีคอมเมิร์ซในยุโรปอาจต้องเชื่อมต่อกับเกตเวย์การชำระเงินใหม่ที่ใช้ API ที่แตกต่างกัน
- ปรับปรุงขวัญและกำลังใจของนักพัฒนา: การทำงานกับโค้ดที่สะอาดและมีโครงสร้างที่ดีนั้นสนุกและมีประสิทธิผลมากขึ้นสำหรับนักพัฒนา การทำ Refactoring สามารถเพิ่มขวัญและกำลังใจและดึงดูดผู้มีความสามารถได้
การระบุส่วนของโค้ดที่ควรทำ Refactoring
ไม่ใช่โค้ด Legacy ทั้งหมดที่ต้องทำ Refactoring สิ่งสำคัญคือต้องจัดลำดับความสำคัญของความพยายามในการทำ Refactoring โดยพิจารณาจากปัจจัยต่อไปนี้:
- ความถี่ในการเปลี่ยนแปลง: โค้ดที่มีการแก้ไขบ่อยครั้งเป็นตัวเลือกหลักสำหรับการทำ Refactoring เนื่องจากการปรับปรุงความสามารถในการบำรุงรักษาจะส่งผลกระทบอย่างมีนัยสำคัญต่อผลิตภาพในการพัฒนา
- ความซับซ้อน: โค้ดที่ซับซ้อนและเข้าใจยากมีแนวโน้มที่จะมีข้อบกพร่องและแก้ไขได้อย่างปลอดภัยได้ยากกว่า
- ผลกระทบของข้อบกพร่อง: โค้ดที่มีความสำคัญต่อการดำเนินธุรกิจหรือมีความเสี่ยงสูงที่จะทำให้เกิดข้อผิดพลาดที่มีค่าใช้จ่ายสูงควรได้รับการจัดลำดับความสำคัญสำหรับการทำ Refactoring
- ปัญหาคอขวดด้านประสิทธิภาพ: โค้ดที่ถูกระบุว่าเป็นปัญหาคอขวดด้านประสิทธิภาพควรได้รับการทำ Refactoring เพื่อปรับปรุงประสิทธิภาพ
- Code Smells: คอยสังเกต code smells ทั่วไป เช่น เมธอดที่ยาว คลาสขนาดใหญ่ โค้ดที่ซ้ำซ้อน และ feature envy สิ่งเหล่านี้เป็นตัวบ่งชี้ของพื้นที่ที่สามารถได้รับประโยชน์จากการทำ Refactoring
ตัวอย่าง: ลองนึกภาพบริษัทโลจิสติกส์ระดับโลกที่มีระบบ Legacy สำหรับการจัดการการจัดส่ง โมดูลที่รับผิดชอบในการคำนวณค่าขนส่งมีการอัปเดตบ่อยครั้งเนื่องจากกฎระเบียบและราคาน้ำมันที่เปลี่ยนแปลงไป โมดูลนี้จึงเป็นตัวเลือกที่สำคัญสำหรับการทำ Refactoring
เทคนิคการทำ Refactoring
มีเทคนิคการทำ Refactoring มากมาย โดยแต่ละเทคนิคถูกออกแบบมาเพื่อจัดการกับ code smells ที่เฉพาะเจาะจงหรือปรับปรุงลักษณะเฉพาะของโค้ด นี่คือเทคนิคที่ใช้กันทั่วไปบางส่วน:
การจัดองค์ประกอบของเมธอด (Composing Methods)
เทคนิคเหล่านี้มุ่งเน้นไปที่การแบ่งเมธอดขนาดใหญ่และซับซ้อนออกเป็นเมธอดที่เล็กกว่าและจัดการได้ง่ายขึ้น ซึ่งช่วยปรับปรุงความสามารถในการอ่าน ลดความซ้ำซ้อน และทำให้โค้ดทดสอบได้ง่ายขึ้น
- Extract Method: เกี่ยวข้องกับการระบุกลุ่มของโค้ดที่ทำงานเฉพาะอย่างและย้ายไปยังเมธอดใหม่
- Inline Method: เกี่ยวข้องกับการแทนที่การเรียกเมธอดด้วยเนื้อหาของเมธอดนั้น ใช้เมื่อชื่อของเมธอดมีความชัดเจนพอๆ กับเนื้อหาของมัน หรือเมื่อคุณกำลังจะใช้ Extract Method แต่เมธอดที่มีอยู่สั้นเกินไป
- Replace Temp with Query: เกี่ยวข้องกับการแทนที่ตัวแปรชั่วคราวด้วยการเรียกเมธอดที่คำนวณค่าของตัวแปรตามความต้องการ
- Introduce Explaining Variable: ใช้เพื่อกำหนดผลลัพธ์ของนิพจน์ให้กับตัวแปรที่มีชื่อที่สื่อความหมาย เพื่อชี้แจงวัตถุประสงค์ของมัน
การย้ายคุณสมบัติระหว่างอ็อบเจกต์ (Moving Features Between Objects)
เทคนิคเหล่านี้มุ่งเน้นไปที่การปรับปรุงการออกแบบของคลาสและอ็อบเจกต์โดยการย้ายความรับผิดชอบไปยังที่ที่ควรจะเป็น
- Move Method: เกี่ยวข้องกับการย้ายเมธอดจากคลาสหนึ่งไปยังอีกคลาสหนึ่งที่มันควรจะอยู่ตามหลักเหตุผล
- Move Field: เกี่ยวข้องกับการย้ายฟิลด์จากคลาสหนึ่งไปยังอีกคลาสหนึ่งที่มันควรจะอยู่ตามหลักเหตุผล
- Extract Class: เกี่ยวข้องกับการสร้างคลาสใหม่จากชุดความรับผิดชอบที่เกี่ยวเนื่องกันซึ่งถูกดึงออกมาจากคลาสที่มีอยู่
- Inline Class: ใช้เพื่อรวมคลาสหนึ่งเข้ากับอีกคลาสหนึ่งเมื่อมันไม่ได้ทำงานมากพอที่จะ justifies การมีอยู่ของมัน
- Hide Delegate: เกี่ยวข้องกับการสร้างเมธอดในเซิร์ฟเวอร์เพื่อซ่อนตรรกะการมอบหมาย (delegation) จากไคลเอ็นต์ ลดการผูกมัดระหว่างไคลเอ็นต์และเดลิเกต
- Remove Middle Man: หากคลาสหนึ่งกำลังมอบหมายงานเกือบทั้งหมดของตน เทคนิคนี้จะช่วยตัดคนกลางออกไป
- Introduce Foreign Method: เพิ่มเมธอดให้กับคลาสไคลเอ็นต์เพื่อให้บริการไคลเอ็นต์ด้วยคุณสมบัติที่จำเป็นจริงๆ จากคลาสเซิร์ฟเวอร์ แต่ไม่สามารถแก้ไขได้เนื่องจากไม่มีสิทธิ์เข้าถึงหรือมีการวางแผนเปลี่ยนแปลงในคลาสเซิร์ฟเวอร์
- Introduce Local Extension: สร้างคลาสใหม่ที่มีเมธอดใหม่ๆ มีประโยชน์เมื่อคุณไม่สามารถควบคุมซอร์สของคลาสและไม่สามารถเพิ่มพฤติกรรมเข้าไปโดยตรงได้
การจัดระเบียบข้อมูล (Organizing Data)
เทคนิคเหล่านี้มุ่งเน้นไปที่การปรับปรุงวิธีการจัดเก็บและเข้าถึงข้อมูล ทำให้เข้าใจและแก้ไขได้ง่ายขึ้น
- Replace Data Value with Object: เกี่ยวข้องกับการแทนที่ค่าข้อมูลธรรมดาด้วยอ็อบเจกต์ที่ห่อหุ้มข้อมูลและพฤติกรรมที่เกี่ยวข้อง
- Change Value to Reference: เกี่ยวข้องกับการเปลี่ยนอ็อบเจกต์ค่า (value object) เป็นอ็อบเจกต์อ้างอิง (reference object) เมื่อมีอ็อบเจกต์หลายตัวใช้ค่าเดียวกัน
- Change Unidirectional Association to Bidirectional: สร้างการเชื่อมโยงสองทิศทางระหว่างสองคลาสในขณะที่มีเพียงการเชื่อมโยงทางเดียว
- Change Bidirectional Association to Unidirectional: ทำให้ความสัมพันธ์ง่ายขึ้นโดยการทำให้ความสัมพันธ์สองทางกลายเป็นทางเดียว
- Replace Magic Number with Symbolic Constant: เกี่ยวข้องกับการแทนที่ค่าที่เขียนตายตัว (literal values) ด้วยค่าคงที่ที่มีชื่อ ทำให้โค้ดเข้าใจและบำรุงรักษาง่ายขึ้น
- Encapsulate Field: จัดเตรียมเมธอด getter และ setter สำหรับการเข้าถึงฟิลด์
- Encapsulate Collection: ทำให้แน่ใจว่าการเปลี่ยนแปลงทั้งหมดในคอลเลกชันเกิดขึ้นผ่านเมธอดที่ควบคุมอย่างระมัดระวังในคลาสที่เป็นเจ้าของ
- Replace Record with Data Class: สร้างคลาสใหม่ที่มีฟิลด์ตรงกับโครงสร้างของเรคคอร์ดและเมธอดเข้าถึง
- Replace Type Code with Class: สร้างคลาสใหม่เมื่อรหัสประเภท (type code) มีชุดค่าที่เป็นไปได้ที่จำกัดและเป็นที่รู้จัก
- Replace Type Code with Subclasses: สำหรับเมื่อค่ารหัสประเภทส่งผลต่อพฤติกรรมของคลาส
- Replace Type Code with State/Strategy: สำหรับเมื่อค่ารหัสประเภทส่งผลต่อพฤติกรรมของคลาส แต่การใช้คลาสย่อย (subclassing) ไม่เหมาะสม
- Replace Subclass with Fields: ลบคลาสย่อยออกและเพิ่มฟิลด์ลงในคลาสแม่ (superclass) ซึ่งแสดงถึงคุณสมบัติที่แตกต่างของคลาสย่อย
การทำให้เงื่อนไขนิพจน์ง่ายขึ้น (Simplifying Conditional Expressions)
ตรรกะเงื่อนไขอาจซับซ้อนได้อย่างรวดเร็ว เทคนิคเหล่านี้มีจุดมุ่งหมายเพื่อทำให้ชัดเจนและง่ายขึ้น
- Decompose Conditional: เกี่ยวข้องกับการแยกคำสั่งเงื่อนไขที่ซับซ้อนออกเป็นส่วนเล็กๆ ที่จัดการได้ง่ายขึ้น
- Consolidate Conditional Expression: เกี่ยวข้องกับการรวมคำสั่งเงื่อนไขหลายๆ อันเป็นคำสั่งเดียวที่กระชับขึ้น
- Consolidate Duplicate Conditional Fragments: เกี่ยวข้องกับการย้ายโค้ดที่ซ้ำกันในหลายๆ สาขาของคำสั่งเงื่อนไขออกไปไว้นอกเงื่อนไข
- Remove Control Flag: กำจัดตัวแปรบูลีนที่ใช้ควบคุมโฟลว์ของตรรกะ
- Replace Nested Conditional with Guard Clauses: ทำให้โค้ดอ่านง่ายขึ้นโดยการวางกรณีพิเศษทั้งหมดไว้ด้านบนและหยุดการประมวลผลหากมีกรณีใดเป็นจริง
- Replace Conditional with Polymorphism: เกี่ยวข้องกับการแทนที่ตรรกะเงื่อนไขด้วย Polymorphism ทำให้อ็อบเจกต์ที่แตกต่างกันสามารถจัดการกับกรณีที่แตกต่างกันได้
- Introduce Null Object: แทนที่จะตรวจสอบค่า null ให้สร้างอ็อบเจกต์เริ่มต้นที่ให้พฤติกรรมเริ่มต้น
- Introduce Assertion: บันทึกความคาดหวังอย่างชัดเจนโดยการสร้างเทสที่ตรวจสอบสิ่งเหล่านั้น
การทำให้การเรียกเมธอดง่ายขึ้น (Simplifying Method Calls)
- Rename Method: แม้จะดูเหมือนชัดเจน แต่มีประโยชน์อย่างยิ่งในการทำให้โค้ดชัดเจน
- Add Parameter: การเพิ่มข้อมูลลงในลายเซ็นเมธอด (method signature) ทำให้เมธอดมีความยืดหยุ่นและนำกลับมาใช้ใหม่ได้มากขึ้น
- Remove Parameter: หากพารามิเตอร์ไม่ได้ถูกใช้ ให้กำจัดมันออกไปเพื่อทำให้อินเทอร์เฟซง่ายขึ้น
- Separate Query from Modifier: หากเมธอดหนึ่งทั้งเปลี่ยนแปลงและส่งคืนค่า ให้แยกออกเป็นสองเมธอดที่แตกต่างกัน
- Parameterize Method: ใช้เพื่อรวมเมธอดที่คล้ายกันให้เป็นเมธอดเดียวที่มีพารามิเตอร์ที่เปลี่ยนแปลงพฤติกรรม
- Replace Parameter with Explicit Methods: ทำตรงกันข้ามกับการใช้พารามิเตอร์ - แยกเมธอดเดียวออกเป็นหลายเมธอดที่แต่ละเมธอดแสดงถึงค่าเฉพาะของพารามิเตอร์
- Preserve Whole Object: แทนที่จะส่งข้อมูลเฉพาะบางรายการไปยังเมธอด ให้ส่งอ็อบเจกต์ทั้งหมดเพื่อให้เมธอดสามารถเข้าถึงข้อมูลทั้งหมดได้
- Replace Parameter with Method: หากเมธอดถูกเรียกด้วยค่าเดียวกันที่ได้มาจากฟิลด์เสมอ ให้พิจารณาสร้างค่าพารามิเตอร์ภายในเมธอด
- Introduce Parameter Object: จัดกลุ่มพารามิเตอร์หลายตัวเข้าด้วยกันเป็นอ็อบเจกต์เมื่อมันอยู่ด้วยกันโดยธรรมชาติ
- Remove Setting Method: หลีกเลี่ยง setters หากฟิลด์ควรจะถูกกำหนดค่าเริ่มต้นเท่านั้น แต่ไม่ควรแก้ไขหลังจากการสร้าง
- Hide Method: ลดระดับการมองเห็นของเมธอดหากมันถูกใช้ภายในคลาสเดียวเท่านั้น
- Replace Constructor with Factory Method: ทางเลือกที่สื่อความหมายได้ดีกว่า constructor
- Replace Exception with Test: หากมีการใช้ exception เป็นการควบคุมโฟลว์ ให้แทนที่ด้วยตรรกะเงื่อนไขเพื่อปรับปรุงประสิทธิภาพ
การจัดการกับการสร้างลักษณะทั่วไป (Dealing with Generalization)
- Pull Up Field: ย้ายฟิลด์จากคลาสย่อยไปยังคลาสแม่
- Pull Up Method: ย้ายเมธอดจากคลาสย่อยไปยังคลาสแม่
- Pull Up Constructor Body: ย้ายเนื้อหาของ constructor จากคลาสย่อยไปยังคลาสแม่
- Push Down Method: ย้ายเมธอดจากคลาสแม่ไปยังคลาสย่อย
- Push Down Field: ย้ายฟิลด์จากคลาสแม่ไปยังคลาสย่อย
- Extract Interface: สร้างอินเทอร์เฟซจากเมธอดสาธารณะของคลาส
- Extract Superclass: ย้ายฟังก์ชันการทำงานร่วมกันจากสองคลาสไปยังคลาสแม่ใหม่
- Collapse Hierarchy: รวมคลาสแม่และคลาสย่อยเป็นคลาสเดียว
- Form Template Method: สร้าง template method ในคลาสแม่ที่กำหนดขั้นตอนของอัลกอริทึม ทำให้คลาสย่อยสามารถ override ขั้นตอนเฉพาะได้
- Replace Inheritance with Delegation: สร้างฟิลด์ในคลาสที่อ้างอิงถึงฟังก์ชันการทำงาน แทนที่จะสืบทอดมา
- Replace Delegation with Inheritance: เมื่อการมอบหมายซับซ้อนเกินไป ให้เปลี่ยนไปใช้การสืบทอด
นี่เป็นเพียงตัวอย่างเล็กน้อยของเทคนิคการทำ Refactoring ที่มีอยู่มากมาย การเลือกใช้เทคนิคใดขึ้นอยู่กับ code smell ที่เฉพาะเจาะจงและผลลัพธ์ที่ต้องการ
ตัวอย่าง: เมธอดขนาดใหญ่ในแอปพลิเคชัน Java ที่ใช้โดยธนาคารระดับโลกใช้คำนวณอัตราดอกเบี้ย การใช้ Extract Method เพื่อสร้างเมธอดที่เล็กกว่าและมุ่งเน้นมากขึ้นจะช่วยปรับปรุงความสามารถในการอ่านและทำให้การอัปเดตตรรกะการคำนวณอัตราดอกเบี้ยง่ายขึ้นโดยไม่ส่งผลกระทบต่อส่วนอื่นๆ ของเมธอด
กระบวนการทำ Refactoring
การทำ Refactoring ควรทำอย่างเป็นระบบเพื่อลดความเสี่ยงและเพิ่มโอกาสในการประสบความสำเร็จสูงสุด นี่คือกระบวนการที่แนะนำ:
- ระบุส่วนของโค้ดที่ควรทำ Refactoring: ใช้เกณฑ์ที่กล่าวถึงก่อนหน้านี้เพื่อระบุพื้นที่ของโค้ดที่จะได้รับประโยชน์สูงสุดจากการทำ Refactoring
- สร้างเทส: ก่อนทำการเปลี่ยนแปลงใดๆ ให้เขียนเทสอัตโนมัติเพื่อตรวจสอบพฤติกรรมที่มีอยู่ของโค้ด นี่เป็นสิ่งสำคัญอย่างยิ่งเพื่อให้แน่ใจว่าการทำ Refactoring จะไม่ทำให้เกิดข้อผิดพลาดใหม่ สามารถใช้เครื่องมือเช่น JUnit (Java), pytest (Python), หรือ Jest (JavaScript) สำหรับการเขียน unit tests
- ทำ Refactoring แบบค่อยเป็นค่อยไป: ทำการเปลี่ยนแปลงเล็กๆ น้อยๆ ทีละขั้นตอน และรันเทสหลังจากการเปลี่ยนแปลงแต่ละครั้ง ซึ่งจะทำให้ง่ายต่อการระบุและแก้ไขข้อผิดพลาดที่เกิดขึ้น
- Commit บ่อยครั้ง: Commit การเปลี่ยนแปลงของคุณไปยังระบบควบคุมเวอร์ชัน (version control) บ่อยๆ ซึ่งจะช่วยให้คุณสามารถย้อนกลับไปยังเวอร์ชันก่อนหน้าได้อย่างง่ายดายหากมีข้อผิดพลาดเกิดขึ้น
- ตรวจสอบโค้ด (Code Review): ให้โค้ดของคุณได้รับการตรวจสอบโดยนักพัฒนาคนอื่น ซึ่งจะช่วยระบุปัญหาที่อาจเกิดขึ้นและทำให้แน่ใจว่าการทำ Refactoring นั้นทำได้อย่างถูกต้อง
- ตรวจสอบประสิทธิภาพ: หลังจากการทำ Refactoring ให้ตรวจสอบประสิทธิภาพของระบบเพื่อให้แน่ใจว่าการเปลี่ยนแปลงไม่ได้ทำให้ประสิทธิภาพลดลง
ตัวอย่าง: ทีมที่กำลังทำ Refactoring โมดูล Python ในแพลตฟอร์มอีคอมเมิร์ซระดับโลกใช้ `pytest` เพื่อสร้าง unit tests สำหรับฟังก์ชันการทำงานที่มีอยู่ จากนั้นพวกเขาใช้เทคนิค Extract Class เพื่อแยกความรับผิดชอบและปรับปรุงโครงสร้างของโมดูล หลังจากการเปลี่ยนแปลงเล็กๆ แต่ละครั้ง พวกเขาก็จะรันเทสเพื่อให้แน่ใจว่าฟังก์ชันการทำงานยังคงไม่เปลี่ยนแปลง
กลยุทธ์การเพิ่มเทสเข้าไปในโค้ด Legacy
ดังที่ Michael Feathers กล่าวไว้อย่างเหมาะสม โค้ด Legacy คือโค้ดที่ไม่มีเทส การเพิ่มเทสเข้าไปในโค้ดเบสที่มีอยู่อาจรู้สึกเหมือนเป็นงานใหญ่ แต่เป็นสิ่งจำเป็นสำหรับการทำ Refactoring อย่างปลอดภัย นี่คือกลยุทธ์หลายประการในการจัดการกับงานนี้:
Characterization Tests (หรือ Golden Master Tests)
เมื่อคุณต้องจัดการกับโค้ดที่เข้าใจยาก Characterization tests สามารถช่วยคุณบันทึกพฤติกรรมที่มีอยู่ของมันก่อนที่คุณจะเริ่มทำการเปลี่ยนแปลง แนวคิดคือการเขียนเทสที่ยืนยันผลลัพธ์ปัจจุบันของโค้ดสำหรับชุดอินพุตที่กำหนด เทสเหล่านี้ไม่จำเป็นต้องตรวจสอบความถูกต้องเสมอไป แต่เพียงแค่บันทึกว่าโค้ด *ปัจจุบัน* ทำอะไร
ขั้นตอน:
- ระบุหน่วยของโค้ดที่คุณต้องการจะระบุลักษณะ (characterize) (เช่น ฟังก์ชันหรือเมธอด)
- สร้างชุดของค่าอินพุตที่แสดงถึงช่วงของสถานการณ์ทั่วไปและกรณีพิเศษ (edge-case)
- รันโค้ดด้วยอินพุตเหล่านั้นและบันทึกผลลัพธ์ที่ได้
- เขียนเทสที่ยืนยันว่าโค้ดสร้างผลลัพธ์เหล่านั้นสำหรับอินพุตเหล่านั้น
ข้อควรระวัง: Characterization tests อาจเปราะบางหากตรรกะพื้นฐานมีความซับซ้อนหรือขึ้นอยู่กับข้อมูล เตรียมพร้อมที่จะอัปเดตหากคุณต้องการเปลี่ยนพฤติกรรมของโค้ดในภายหลัง
Sprout Method และ Sprout Class
เทคนิคเหล่านี้ซึ่งอธิบายโดย Michael Feathers เช่นกัน มีเป้าหมายเพื่อเพิ่มฟังก์ชันการทำงานใหม่เข้าไปในระบบ Legacy พร้อมกับลดความเสี่ยงในการทำลายโค้ดที่มีอยู่
Sprout Method: เมื่อคุณต้องการเพิ่มฟีเจอร์ใหม่ที่ต้องแก้ไขเมธอดที่มีอยู่ ให้สร้างเมธอดใหม่ที่มีตรรกะใหม่ จากนั้นเรียกเมธอดใหม่นี้จากเมธอดที่มีอยู่ ซึ่งจะช่วยให้คุณสามารถแยกโค้ดใหม่ออกมาและทดสอบได้อย่างอิสระ
Sprout Class: คล้ายกับ Sprout Method แต่สำหรับคลาส ให้สร้างคลาสใหม่ที่ implement ฟังก์ชันการทำงานใหม่ จากนั้นจึงรวมเข้ากับระบบที่มีอยู่
Sandboxing
Sandboxing คือการแยกโค้ด Legacy ออกจากส่วนที่เหลือของระบบ ทำให้คุณสามารถทดสอบในสภาพแวดล้อมที่มีการควบคุมได้ ซึ่งสามารถทำได้โดยการสร้าง mocks หรือ stubs สำหรับ dependencies หรือโดยการรันโค้ดใน virtual machine
The Mikado Method
The Mikado Method เป็นแนวทางการแก้ปัญหาด้วยภาพสำหรับการจัดการกับงาน Refactoring ที่ซับซ้อน ซึ่งเกี่ยวข้องกับการสร้างแผนภาพที่แสดงความสัมพันธ์ระหว่างส่วนต่างๆ ของโค้ด จากนั้นทำการ Refactoring โค้ดในลักษณะที่ลดผลกระทบต่อส่วนอื่นๆ ของระบบให้น้อยที่สุด หลักการสำคัญคือ "ลอง" ทำการเปลี่ยนแปลงและดูว่าอะไรเสียหาย หากเสียหาย ให้ย้อนกลับไปยังสถานะที่ใช้งานได้ล่าสุดและบันทึกปัญหา จากนั้นแก้ไขปัญหานั้นก่อนที่จะพยายามทำการเปลี่ยนแปลงเดิมอีกครั้ง
เครื่องมือสำหรับการทำ Refactoring
เครื่องมือหลายอย่างสามารถช่วยในการทำ Refactoring โดยทำงานซ้ำๆ โดยอัตโนมัติและให้คำแนะนำเกี่ยวกับแนวทางปฏิบัติที่ดีที่สุด เครื่องมือเหล่านี้มักจะถูกรวมเข้ากับสภาพแวดล้อมการพัฒนาแบบเบ็ดเสร็จ (Integrated Development Environments - IDEs):
- IDEs (เช่น IntelliJ IDEA, Eclipse, Visual Studio): IDEs มีเครื่องมือ Refactoring ในตัวที่สามารถทำงานต่างๆ ได้โดยอัตโนมัติ เช่น การเปลี่ยนชื่อตัวแปร การแยกเมธอด และการย้ายคลาส
- เครื่องมือวิเคราะห์โค้ดแบบสถิต (Static Analysis Tools) (เช่น SonarQube, Checkstyle, PMD): เครื่องมือเหล่านี้จะวิเคราะห์โค้ดเพื่อหา code smells ข้อบกพร่องที่อาจเกิดขึ้น และช่องโหว่ด้านความปลอดภัย สามารถช่วยระบุส่วนของโค้ดที่จะได้รับประโยชน์จากการทำ Refactoring
- เครื่องมือวัดความครอบคลุมของโค้ด (Code Coverage Tools) (เช่น JaCoCo, Cobertura): เครื่องมือเหล่านี้จะวัดเปอร์เซ็นต์ของโค้ดที่ครอบคลุมโดยเทส สามารถช่วยระบุส่วนของโค้ดที่ไม่ได้รับการทดสอบอย่างเพียงพอ
- Refactoring Browsers (เช่น Smalltalk Refactoring Browser): เครื่องมือพิเศษที่ช่วยในกิจกรรมการปรับโครงสร้างขนาดใหญ่
ตัวอย่าง: ทีมพัฒนาที่ทำงานกับแอปพลิเคชัน C# สำหรับบริษัทประกันภัยระดับโลกใช้เครื่องมือ Refactoring ในตัวของ Visual Studio เพื่อเปลี่ยนชื่อตัวแปรและแยกเมธอดโดยอัตโนมัติ พวกเขายังใช้ SonarQube เพื่อระบุ code smells และช่องโหว่ที่อาจเกิดขึ้น
ความท้าทายและความเสี่ยง
การทำ Refactoring โค้ด Legacy ไม่ได้ปราศจากความท้าทายและความเสี่ยง:
- การทำให้เกิดข้อผิดพลาดใหม่ (Regressions): ความเสี่ยงที่ใหญ่ที่สุดคือการทำให้เกิดข้อบกพร่องระหว่างกระบวนการ Refactoring ซึ่งสามารถลดความเสี่ยงนี้ได้โดยการเขียนเทสที่ครอบคลุมและการทำ Refactoring แบบค่อยเป็นค่อยไป
- ขาดความรู้เฉพาะทาง (Domain Knowledge): หากนักพัฒนาเดิมย้ายออกไปแล้ว อาจเป็นเรื่องยากที่จะเข้าใจโค้ดและวัตถุประสงค์ของมัน ซึ่งอาจนำไปสู่การตัดสินใจทำ Refactoring ที่ไม่ถูกต้อง
- การผูกมัดที่แน่นหนา (Tight Coupling): โค้ดที่มีการผูกมัดอย่างแน่นหนาจะทำ Refactoring ได้ยากกว่า เนื่องจากการเปลี่ยนแปลงในส่วนหนึ่งของโค้ดอาจส่งผลกระทบที่ไม่คาดคิดต่อส่วนอื่นๆ ของโค้ด
- ข้อจำกัดด้านเวลา: การทำ Refactoring อาจใช้เวลา และอาจเป็นเรื่องยากที่จะให้เหตุผลในการลงทุนกับผู้มีส่วนได้ส่วนเสียที่มุ่งเน้นการส่งมอบฟีเจอร์ใหม่ๆ
- การต่อต้านการเปลี่ยนแปลง: นักพัฒนาบางคนอาจต่อต้านการทำ Refactoring โดยเฉพาะอย่างยิ่งหากพวกเขาไม่คุ้นเคยกับเทคนิคที่เกี่ยวข้อง
แนวทางปฏิบัติที่ดีที่สุด (Best Practices)
เพื่อลดความท้าทายและความเสี่ยงที่เกี่ยวข้องกับการทำ Refactoring โค้ด Legacy ให้ปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดเหล่านี้:
- ได้รับการสนับสนุน (Get Buy-In): ตรวจสอบให้แน่ใจว่าผู้มีส่วนได้ส่วนเสียเข้าใจถึงประโยชน์ของการทำ Refactoring และยินดีที่จะลงทุนเวลาและทรัพยากรที่จำเป็น
- เริ่มจากสิ่งเล็กๆ: เริ่มต้นด้วยการทำ Refactoring ชิ้นส่วนโค้ดเล็กๆ ที่แยกจากกัน ซึ่งจะช่วยสร้างความมั่นใจและแสดงให้เห็นถึงคุณค่าของการทำ Refactoring
- ทำ Refactoring แบบค่อยเป็นค่อยไป: ทำการเปลี่ยนแปลงเล็กๆ ทีละขั้นตอนและทดสอบบ่อยๆ ซึ่งจะทำให้ง่ายต่อการระบุและแก้ไขข้อผิดพลาดที่เกิดขึ้น
- ทำให้เทสเป็นอัตโนมัติ: เขียนเทสอัตโนมัติที่ครอบคลุมเพื่อตรวจสอบพฤติกรรมของโค้ดก่อนและหลังการทำ Refactoring
- ใช้เครื่องมือ Refactoring: ใช้ประโยชน์จากเครื่องมือ Refactoring ที่มีอยู่ใน IDE หรือเครื่องมืออื่นๆ ของคุณเพื่อทำงานซ้ำๆ โดยอัตโนมัติและให้คำแนะนำเกี่ยวกับแนวทางปฏิบัติที่ดีที่สุด
- บันทึกการเปลี่ยนแปลงของคุณ: บันทึกการเปลี่ยนแปลงที่คุณทำระหว่างการทำ Refactoring ซึ่งจะช่วยให้นักพัฒนาคนอื่นเข้าใจโค้ดและหลีกเลี่ยงการทำให้เกิดข้อผิดพลาดใหม่ในอนาคต
- การทำ Refactoring อย่างต่อเนื่อง: ทำให้การทำ Refactoring เป็นส่วนหนึ่งของกระบวนการพัฒนาอย่างต่อเนื่อง แทนที่จะเป็นกิจกรรมที่ทำครั้งเดียว ซึ่งจะช่วยให้โค้ดเบสสะอาดและบำรุงรักษาได้ง่าย
บทสรุป
การทำ Refactoring โค้ด Legacy เป็นความพยายามที่ท้าทายแต่คุ้มค่า ด้วยการปฏิบัติตามกลยุทธ์และแนวทางปฏิบัติที่ดีที่สุดที่ระบุไว้ในคู่มือนี้ คุณสามารถปราบพยศโค้ดเก่าและเปลี่ยนระบบ Legacy ของคุณให้กลายเป็นสินทรัพย์ที่บำรุงรักษาได้ น่าเชื่อถือ และมีประสิทธิภาพสูง อย่าลืมเข้าถึงการทำ Refactoring อย่างเป็นระบบ ทดสอบบ่อยครั้ง และสื่อสารกับทีมของคุณอย่างมีประสิทธิภาพ ด้วยการวางแผนและการดำเนินการอย่างรอบคอบ คุณสามารถปลดล็อกศักยภาพที่ซ่อนอยู่ภายในโค้ด Legacy ของคุณและปูทางสำหรับนวัตกรรมในอนาคตได้