การเปลี่ยนโค้ดนั้นยอดเยี่ยม มันคือสิ่งที่เราทำเพื่อหาเลี้ยงชีพ แต่การเปลี่ยนโค้ดบางรูปแบบทำให้ชีวิตยุ่งยาก ในขณะที่บางรูปแบบก็ทำให้ง่ายขึ้นมาก ในวงการนี้เรายังไม่ได้พูดถึงเรื่องนั้นมากนัก สิ่งที่ใกล้เคียงที่สุดคือวรรณกรรมเกี่ยวกับ refactoring ผมคิดว่าเราสามารถขยายขอบเขตการพูดคุยออกไปอีกหน่อย และพูดถึงวิธีจัดการกับโค้ดในสถานการณ์ที่ยุ่งยากที่สุด การจะทำแบบนั้นได้ เราต้องลงลึกไปในกลไกของการเปลี่ยนแปลง
Four Reasons to Change Software (สี่เหตุผลในการเปลี่ยนซอฟต์แวร์)
เพื่อความง่าย เรามาดูสี่เหตุผลหลักในการเปลี่ยนซอฟต์แวร์กัน
1. การเพิ่มฟีเจอร์
2. การแก้บั๊ก
3. การปรับปรุงการออกแบบ
4. การเพิ่มประสิทธิภาพการใช้ทรัพยากร
Adding Features and Fixing Bugs (การเพิ่มฟีเจอร์และการแก้บั๊ก)
การเพิ่มฟีเจอร์ดูเหมือนเป็นการเปลี่ยนแปลงที่ตรงไปตรงมาที่สุด ซอฟต์แวร์ทำงานในลักษณะหนึ่ง แล้วผู้ใช้บอกว่าระบบจำเป็นต้องทำอย่างอื่นเพิ่มเติมด้วย
สมมติว่าเรากำลังทำงานบนเว็บแอปพลิเคชัน และผู้จัดการบอกเราว่าเธอต้องการย้ายโลโก้บริษัทจากด้านซ้ายของหน้าไปไว้ด้านขวา เราคุยกับเธอและพบว่ามันไม่ง่ายอย่างนั้น เธอต้องการย้ายโลโก้ แต่ก็ต้องการการเปลี่ยนแปลงอื่นๆ ด้วย เธออยากทำให้โลโก้เป็นอนิเมชันในรุ่นถัดไป นี่คือการแก้บั๊กหรือการเพิ่มฟีเจอร์ใหม่? ขึ้นอยู่กับมุมมองของคุณ จากมุมมองของลูกค้า เธอกำลังขอให้เราแก้ปัญหาจริงๆ บางทีเธออาจเห็นเว็บไซต์และเข้าร่วมประชุมกับคนในแผนกของเธอ แล้วพวกเขาก็ตัดสินใจเปลี่ยนตำแหน่งโลโก้และขอฟังก์ชันเพิ่มเติมอีกเล็กน้อย จากมุมมองของนักพัฒนา การเปลี่ยนแปลงนี้มองว่าเป็นฟีเจอร์ใหม่ก็ได้ "ถ้าพวกเขาเลิกเปลี่ยนใจซะที เราก็คงเสร็จไปนานแล้ว" แต่ในบางองค์กร การย้ายโลโก้ถูกมองว่าเป็นแค่การแก้บั๊ก ไม่ว่าทีมจะต้องทำงานใหม่มากแค่ไหนก็ตาม
มันน่าดึงดูดที่จะบอกว่าทั้งหมดนี้เป็นแค่เรื่องของมุมมอง คุณมองว่ามันเป็นการแก้บั๊ก และผมมองว่ามันเป็นฟีเจอร์ แล้วก็จบกัน แต่ที่น่าเสียดายคือ ในหลายองค์กร การแก้บั๊กและการเพิ่มฟีเจอร์ต้องถูกติดตามและบันทึกแยกจากกันเพราะข้อกำหนดในสัญญาหรือโครงการริเริ่มด้านคุณภาพ ในระดับบุคคล เราสามารถโต้แย้งกันไปมาไม่รู้จบว่าเรากำลังเพิ่มฟีเจอร์หรือแก้บั๊ก แต่มันก็แค่การเปลี่ยนโค้ดและสิ่งอื่นๆ เท่านั้น น่าเสียดายที่การพูดถึงการแก้บั๊กและการเพิ่มฟีเจอร์นี้บดบังสิ่งที่สำคัญกว่ามากสำหรับเราในทางเทคนิค นั่นคือการเปลี่ยนแปลงพฤติกรรม (behavioral change) มีความแตกต่างอย่างมากระหว่างการเพิ่มพฤติกรรมใหม่และการเปลี่ยนพฤติกรรมเดิม
พฤติกรรม (Behavior) คือสิ่งที่สำคัญที่สุดของซอฟต์แวร์ มันคือสิ่งที่ผู้ใช้พึ่งพา ผู้ใช้ชอบเมื่อเราเพิ่มพฤติกรรม (ถ้ามันเป็นสิ่งที่พวกเขาต้องการจริงๆ) แต่ถ้าเราเปลี่ยนหรือลบพฤติกรรมที่พวกเขาพึ่งพาอยู่ (ทำให้เกิดบั๊ก) พวกเขาจะหยุดไว้วางใจเรา
ในตัวอย่างโลโก้บริษัท เรากำลังเพิ่มพฤติกรรมหรือไม่? ใช่ หลังการเปลี่ยนแปลง ระบบจะแสดงโลโก้ทางด้านขวาของหน้า เรากำลังกำจัดพฤติกรรมใดๆ หรือไม่? ใช่ จะไม่มีโลโก้ทางด้านซ้ายอีกต่อไป
มาดูกรณีที่ซับซ้อนกว่านี้กัน สมมติว่าลูกค้าต้องการเพิ่มโลโก้ทางด้านขวาของหน้า แต่เดิมไม่มีโลโก้ทางด้านซ้ายอยู่ก่อนแล้ว ใช่ เรากำลังเพิ่มพฤติกรรม แต่เรากำลังลบอะไรหรือไม่? มีอะไรถูกเรนเดอร์ในตำแหน่งที่กำลังจะวางโลโก้หรือเปล่า?
เรากำลังเปลี่ยนพฤติกรรม เพิ่มมัน หรือทั้งสองอย่าง?
ปรากฏว่าสำหรับเราแล้ว เราสามารถแยกแยะแบบที่เป็นประโยชน์ต่อเราในฐานะโปรแกรมเมอร์ได้ ถ้าเราต้องแก้ไขโค้ด (และ HTML ก็ถือว่าเป็นโค้ดแบบหนึ่ง) เราอาจกำลังเปลี่ยนพฤติกรรม แต่ถ้าเราแค่เพิ่มโค้ดและเรียกใช้มัน ส่วนใหญ่แล้วเรากำลังเพิ่มพฤติกรรม มาดูตัวอย่างอื่นกัน นี่คือ method ใน Java class:
public class CDPlayer
{
public void addTrackListing(Track track) {
...
}
...
}
คลาสนี้มี method ที่ให้เราเพิ่มรายการแทร็กได้ มาลองเพิ่มอีก method ที่ให้เราแทนที่รายการแทร็กกัน
public class CDPlayer
{
public void addTrackListing(Track track) {
...
}
public void replaceTrackListing(String name, Track track) {
...
}
...
}
เมื่อเราเพิ่ม method นั้น เราได้เพิ่มพฤติกรรมใหม่ให้กับแอปพลิเคชันหรือเปลี่ยนแปลงมัน? คำตอบคือ: ไม่ทั้งสองอย่าง การเพิ่ม method ไม่ได้เปลี่ยนพฤติกรรมนอกจาก method นั้นจะถูกเรียกขึ้นมา
มาลองเปลี่ยนโค้ดกันอีกครั้ง สมมติเราใส่ปุ่มใหม่บน user interface สำหรับ CD player ปุ่มนี้ให้ผู้ใช้แทนที่รายการแทร็กได้ ด้วยการเปลี่ยนแปลงนี้ เรากำลังเพิ่มพฤติกรรมที่เราระบุใน method replaceTrackListing แต่เราก็กำลังเปลี่ยนพฤติกรรมอย่างละเอียดอ่อนด้วย UI จะเรนเดอร์แตกต่างออกไปเมื่อมีปุ่มใหม่นี้ โอกาสสูงที่ UI จะใช้เวลาแสดงผลนานขึ้นอีกประมาณหนึ่งไมโครวินาที ดูเหมือนว่าแทบจะเป็นไปไม่ได้เลยที่จะเพิ่มพฤติกรรมโดยไม่เปลี่ยนแปลงมันในระดับหนึ่ง
Improving Design (การปรับปรุงการออกแบบ)
การปรับปรุงการออกแบบเป็นการเปลี่ยนแปลงซอฟต์แวร์อีกประเภทหนึ่ง เมื่อเราต้องการปรับเปลี่ยนโครงสร้างของซอฟต์แวร์เพื่อให้บำรุงรักษาได้ง่ายขึ้น โดยทั่วไปเราก็ต้องการคงพฤติกรรมเดิมเอาไว้ด้วย เมื่อเราทำพฤติกรรมหลุดหายไปในกระบวนการนั้น เรามักเรียกมันว่าบั๊ก สาเหตุหลักประการหนึ่งที่โปรแกรมเมอร์หลายคนไม่พยายามปรับปรุงการออกแบบบ่อยนักก็เพราะว่ามันค่อนข้างง่ายที่จะสูญเสียพฤติกรรมหรือสร้างพฤติกรรมที่ไม่ดีขึ้นในระหว่างกระบวนการ
การปรับปรุงการออกแบบโดยไม่เปลี่ยนพฤติกรรมเรียกว่า refactoring แนวคิดเบื้องหลัง refactoring คือเราสามารถทำให้ซอฟต์แวร์บำรุงรักษาได้ง่ายขึ้นโดยไม่เปลี่ยนพฤติกรรม ถ้าเราเขียนเทสเพื่อให้แน่ใจว่าพฤติกรรมเดิมไม่เปลี่ยนแปลง และค่อยๆ ทำเป็นขั้นเล็กๆ เพื่อตรวจสอบตลอดกระบวนการ ผู้คนทำความสะอาดโค้ดในระบบมานานหลายปี แต่เพิ่งไม่กี่ปีมานี้ที่ refactoring ได้รับความนิยมอย่างจริงจัง Refactoring แตกต่างจากการทำความสะอาดทั่วไปตรงที่เราไม่ได้แค่ทำสิ่งที่มีความเสี่ยงต่ำอย่างการจัดรูปแบบซอร์สโค้ดใหม่ หรือสิ่งที่รุกล้ำและเสี่ยงอย่างการเขียนส่วนใหญ่ใหม่ แต่เรากำลังทำการปรับเปลี่ยนโครงสร้างเล็กๆ หลายครั้ง โดยมีเทสสนับสนุนเพื่อทำให้โค้ดเปลี่ยนแปลงได้ง่ายขึ้น ประเด็นสำคัญของ refactoring ในมุมมองของการเปลี่ยนแปลงคือ ไม่ควรมีการเปลี่ยนแปลงฟังก์ชันการทำงานใดๆ เมื่อคุณ refactor (แม้ว่าพฤติกรรมอาจเปลี่ยนไปบ้างเนื่องจากการเปลี่ยนแปลงโครงสร้างที่คุณทำอาจส่งผลต่อประสิทธิภาพ ไม่ว่าจะดีขึ้นหรือแย่ลง)
Optimization (การเพิ่มประสิทธิภาพ)
Optimization ก็เหมือนกับ refactoring แต่เมื่อเราทำมัน เรามีเป้าหมายที่ต่างออกไป ทั้ง refactoring และ optimization เราพูดว่า "เราจะคงฟังก์ชันการทำงานให้เหมือนเดิมทุกประการเมื่อเราทำการเปลี่ยนแปลง แต่เราจะเปลี่ยนอย่างอื่น" ใน refactoring "อย่างอื่น" นั้นคือโครงสร้างของโปรแกรม เราต้องการทำให้ดูแลรักษาง่ายขึ้น ใน optimization "อย่างอื่น" คือทรัพยากรบางอย่างที่โปรแกรมใช้ โดยปกติคือเวลาหรือหน่วยความจำ
Putting It All Together (การรวมทุกอย่างเข้าด้วยกัน)
มันอาจดูแปลกที่ refactoring และ optimization คล้ายกัน พวกมันดูใกล้ชิดกันมากกว่าการเพิ่มฟีเจอร์หรือการแก้บั๊ก แต่นั่นเป็นความจริงจริงหรือ? สิ่งที่เหมือนกันระหว่าง refactoring และ optimization คือเราคงฟังก์ชันการทำงานให้ไม่เปลี่ยนแปลงในขณะที่ปล่อยให้อย่างอื่นเปลี่ยนไป
โดยทั่วไปแล้ว มีสามสิ่งที่สามารถเปลี่ยนแปลงได้เมื่อเราทำงานในระบบ: โครงสร้าง (structure), ฟังก์ชันการทำงาน (functionality), และการใช้ทรัพยากร (resource usage)
มาดูกันว่าอะไรที่มักจะเปลี่ยนแปลงและอะไรที่คงเดิมไม่มากก็น้อยเมื่อเราทำการเปลี่ยนแปลงสี่แบบที่แตกต่างกัน (ใช่ บ่อยครั้งทั้งสามอย่างเปลี่ยนแปลง แต่มาดูกรณีทั่วไปกัน):
เมื่อมองผิวเผิน refactoring และ optimization ดูคล้ายกันมาก พวกมันคงฟังก์ชันการทำงานให้ไม่เปลี่ยนแปลง แต่จะเกิดอะไรขึ้นเมื่อเราคิดถึงฟังก์ชันใหม่แยกต่างหาก? เมื่อเราเพิ่มฟีเจอร์ เรามักจะเพิ่มฟังก์ชันใหม่ โดยไม่เปลี่ยนแปลงฟังก์ชันที่มีอยู่
การเพิ่มฟีเจอร์ การ refactor และการ optimize ล้วนคงฟังก์ชันการทำงานที่มีอยู่ให้ไม่เปลี่ยนแปลง อันที่จริง ถ้าเราพิจารณาการแก้บั๊กอย่างละเอียด ใช่ มันเปลี่ยนฟังก์ชันการทำงาน แต่การเปลี่ยนแปลงมักจะเล็กน้อยมากเมื่อเทียบกับปริมาณฟังก์ชันการทำงานที่มีอยู่ที่ไม่ถูกเปลี่ยนแปลง
การเพิ่มฟีเจอร์และการแก้บั๊กก็คล้ายกับ refactoring และ optimization มาก ในทั้งสี่กรณี เราต้องการเปลี่ยนฟังก์ชันการทำงานบางอย่าง พฤติกรรมบางอย่าง แต่เราต้องการคงไว้ซึ่งมากกว่านั้นมาก (ดู Figure 1.1)
Figure 1.1 การคงพฤติกรรมเดิมไว้
นั่นเป็นมุมมองที่ดีเกี่ยวกับสิ่งที่ควรเกิดขึ้นเมื่อเราทำการเปลี่ยนแปลง แต่มันหมายความว่าอย่างไรในทางปฏิบัติ? ในด้านบวก มันดูเหมือนจะบอกเราว่าเราต้องให้ความสำคัญกับอะไร เราต้องแน่ใจว่าสิ่งจำนวนน้อยที่เราเปลี่ยนแปลงนั้นถูกต้อง ในด้านลบ ก็ใช่ว่านั่นจะเป็นสิ่งเดียวที่เราต้องให้ความสำคัญ เราต้องหาวิธีรักษาพฤติกรรมที่เหลือเอาไว้ น่าเสียดายที่การรักษามันไว้เกี่ยวข้องกับมากกว่าแค่การไม่ยุ่งกับโค้ด เราต้องรู้ว่าพฤติกรรมนั้นไม่เปลี่ยนแปลง และนั่นอาจเป็นเรื่องยาก ปริมาณพฤติกรรมที่เราต้องรักษาไว้มักจะมีมาก แต่นั่นไม่ใช่ประเด็นใหญ่ ประเด็นใหญ่คือเรามักไม่รู้ว่าพฤติกรรมมากแค่ไหนที่เสี่ยงเมื่อเราทำการเปลี่ยนแปลง ถ้าเรารู้ เราก็สามารถมุ่งความสนใจไปที่พฤติกรรมนั้นและไม่ต้องสนใจส่วนที่เหลือ ความเข้าใจ (Understanding) คือกุญแจสำคัญที่เราต้องการเพื่อทำการเปลี่ยนแปลงอย่างปลอดภัย
การรักษาพฤติกรรมเดิมที่มีอยู่เป็นหนึ่งในความท้าทายที่ใหญ่ที่สุดในการพัฒนาซอฟต์แวร์ แม้ในขณะที่เรากำลังเปลี่ยนฟีเจอร์หลัก เราก็มักจะมีพื้นที่พฤติกรรมขนาดใหญ่ที่เราต้องรักษาไว้
Risky Change (การเปลี่ยนแปลงที่มีความเสี่ยง)
การรักษาพฤติกรรมเป็นความท้าทายครั้งใหญ่ เมื่อเราต้องเปลี่ยนแปลงและรักษาพฤติกรรมไว้ มันอาจเกี่ยวข้องกับความเสี่ยงอย่างมาก
เพื่อลดความเสี่ยง เราต้องถามคำถามสามข้อ:
1. เราต้องเปลี่ยนแปลงอะไรบ้าง?
2. เราจะรู้ได้อย่างไรว่าเราทำถูกต้อง?
3. เราจะรู้ได้อย่างไรว่าเราไม่ได้ทำอะไรพัง?
คุณสามารถรับการเปลี่ยนแปลงได้มากแค่ไหนถ้าการเปลี่ยนแปลงมีความเสี่ยง?
ทีมส่วนใหญ่ที่ผมเคยทำงานด้วยพยายามจัดการความเสี่ยงในแบบที่อนุรักษ์นิยมมาก พวกเขาลดจำนวนการเปลี่ยนแปลงที่ทำกับโค้ดเบสให้เหลือน้อยที่สุด บางครั้งนี่เป็นนโยบายของทีม: "ถ้ามันไม่พัง ก็ไม่ต้องไปซ่อมมัน" บางครั้งก็ไม่ใช่สิ่งที่ใครพูดออกมา นักพัฒนาแค่ระมัดระวังอย่างมากเมื่อต้องเปลี่ยนแปลงอะไร "อะไรนะ? สร้างอีก method สำหรับเรื่องนั้น? ไม่ล่ะ ผมแค่ใส่โค้ดสองสามบรรทัดไว้ตรงนี้ใน method เดิม ที่ที่ผมเห็นมันและเห็นโค้ดส่วนอื่นๆ ได้ มันแก้ไขน้อยกว่า และปลอดภัยกว่า"
มันน่าดึงดูดที่จะคิดว่าเราสามารถลดปัญหาซอฟต์แวร์ได้โดยการหลีกเลี่ยงมัน แต่น่าเสียดายที่มันกลับมาทวงเราเสมอ เมื่อเราหลีกเลี่ยงการสร้างคลาสและ method ใหม่ คลาสและ method ที่มีอยู่ก็จะใหญ่ขึ้นและเข้าใจยากขึ้น เมื่อคุณเปลี่ยนแปลงอะไรในระบบขนาดใหญ่ คุณควรคาดหวังว่าจะต้องใช้เวลาสักหน่อยในการทำความคุ้นเคยกับพื้นที่ที่คุณกำลังทำงาน ความแตกต่างระหว่างระบบที่ดีและระบบที่ไม่ดีคือ ในระบบที่ดี คุณจะรู้สึกค่อนข้างสงบหลังจากที่ได้เรียนรู้แล้ว และคุณมั่นใจในการเปลี่ยนแปลงที่กำลังจะทำ ในโค้ดที่มีโครงสร้างไม่ดี การเปลี่ยนจากการทำความเข้าใจไปสู่การเปลี่ยนแปลงให้ความรู้สึกเหมือนกระโดดหน้าผาเพื่อหนีเสือ คุณลังเลแล้วลังเลอีก "ฉันพร้อมจะทำมันหรือยัง? เออ คงต้องทำ"
การหลีกเลี่ยงการเปลี่ยนแปลงยังมีผลเสียอื่นๆ อีก เมื่อคนไม่เปลี่ยนแปลงอะไรบ่อยๆ พวกเขาจะฝีมือตก การแยกคลาสใหญ่เป็นชิ้นส่วนอาจเป็นงานที่ยุ่งยากพอสมควร เว้นแต่คุณจะทำมันสัปดาห์ละสองสามครั้ง เมื่อคุณทำมันบ่อย มันกลายเป็นกิจวัตร คุณจะเก่งขึ้นในการหาว่าอะไรพังได้และอะไรพังไม่ได้ และมันก็ง่ายขึ้นมากที่จะทำ
ผลเสียสุดท้ายของการหลีกเลี่ยงการเปลี่ยนแปลงคือความกลัว น่าเสียดายที่หลายทีมใช้ชีวิตอยู่กับความกลัวการเปลี่ยนแปลงอย่างไม่น่าเชื่อ และมันแย่ลงทุกวัน บ่อยครั้งที่พวกเขาไม่รู้ตัวว่ากลัวมากแค่ไหน จนกว่าพวกเขาจะเรียนรู้เทคนิคที่ดีกว่าและความกลัวเริ่มจางหายไป
เราได้พูดถึงว่าการหลีกเลี่ยงการเปลี่ยนแปลงเป็นสิ่งที่ไม่ดี แต่อะไรคือทางเลือกของเรา? ทางเลือกหนึ่งคือการพยายามให้หนักขึ้น บางทีเราอาจจ้างคนเพิ่มเพื่อให้ทุกคนมีเวลามานั่งวิเคราะห์ ตรวจสอบโค้ดทั้งหมด และทำการเปลี่ยนแปลงในแบบที่ "ถูกต้อง" แน่นอนว่าเวลาที่มากขึ้นและการตรวจสอบที่มากขึ้นจะทำให้การเปลี่ยนแปลงปลอดภัยขึ้น หรือว่าจะไม่? หลังจากการตรวจสอบทั้งหมดนั้น จะมีใครรู้ไหมว่าพวกเขาทำมันถูกต้อง?