Topic 40 Refactoring (การรีแฟคเตอร์)

การเปลี่ยนแปลงและความเสื่อมสลายที่ฉันเห็นอยู่รอบตัว...

H. F. Lyte , Abide With Me

เมื่อโปรแกรมพัฒนาไปเรื่อย ๆ ก็จำเป็นต้องกลับมาทบทวนการตัดสินใจในอดีตและปรับปรุงบางส่วนของโค้ดใหม่ กระบวนการนี้เป็นเรื่องธรรมชาติอย่างยิ่ง โค้ดจำเป็นต้อง evolve มันไม่ใช่สิ่งที่หยุดนิ่งครับ

แต่น่าเสียดายที่ metaphor ที่คนใช้กันบ่อยที่สุดสำหรับการพัฒนา software คือการก่อสร้างอาคาร ผลงานคลาสสิกของ Bertrand Meyer เรื่อง Object-Oriented Software Construction  [Mey97] ใช้คำว่า “Software Construction” และแม้แต่ผู้เขียนหนังสือเล่มนี้เองก็เคยเป็นบรรณาธิการคอลัมน์ Software Construction ให้กับ IEEE Software ในช่วงต้นยุค 2000s [55]

แต่การใช้การก่อสร้างเป็น metaphor หลักนั้นสื่อถึงขั้นตอนต่อไปนี้:

  1. สถาปนิกร่างพิมพ์เขียว

  2. ผู้รับเหมาขุดฐานราก สร้างโครงสร้างหลัก เดินสายไฟและท่อประปา แล้วเก็บรายละเอียดขั้นสุดท้าย

  3. ผู้เช่าย้ายเข้ามาอยู่และใช้ชีวิตอย่างมีความสุขตลอดไป โดยโทรเรียกฝ่ายซ่อมบำรุงอาคารเมื่อมีปัญหา

แต่ software ไม่ได้ทำงานแบบนั้นเสียทีเดียว แทนที่จะเป็นการก่อสร้าง software กลับเหมือนกับการ ทำสวน มากกว่า—มันเป็นธรรมชาติ (organic) มากกว่าที่จะเป็นคอนกรีตแข็ง ๆ คุณปลูกสิ่งต่าง ๆ มากมายในสวนตามแผนและเงื่อนไขเริ่มต้น บางต้นก็เติบโตงอกงาม บางต้นก็มีจุดจบเป็นปุ๋ยหมัก คุณอาจย้ายต้นไม้ให้สัมพันธ์กันเพื่อใช้ประโยชน์จากแสงและเงา ลมและฝน ต้นไม้ที่โตเกินไปก็ถูกแยกหรือตัดแต่ง และสีที่ขัดกันก็อาจถูกย้ายไปยังตำแหน่งที่สวยงามกว่า คุณถอนวัชพืช และใส่ปุ๋ยให้กับต้นไม้ที่ต้องการความช่วยเหลือเพิ่มเติม คุณคอยเฝ้าดูสุขภาพของสวนอยู่ตลอดเวลา และปรับเปลี่ยน (ดิน ต้นไม้ ผังสวน) ตามความจำเป็นครับ

คนทำธุรกิจมักจะรู้สึกสบายใจกับ metaphor ของการก่อสร้างอาคารมากกว่า เพราะมันดูมีความเป็นวิทยาศาสตร์มากกว่าการทำสวน ทำซ้ำได้ มีลำดับชั้นการรายงานที่ชัดเจนสำหรับฝ่ายบริหาร และอื่น ๆ แต่เราไม่ได้กำลังสร้างตึกระฟ้า—เราไม่ได้ถูกจำกัดด้วยขอบเขตของฟิสิกส์และโลกความเป็นจริงมากขนาดนั้นครับ

metaphor การทำสวนนั้นใกล้เคียงกับความเป็นจริงของการพัฒนา software มากกว่ามากครับ บางที routine หนึ่ง ๆ อาจจะโตเกินไป หรือพยายามทำอะไรมากเกินไป—มันก็ต้องถูกแยกออกเป็นสองส่วน สิ่งที่ไม่เป็นไปตามแผนก็ต้องถูกถอนทิ้งหรือตัดแต่ง

การเขียนใหม่ การปรับปรุงใหม่ และการปรับ architecture ใหม่ของโค้ด รวมเรียกว่า restructuring แต่มีกิจกรรมย่อยส่วนหนึ่งที่กลายมาเป็นแนวปฏิบัติที่เรียกว่า refactoring .

Refactoring  [Fow19] ถูกนิยามโดย Martin Fowler ว่าเป็น:

เทคนิคที่มีวินัยสำหรับการปรับโครงสร้างโค้ดที่มีอยู่แล้ว โดยเปลี่ยนแปลงโครงสร้างภายในโดยไม่เปลี่ยนพฤติกรรมภายนอกของมัน

ส่วนที่สำคัญที่สุดของนิยามนี้คือ:

  1. กิจกรรมนี้มีวินัย ไม่ใช่การทำอะไรก็ได้ตามใจชอบ

  2. พฤติกรรมภายนอกไม่เปลี่ยนแปลง นี่ไม่ใช่เวลาที่จะเพิ่ม feature ใหม่

refactoring ไม่ได้ตั้งใจให้เป็นกิจกรรมพิเศษที่ต้องจัดพิธีใหญ่โตและทำนาน ๆ ครั้ง เหมือนไถทั้งสวนใหม่เพื่อปลูกใหม่หมด ในทางกลับกัน refactoring เป็นกิจกรรมประจำวัน ที่ใช้ก้าวเล็ก ๆ ความเสี่ยงต่ำ เหมือนการถอนวัชพืชและคราดดินมากกว่า แทนที่จะเป็นการเขียนโค้ดทั้งระบบใหม่หมดแบบไม่มีทิศทาง มันเป็นแนวทางที่มุ่งเป้าและแม่นยำ เพื่อช่วยให้โค้ดยังคงเปลี่ยนแปลงได้ง่ายครับ

เพื่อรับประกันว่าพฤติกรรมภายนอกไม่ได้เปลี่ยนไป คุณต้องมี automated unit testing ที่ดีคอยตรวจสอบพฤติกรรมของโค้ดครับ

When Should You Refactor? (ควร Refactor เมื่อไหร่)

คุณ refactor เมื่อคุณได้เรียนรู้อะไรบางอย่าง เมื่อคุณเข้าใจอะไรบางอย่างดีขึ้นกว่าที่เคยเข้าใจเมื่อปีที่แล้ว เมื่อวาน หรือแม้แต่เมื่อสิบนาทีที่แล้วครับ

บางทีคุณอาจเจออุปสรรคเพราะโค้ดมันเริ่มไม่เข้ากันแล้ว หรือสังเกตเห็นว่ามีสองสิ่งที่ควรจะรวมกัน หรืออะไรก็ตามที่ทำให้คุณรู้สึกว่ามัน “ผิด” อย่าลังเลที่จะเปลี่ยนมัน ไม่มีเวลาไหนดีไปกว่าตอนนี้อีกแล้ว มีหลายสิ่งที่อาจทำให้โค้ดสมควรถูก refactor:

Duplication (ความซ้ำซ้อน)

คุณค้นพบการละเมิดหลักการ DRY

Nonorthogonal Design (ดีไซน์ที่ไม่ Orthogonal)

คุณค้นพบบางอย่างที่สามารถทำให้ orthogonal มากขึ้นได้

Outdated Knowledge (ความรู้ที่ล้าสมัย)

สิ่งต่าง ๆ เปลี่ยนไป requirement เปลี่ยนไป และความรู้ของคุณเกี่ยวกับปัญหาก็เพิ่มขึ้น โค้ดก็ต้องตามให้ทันครับ

Usage (การใช้งานจริง)

เมื่อระบบถูกใช้งานโดยคนจริง ๆ ภายใต้สถานการณ์จริง คุณจะพบว่า feature บางอย่างสำคัญกว่าที่เคยคิดไว้ และ feature ที่เคยคิดว่า “must have” บางทีก็ไม่ได้จำเป็นขนาดนั้น

Performance (ประสิทธิภาพ)

คุณจำเป็นต้องย้าย functionality จากส่วนหนึ่งของระบบไปยังอีกส่วนหนึ่งเพื่อปรับปรุง performance

The Tests Pass (เทสต์ผ่านแล้ว)

ใช่ครับ พูดจริง ๆ เราบอกไปแล้วว่า refactoring ควรเป็นกิจกรรมสเกลเล็ก ที่มี test ที่ดีคอยรองรับ ดังนั้นเมื่อคุณเพิ่มโค้ดเล็ก ๆ เข้าไป และ test ที่เพิ่มเข้ามานั้นผ่าน คุณก็มีโอกาสดี ๆ ที่จะลงมือจัดระเบียบสิ่งที่คุณเพิ่งเขียนไปครับ

การ refactor โค้ดของคุณ—การย้าย functionality ไปมาและอัปเดตการตัดสินใจเก่า ๆ—จริง ๆ แล้วก็คือการฝึก จัดการความเจ็บปวด (pain management) ยอมรับกันตามตรงเถอะครับ การเปลี่ยนแปลง source code นั้นเจ็บปวดได้มากทีเดียว มันเคยทำงานได้ดีอยู่แล้ว บางทีปล่อยมันไว้เฉย ๆ อาจจะดีกว่า นักพัฒนาหลายคนลังเลที่จะกลับไปเปิดโค้ดส่วนหนึ่งใหม่ เพียงเพราะมันไม่ค่อยถูกต้องนัก

Real-World Complications (ความซับซ้อนในโลกจริง)

แล้วคุณก็ไปบอกเพื่อนร่วมทีมหรือลูกค้าว่า “โค้ดนี้ทำงานได้นะ แต่ผมต้องใช้เวลาอีกหนึ่งสัปดาห์เพื่อ refactor มันให้เสร็จสมบูรณ์”

คำตอบที่พวกเขาพูดกลับมานั้น เราพิมพ์ลงในหนังสือไม่ได้หรอกครับ

แรงกดดันด้านเวลามักถูกใช้เป็นข้ออ้างในการไม่ refactor แต่ข้ออ้างนี้ฟังไม่ขึ้นเลยครับ ถ้าไม่ refactor ตอนนี้ ในอนาคตคุณจะต้องลงทุนเวลามากกว่าเดิมมากเพื่อแก้ปัญหา—ตอนที่มี dependency ให้ต้องจัดการมากขึ้น แล้วตอนนั้นจะมีเวลาว่างมากกว่านี้ไหม ไม่มีทางครับ

คุณอาจลองอธิบายหลักการนี้ให้คนอื่นฟังด้วยการเปรียบเทียบทางการแพทย์ ลองคิดว่าโค้ดที่ต้อง refactor เป็นเหมือน “เนื้องอก” การเอามันออกต้องผ่าตัด คุณสามารถผ่าตัดเอาออกตั้งแต่ตอนนี้ ในขณะที่มันยังเล็กอยู่ หรือคุณจะรอให้มันโตขึ้นและลุกลาม—แต่การเอามันออกตอนนั้นจะทั้งแพงกว่าและอันตรายกว่า ยิ่งรอนานเข้าไปอีก คุณอาจสูญเสียคนไข้ไปเลยก็ได้ครับ

Tip 65 Refactor แต่เนิ่น ๆ และ Refactor บ่อย ๆ

ความเสียหายทางอ้อมในโค้ดก็อันตรายถึงตายได้พอ ๆ กันเมื่อเวลาผ่านไป (ดู Topic 3, ​ Software Entropy ​ ) refactoring ก็เหมือนกับเรื่องส่วนใหญ่ คือทำได้ง่ายกว่าตอนที่ปัญหายังเล็กอยู่ เป็นกิจกรรมต่อเนื่องระหว่างที่เขียนโค้ด คุณไม่ควรต้องใช้เวลา “หนึ่งสัปดาห์เพื่อ refactor” โค้ดชิ้นหนึ่ง—นั่นมันคือการเขียนใหม่ทั้งหมดแล้ว ถ้าจำเป็นต้องรบกวนงานถึงระดับนั้นจริง ๆ คุณอาจจะทำมันทันทีไม่ได้ แต่ให้แน่ใจว่ามันถูกใส่ไว้ใน schedule แทน และให้แน่ใจว่าผู้ใช้โค้ดส่วนที่ได้รับผลกระทบ รู้ ว่ามันถูกวางแผนจะเขียนใหม่ และเรื่องนี้อาจส่งผลกระทบกับพวกเขาอย่างไรครับ

How Do You Refactor? (จะ Refactor อย่างไร)

refactoring เริ่มต้นขึ้นในชุมชน Smalltalk และเพิ่งจะเริ่มเป็นที่รู้จักในวงกว้างตอนที่เราเขียนหนังสือเล่มนี้ฉบับพิมพ์ครั้งแรก ซึ่งอาจต้องขอบคุณหนังสือเล่มสำคัญเล่มแรกเกี่ยวกับ refactoring ( Refactoring: Improving the Design of Existing Code  [Fow19] ซึ่งตอนนี้พิมพ์ถึงฉบับที่สองแล้ว)

แก่นแท้ของ refactoring ก็คือการ redesign สิ่งใดก็ตามที่คุณหรือคนอื่นในทีมออกแบบไว้ สามารถถูกออกแบบใหม่ได้เมื่อมีข้อเท็จจริงใหม่ ความเข้าใจที่ลึกขึ้น requirement ที่เปลี่ยนไป และอื่น ๆ แต่ถ้าคุณลงมือฉีกโค้ดจำนวนมากทิ้งแบบไม่ยั้งคิด คุณอาจพบว่าตัวเองอยู่ในสถานะที่แย่กว่าตอนเริ่มต้นก็ได้ครับ

เห็นได้ชัดว่า refactoring เป็นกิจกรรมที่ต้องทำอย่างช้า ๆ ตั้งใจ และระมัดระวัง Martin Fowler เสนอเคล็ดลับง่าย ๆ ต่อไปนี้สำหรับการ refactor โดยไม่ทำร้ายมากกว่าช่วยครับ [56]

  1. อย่าพยายาม refactor และเพิ่ม functionality ใหม่ไปพร้อม ๆ กัน

  2. ตรวจสอบให้แน่ใจว่าคุณมี test ที่ดีก่อนเริ่ม refactor รัน test บ่อยที่สุดเท่าที่ทำได้ วิธีนี้จะทำให้คุณรู้ได้อย่างรวดเร็วว่าการเปลี่ยนแปลงของคุณทำให้อะไรพังหรือเปล่า

  3. ทำทีละก้าวสั้น ๆ อย่างตั้งใจ เช่น ย้าย field จาก class หนึ่งไปอีก class หนึ่ง แยก method ออก หรือเปลี่ยนชื่อ variable refactoring มักเกี่ยวข้องกับการเปลี่ยนแปลงเล็ก ๆ เฉพาะจุดจำนวนมาก ที่รวมกันแล้วกลายเป็นการเปลี่ยนแปลงขนาดใหญ่ ถ้าคุณทำทีละก้าวเล็ก ๆ และ test หลังจากแต่ละก้าว คุณจะหลีกเลี่ยงการ debug ที่ยืดเยื้อได้ครับ [57]

Automatic Refactoring (การ Refactor แบบอัตโนมัติ)

ย้อนกลับไปในฉบับพิมพ์ครั้งแรก เราเคยบอกไว้ว่า “เทคโนโลยีนี้ยังไม่ปรากฏนอกโลกของ Smalltalk แต่มีแนวโน้มว่าจะเปลี่ยนไป….” และมันก็เป็นจริงตามนั้นครับ เพราะตอนนี้การ refactor อัตโนมัติมีให้ใช้ใน IDE หลายตัว และรองรับภาษาหลัก ๆ ส่วนใหญ่แล้ว

IDE เหล่านี้สามารถเปลี่ยนชื่อ variable และ method แยก routine ยาว ๆ ออกเป็นส่วนเล็ก ๆ พร้อมกระจายการเปลี่ยนแปลงที่จำเป็นให้อัตโนมัติ ลากแล้ววางเพื่อช่วยคุณย้ายโค้ด และอื่น ๆ อีกมากมายครับ

เราจะพูดถึงเรื่อง testing ในระดับนี้เพิ่มเติมใน Topic 41, ​ Test to Code ​ และ testing ในระดับที่ใหญ่ขึ้นใน ​ Ruthless and Continuous Testing ​ แต่ประเด็นของคุณ Fowler เรื่องการรักษา regression test ที่ดีไว้นั้น คือกุญแจสำคัญของการ refactor อย่างปลอดภัยครับ

ถ้าคุณจำเป็นต้องทำมากกว่าแค่ refactor และจบลงด้วยการเปลี่ยนพฤติกรรมภายนอกหรือ interface การจงใจทำให้ build พังก็อาจช่วยได้ นั่นคือทำให้ client เก่าของโค้ดนี้ compile ไม่ผ่าน วิธีนี้จะทำให้คุณรู้ว่าต้องอัปเดตอะไรบ้าง ครั้งต่อไปที่คุณเห็นโค้ดที่ไม่ค่อยถูกต้องนัก ก็จัดการแก้มันซะ จัดการความเจ็บปวด ถ้ามันเจ็บตอนนี้ แต่จะเจ็บมากกว่านี้ในภายหลัง คุณก็ควรจัดการมันให้จบไปเลยดีกว่า อย่าลืมบทเรียนจาก Topic 3, ​ Software Entropy ​ : อย่าปล่อยให้อยู่กับหน้าต่างที่แตกครับ

Related Sections Include (หัวข้อที่เกี่ยวข้อง)

  • Topic 3, ​ Software Entropy ​
  • Topic 9, ​ DRY—The Evils of Duplication ​
  • Topic 12, ​ Tracer Bullets ​
  • Topic 27, ​ Don't Outrun Your Headlights ​
  • Topic 44, ​ Naming Things ​
  • Topic 48, ​ The Essence of Agility ​