Legacy code คืออะไร? เรากำลังสร้าง Legacy code อยู่หรือเปล่า และทำอย่างไรที่จะหยุดวงจรนี้
Developers เราคงเคยได้ยินคำว่า legacy code กันมาแล้วแทบทุกคน และในชีวิตการทำงานจริงๆของใครหลายคนนั้น ก็ทำงานอยู่กับแต่ legacy code อย่างเดียวมาโดยตลอด มันคือ code base ที่เราได้รับการส่งต่อมาจากใครสักคนที่ทำงานก่อนหน้าเรา และส่วนมากแล้วมักจะมาในรูปแบบที่ไม่มี document ใดๆเลยด้วยซ้ำ
โดยส่วนตัวแล้ว ผมเองก็ทำงานอยู่กับ legacy code มาแทบจะโดยตลอดเช่นกัน และมีสิ่งหนึ่งที่ผมต้องพยายามเข้าใจก่อนเลยคือ legacy code ไม่ใช่ code ที่ไม่ดีทั้งหมด แค่มันเก่าหรือมีอายุนานไปหน่อย มีความซับซ้อนเยอะกว่าที่มันควรจะเป็น และเวลาที่เราสงสัยมัน เรามักจะหาตัวคนที่เขียนมันขึ้นมาไม่ได้แล้ว
การทำงานกับ legacy code เป็นเรื่องที่ยาก เราพยายามหาทางที่จะเขียนมันใหม่และก็ประนามคนที่เขียนก่อนหน้าเรา แต่เรากำลังสร้าง legacy code เพิ่มกันอยู่หรือเปล่า เราต้องทำอย่างไรถึงจะกำจัด legacy code ออกไปจาก code base ได้ บทความนี้เราจะมาทำความรู้จักกับ legacy code และทำอย่างไรเราถึงจะหยุดวงจรนี้ได้
What is Legacy Code ?
อะไรคือ legacy code ? ถ้าตอบเอาแบบง่ายๆ โดยส่วนมากแล้ว เวลา developers เราคุยกัน เรามักจะบอกว่า legacy code คือ code ที่เราได้รับต่อมาจากใครสักคนก่อนหน้านั้นและเราต้องมารับช่วงต่อ และผู้คนก็ให้นิยามคำว่า legacy code ที่หลากหลายแตกต่างกันไป เช่น
- Legacy code คือ code ที่ไม่มีการ support อีกต่อไปแล้ว และ legacy code ยังรวมไปถึงการไม่ support ของ OS, hardware, หรือ formats ด้วย
- Legacy code คือ code ที่ถูกส่งต่อมาอีกทีและเราต้องบำรุงรักษามันต่อไป เพราะมันยังทำงานได้อยู่
- Legacy code คือ code ที่ใครก็ไม่รู้เขียนไว้ (โดยปกติแล้ว ใครสักคนที่เราไม่สามารถติดต่อเขาได้แล้ว) แต่เราต้องมาทำงานอยู่กับ code base นั้นๆ
- Code ก็เป็น Legacy code ทันทีแหละ หลังจากที่เราเขียนมันขึ้นมา
จากนิยามด้านบน ผู้คนมักนิยามคำว่า legacy code ในหลายๆรูปแบบ ซึ่งจริงๆแล้ว ก็ไม่มีนิยามไหนที่ถูกต้องที่สุดหรือผิดไปอย่างสิ้นเชิง แล้วอย่างนี้ จริงๆแล้วคำว่า legacy code คืออะไรกันแน่
อย่างไรก็ตาม จากหนังสือ Working Effectively with Legacy Code ของ Michael Feathers เขาได้ให้จำกัดความว่า
หรือแปลเป็นไทยง่ายๆว่า legacy code ก็คือ code ที่ไม่ได้มีการเขียน tests ทำการทดสอบ code นั้นๆ โดย code ที่ไม่มีการเขียน tests นั้น ไม่สำคัญว่าเราจะเขียน code นั้นสะสวยแค่ไหน clean ขนาดไหน หรือ structured ที่ดีอย่างไร ถ้ามี tests เรายังสามารถปรับเปลี่ยน behavior หรือปรับปรุง code ได้อย่างรวดเร็วและสามารถตรวจสอบได้ แต่ถ้าไม่มี tests แล้ว เราไม่มีทางรู้ได้เลยว่า code ของเรานั้น ถูกแก้ไขไปในทางที่ดีขึ้น หรือว่าแย่ลงกว่าเดิม
Working with Legacy Code ~ The Mechanics of Change
การทำงานกับ legacy code เป็นเรื่องยาก แต่ก็มีหลายวิธีทำให้เราทำงานกับ legacy code ให้ยุ่งยากเข้าไปยิ่งกว่าเดิมและก็มีอีกหลายวิธีที่ทำให้เราทำงานกับ legacy code ได้ง่ายขึ้นกว่าเดิมมาก ในการทำงานกับ code ที่มีความยุ่งเหยิงมากๆ เราต้องเข้าใจกลไกในการเปลี่ยนแปลงของ code ซะก่อน ซึ่ง Michael Feathers ได้เขียนในหนังสือของเขาไว้ โดยสาเหตุหลักๆในการเปลี่ยนแปลงของ software นั้นจะมีหลักๆอยู่ด้วยกัน 4 ข้อ ซึ่งประกอบไปด้วย
- การเพิ่ม feature ใหม่ๆ
- การ fixing bug
- การ improving design
- การทำ optimizing การใช้งาน resource
การเพิ่ม feature ใหม่ๆ
feature เป็นสิ่งที่ users ใช้งาน ฉะนั้น การเพิ่ม feature ใหม่ๆเป็นการตอบสนองความต้องการของ users
การ fixing bug
การ fixing bug นั้นมีความใกล้เคียงกับการเพิ่ม feature แต่จริงๆแล้วทั้ง 2 อย่างนี้มีความแตกต่างกัน ยกตัวอย่างเช่น มี website ของบริษัท ซึ่ง website นี้มี logo อยู่ด้านซ้ายบน แต่ business ต้องการย้าย logo จากด้านซ้ายบน ไปไว้ด้านขวาบน ถ้าในมุมมองของ business อาจจะมองว่า นี้คือการ fixing bug แต่ในมุมของ developers แล้ว เราจะมองว่านี้คือการเพิ่ม feature ใหม่
การ improving design
พอทำงานไปสักระยะ เมื่อ code มีความซับซ้อนมากยิ่งขึ้น เราจำเป็นมีการปรับเปลี่ยน design เพื่อให้ code ของเราสามารถ maintain ได้มากยิ่งขึ้น โดยยังคงรักษา behavior เดิมไว้ ซึ่งการ improving design ในรูปแบบนี้เอง ที่เราเรียกว่าการ refactoring code
การทำ optimizing การใช้งาน resource
การทำ optimizing มีความใกล้เคียงกับการ refactoring แต่การ optimization มักจะทำในเป้าหมายที่แตกต่างออกไป ทั้ง refactoring และ optimization นั้น มีความเหมือนกันคือ เราต้องรักษา behavior ของ software ไว้เหมือนเดิม แต่เราแก้ไขปรับเปลี่ยนอย่างอื่น อย่างอื่นใน refactoring คือการปรับเปลี่ยน structure ของ program แต่อย่างอื่นสำหรับ optimization นั้น คือการปรับการใช้งาน resource ของ program เช่น เวลาในการโหลดข้อมูล การใช้งาน memory หรือการใช้งาน internet data เป็นต้น
Working with Legacy Code ~ The Legacy Code Change Algorithm
หลังจากที่เราเข้าใจพื้นฐานของการเปลี่ยนแปลงของ Software แล้ว ก็มาถึงขั้นตอนในการแก้ไข legacy code แต่ว่าในการทำงานกับ legacy code นั้น มีอยู่หลายวิธี แล้วเราจะเลือกวิธีไหนหละ ถ้าเราต้องทำงานกับ legacy code ? หลายคนอาจจะเลือกวิธีทำงานที่ได้รับมอบหมายให้มันเสร็จๆไป ทำไมต้องคิดเรื่องปรับปรุง code ด้วย เรามีเวลาไม่พอที่จะ refactor code หรอก ทำงานให้มันเสร็จๆแล้วก็จบ story นั้นไป
ลุง Michael Feathers จะเรียกการทำงานในรูปแบบนี้ว่า “Edit and Pray” โดยเราทำการแก้ไขและ release มัน จากนั้นก็ทำการ “ภาวนา” ให้มันทำงานได้ดี โดยหวังว่ามันจะไม่มี bug ใดๆเกิดขึ้น แต่ก็มีอีกแนวทางหนึ่งที่ตรงกันข้ามกัน ซึ่งเรียกวิธีการแบบนี้ว่า “Cover and Modify” โดยแนวคิดการทำงานแบบ Cover and Modify นี้ คือการสร้างสิ่งที่เรียกว่า safety net เมื่อเราทำการแก้ไข software ซึ่ง safety net ในที่นี้คือการเขียน tests หรือชุดการทดสอบของ code ที่เราจะทำการแก้ไข จากนั้นเราถึงทำการแก้ไข code เป็นขั้นตอนต่อไป โดย safety net จะทำหน้าที่ในการให้ feedback กับเรา และเราก็จะทำงานกับ feedback เหล่านั้น ด้วยวิธีการแบบนี้จะทำให้เราสามารถแก้ไข code และรับรู้ได้ว่า code ที่เราแก้ไขนั้น ดำเนินไปในแนวทางที่ดีขึ้นหรือว่าแย่ลงกว่าเดิม
ใน software นั้น testing เป็นวิธีที่เราใช้ในการหา bug ในโปรแกรมของเรา หรือ testing เพื่อที่จะแสดงความถูกต้องของ program ดังนั้นการทำงานในชีวิตจริงของเราถึงต้องมี testing ทีม ที่คอยให้ feedback กับ development ทีม ซึ่งใน software industry นั้น คำว่า testing มีอยู่หลายรูปแบบ ไม่ว่าจะเป็น
- Unit Tests
- Integration Tests
- End-to-end tests
- Performance testing
- etc.
จาก testing หลากหลายรูปแบบข้างบนนั้น unit testing เป็นสิ่งที่สำคัญมากในการทำงานกับ legacy code ด้วย unit testing มีขนาดที่เล็ก และมีความเป็น localized tests ที่ให้ feedback กับ deveopers ได้เร็ว และสามารถให้เรา refactor ได้อย่างมีความปลอดภัย
เมื่อเราต้องแก้ไข legacy code base แล้ว Michael Feathers ได้แนะนำ algorithm ที่ใช้ในการแก้ไข legacy code base ดังนี้
Identify change points
ในการแก้ไข legacy code นั้น สิ่งแรกที่เราต้องทำคือการ ต้องระบุตำแหน่งที่เราจะทำการแก้ไข ซึ่งในหนังสือของ Michael Feathers เขาก็มี techniques ที่ใช้ในการทำ identify change points ก็คือ
— ทำความเข้าใจกับ code และทำการสร้าง notes และ sketchs code คร่าวๆโดยการใช้ syntax ในรูปแบบของตัวเขาเองในการทำ sketching และทำการ overview code ทั้งหมด
— จากนั้นก็ทำการ run debug หรือ print code listing และทำการ group หรือ separate หน้าที่ความรับผิดชอบของ code แต่ละส่วนเพื่อดูความแตกต่างของ code
— หลังจากนั้น ทำการ scratch refactoring ขั้นตอนนี้คือการทดลอง refactoring code ลองทำให้ code clean ขึ้น เข้าใจง่ายขึ้นและเรียนรู้ code นั้นๆ หลังจากนั้นก็ให้ทำการ undo refactoring ที่ทำไปก่อนหน้านั้นหลังจากที่เราได้เรียนรู้ code นั้นๆแล้ว
ซึ่ง techniques ในการเรียนรู้ code ของแต่ละคนนั้นมักไม่เหมือนกัน ในขั้นตอน identify change points นี้ จุดประสงค์ก็คือ ให้เราทำการเรียนรู้หรือทำความเข้าใจเกี่ยวกับ code ทดลองหาแนวทางในการ refactoring แบบคร่าวๆ
Find test points
ในขั้นตอน finding test points นั้น เราสามารถนำ The method use rule ของ Michael Feathers มาใช้ โดยก่อนที่เราจะใช้งาน method ใดๆก็ตามใน legacy system ให้ทำการตรวจสอบก่อนเลยว่า method นั้นๆมี tests อยู่รึป่าว ต้องการรู้ว่า tests ของ code ที่เรากำลังจะแก้ไขนั้นอยู่ที่ไหน เพราะ tests จะให้บอกเราได้ว่าผลกระทบของการเปลี่ยนแปลงจะมีอะไรบ้าง โดยปกติแล้วเราสามารถใช้ ความสามารถของ IDE เช่น Intellij หรือ Eclipse เพื่อทำการหาว่า methods นั้นๆถูก call มาจากที่ไหนบ้าง ด้วยความสามารถนี้จะทำให้เรารู้ว่า variables ที่ใช้มาจากที่ไหน และหา dependencies ระหว่าง classes รวมไปถึง methods และ properties ต่างๆด้วย
Break dependencies
ทำไมเราต้องทำการ break dependencies ด้วยหละ ? การ break dependencies จะทำให้เราสามารถสร้าง instance ของ object และทำให้เราสามารถ call method จาก test framework ได้ และยังทำให้เราสามารถ isolate ตัว class หรือ method จาก objects อื่นๆได้ด้วย ซึ่งการ break dependencies นั้น จะทำให้เราสามารถเขียน unit tests ได้ง่ายมากยิ่งขึ้นด้วย
Write tests
มาถึงขั้นตอนการเขียน tests แล้ว ว่าแต่ tests แบบไหนหละที่เราจะเขียน ? ใน test-driven development (TDD) นั้น เราทำการเขียน tests เพื่อทำการกำหนดลักษณะหรือ characterize functionality ของ feature ที่เรากำลังทำการสร้าง การเพิ่ม feature ใน legacy system ก็เหมือนกัน เราต้องทำการเขียน tests เพื่อทำการปกป้อง behavior เดิมที่มีอยู่แล้วบนระบบ เพื่อที่จะทำอย่างนั้น เราจึงต้องทำการเขียน characterize functionality ที่ระบบมีอยู่แล้ว อันนี้สำคัญมาก เวลาเราเขียน tests ในส่วนนี้ โดยเราจะไม่เขียน tests จากสิ่งที่ระบบควรจะเป็น แต่เราต้องเขียน characterize functionality ของสิ่งที่มันเป็นอยู่จริงๆหรือการทำงานจริงๆของระบบนั้นๆ
ขั้นตอนคร่าวๆในการเขียน Characterization tests
— เขียน test สำหรับพื้นที่ที่เราจะทำการปรับเปลี่ยน code โดยใส่ค่า expectation ไว้แบบคร่าวๆ
— จากนั้นทำการ run test นั้นๆ และแน่นอนว่า test case นั้นๆจะต้อง fail ซะก่อน
หลังจากที่ test fail ให้เราทำการ copy ค่า expected แล้วทำไปแทนที่ค่า expectation ที่เราใส่ไว้ในตอนแรก
— จากนั้นให้ทำการ run test จะเห็นว่า test pass
จากนั้นเขียน test case เพิ่มเรื่อยๆให้เพียงพอที่จะให้เราเข้าใจ behavior ของ code นั้นๆ การทำแบบนี้ เป็นการเขียน test เพื่อ characterize functionality ของ behavior ที่มันเป็นอยู่จริงๆในระบบ
Make changes and refactor
ในขั้นตอนนี้ Michael Feathers เขาแนะนำให้ใช้งาน test-driven development (TDD) ในการเพิ่มหรือในการแก้ไข code ซึ่งขั้นตอนที่สำคัญคือการเขียน test เพื่อทำการอธิบายและทดสอบการทำงานของ functionality ใหม่ที่เราเพิ่มเข้ามา ก่อนที่เราจะทำการเขียน code ตาม step ที่เราได้เขียน test ไว้แล้ว ซึ่งการทำงานแบบนี้ จะทำให้ code ของเราอยู่ภายใต้ test control หลังจากนั้นค่อยทำการ refactor code ตามที่เราต้องการหลังจากที่เรามี test covered เรียบร้อยแล้ว ซึ่งการทำแบบนี้ก่อนที่เราจะทำการเพิ่ม feature ใหม่ๆหรือ fixing bug จะสามารถนำไปสู่กระบวนการในการทำงานที่ง่ายกว่าในอนาคต
Conclusion
หลังจากที่เราได้รู้จัก legacy code และวิธีที่จะทำงานกับมัน เราได้เรียนรู้ว่า legacy code คือ code ที่ไม่ได้เขียน test หรือ code ที่มี test จำนวนน้อยมากหรือมีไม่เพียงพอ เราได้รู้จักขั้นแรกในการจัดการกับ legacy code คือการเขียน tests เพื่อช่วยให้เราปกป้อง functionality เดิมที่มีอยู่ในระบบ แต่อย่างไรก็ตาม การเขียน unit testing อาจจะไม่ได้ง่ายเสมอไป บางครั้ง legacy code มักมากับ code ที่มักจะผูกมัดกันมากเกินไป หรือเป็น code ที่ design ไม่ค่อยจะดีนัก ฉะนั้นในการแก้ไข เราจึงต้องมีสิ่งที่เรียกว่า The Legacy Code Change Algorithm เข้ามาเป็นขั้นตอนหรือกระบวนการในการแก้ไข legacy code
legacy code อาจไม่ได้เป็นเรื่องสำคัญในทางธุรกิจ การทำให้มันดีขึ้นก็เหมือนการช่วยให้เราทำงานได้ง่ายยิ่งขึ้น เราค่อยๆปรับปรุงแต่ละส่วนของมัน ทีละเล็กทีละน้อยเท่าที่เราสามารถทำได้ เราทำการ refactor legacy code เพื่อให้ code ที่เราดูแลอยู่ดียิ่งกว่าตอนที่เราได้รับมันมา เพื่อที่จะให้คนที่เข้ามาร่วมทีมในภายหลังหรือหลังจากที่เราไม่ได้อยู่ดูแลมันแล้ว คนอื่นๆที่มาดูแลมันต่อสามารถที่จะทำเข้าใจมันได้ไม่ยาก