คุณเพิ่งได้รับสืบทอดโค้ดเบส C++ เลกาซีมา แล้วควรทำอย่างไรต่อ?
(gaultier.github.io)- หากคุณต้องรับผิดชอบ โค้ดเบส C++ เลกาซี ขนาดใหญ่และซับซ้อน เป้าหมายไม่ใช่ “โค้ดสะอาด” แต่คือการยกระดับความปลอดภัย ประสบการณ์ของนักพัฒนา ความถูกต้อง และประสิทธิภาพให้อยู่ในระดับที่ยอมรับได้
- จุดเริ่มต้นคือการขอ ความเห็นชอบและกรอบเวลา จากองค์กร ระบุแพลตฟอร์มที่รองรับไว้ใน README แล้วทำให้การ build และ test ในเครื่อง local ผ่านได้อย่างเสถียร
- การปรับปรุงความเร็วของ build/test ควรเริ่มจาก งานที่ทำได้ง่าย พร้อมการวัดผล เช่น ตัดการทดสอบของ dependency ออก จัดระเบียบ target ที่ไม่จำเป็น และทดลอง linker ที่เร็วอย่าง
mold - หลังจากลดโค้ดที่ไม่ได้ใช้และโค้ดสำหรับแพลตฟอร์มที่ไม่รองรับแล้ว ควรสร้าง ลูปตรวจสอบอัตโนมัติ ด้วย
clang-tidy,cppcheck,clang-format,-fsanitize=address,undefinedและ CI - การ rewrite หรือการนำมาตรฐาน C++ รุ่นใหม่มาใช้ไม่ใช่เป้าหมาย แต่เป็นเครื่องมือ ส่วนการเขียนใหม่ด้วยภาษาที่ปลอดภัยด้านหน่วยความจำก็ควรพิจารณาเฉพาะเมื่อมี เหตุผลที่หนักแน่น เท่านั้น
ช่วงแรกให้จัดการคนและกระบวนการก่อนโค้ด
- การปรับปรุงโค้ดเบส C++ เลกาซีไม่ควรเป็นงานที่แอบผลักดันคนเดียวหลังเลิกงาน หรือ “death march” ระยะยาว แต่ควรเป็น งานวิศวกรรมซอฟต์แวร์ ที่ยั่งยืน
- ต้องอธิบายปัญหา วิธีแก้ และกรอบเวลาให้หัวหน้า เพื่อนร่วมงาน และสมาชิกที่ไม่ใช่สายเทคนิคเข้าใจด้วยภาษาง่าย ๆ
- หากพนักงานใหม่ใช้เวลา 3 สัปดาห์กว่าจะ build ในเครื่อง local และทำ contribution แรกได้ ก็สามารถเสนอเป้าหมายให้ลดเหลือไม่กี่นาทีได้
- หากการตั้งค่า fuzzing แบบง่าย ๆ ทำให้แอป crash 253 ครั้งภายในไม่กี่วินาที นั่นเป็นหลักฐานสำหรับอธิบายความเสี่ยงใน production
- หากการ deploy ขึ้นอยู่กับ build server FreeBSD 9 เพียงเครื่องเดียวที่ไม่ได้รับการรองรับมา 8 ปีแล้ว เมื่อ server นั้นล่ม การ deploy ก็จะหยุดชะงัก
- หาก linter มาตรฐานอุตสาหกรรมสามารถจับ undefined behavior ที่เป็นสาเหตุของ bug ใน production ได้ทันที ก็มีเหตุผลเพียงพอที่จะรันทุกครั้งที่มีการเปลี่ยนแปลง
- หากไม่สามารถทราบผลกระทบของช่องโหว่ได้เพราะมีไลบรารีเข้ารหัสที่คัดลอกมาแก้ไขเองแบบ manual ก็จำเป็นต้องจัดการ dependency และตั้งการแจ้งเตือนอัตโนมัติ
- แนวทางที่ควรหลีกเลี่ยงก็ต้องชัดเจนเช่นกัน
- ไม่มี test แต่ทำการอัปเกรดทั้งหมดเป็นมาตรฐาน C++ รุ่นใหม่ตลอด 2 สัปดาห์
- สร้างการเปลี่ยนแปลงขนาดใหญ่ใน branch แยกเป็นเวลาหลายเดือน แล้วคาดหวังว่าวันหนึ่งจะ merge ได้
- เริ่มเขียนใหม่ทั้งหมดตั้งแต่แรกโดยสมมติว่าจะเสร็จในไม่กี่สัปดาห์
- เริ่ม “ปรับปรุงโค้ดเบส” โดยยังไม่รู้ว่าจะทำอะไรและให้เสร็จเมื่อไร
วิธีปรับปรุงอย่างปลอดภัย
- ทุกการเปลี่ยนแปลงควรเล็กและค่อยเป็นค่อยไป และก่อน/หลังการเปลี่ยนแปลง แอปต้องยังทำงาน รวมถึง test และ linter ต้องผ่าน
- การแก้ bug ด่วนต้องยังทำได้เหมือนเดิม และไม่ควรถูกขัดขวางเพราะงานปรับปรุง
- แต่ละการเปลี่ยนแปลงควรเป็นการปรับปรุงที่วัดผลได้ และสามารถอธิบายหรือสาธิตให้คนที่ไม่ใช่ผู้เชี่ยวชาญเข้าใจได้
- แม้งานทั้งหมดจะหยุดลงเพราะปัญหาเรื่องลำดับความสำคัญหรืองบประมาณ ก็ต้องเหลือ ผลสุทธิที่วัดได้ มากกว่าก่อนเริ่ม
ระบุแพลตฟอร์มที่รองรับไว้ใน README
- README ควรระบุคู่
<architecture>-<operating-system>ที่รองรับอย่างเป็นทางการ- ตัวอย่าง:
x86_64-linux,aarch64-darwin
- ตัวอย่าง:
- รายการนี้เป็นเกณฑ์ตรวจสอบว่า build ได้บนทุกแพลตฟอร์มที่รองรับหรือไม่ และภายหลังจะเป็นเหตุผลสำหรับลบโค้ดสำหรับแพลตฟอร์มที่ไม่รองรับ
- หากจำเป็น อาจระบุเวอร์ชันสถาปัตยกรรม เช่น ARMv6, ARMv7 ด้วย
- รายการแพลตฟอร์มที่รองรับช่วยตอบคำถามต่อไปนี้
- สามารถพึ่งพา floating point, SIMD, SHA256 hardware support ได้หรือไม่
- จำเป็นต้องรองรับ 32-bit หรือไม่
- รันบนแพลตฟอร์ม big-endian หรือไม่
- ต้องพิจารณาความเป็นไปได้ที่
charจะเป็น 7-bit หรือไม่
- workstation ของนักพัฒนาต้องรวมอยู่ในรายการนี้ด้วย
ทำให้ local build และ test เสถียรก่อน
- แม้แต่โค้ดเบส C++ หลักของผลิตภัณฑ์ที่ประสบความสำเร็จก็มัก compile ได้ไม่เสถียร เป้าหมายไม่ใช่ “build ที่สำเร็จเป็นบางครั้ง” แต่คือ build ที่สำเร็จอย่างสม่ำเสมอ บนทุกแพลตฟอร์มที่รองรับ
- สถานะที่ดีที่สุดคือสามารถ build และรันบนเครื่องของนักพัฒนาได้โดยตรง
- หากโปรเจกต์ใหญ่เกินไปจน RAM ไม่พอ อาจเช่า server ขนาดใหญ่เพื่อใช้ build ได้
- หากต้องใช้ API เฉพาะแพลตฟอร์มอย่าง
io_uringอาจ implement shim หรือ build ภายใน virtual machine บน workstation ได้ - ถึงอย่างนั้น การ build ในเครื่อง local โดยตรงก็ยังเป็นตัวเลือกที่ดีที่สุด
- หากไม่มี test ต้องเขียน test ก่อนแก้โค้ด
- วิธีเริ่มที่ง่ายที่สุดคือ capture input และ output ของโปรแกรมที่รันจริง แล้วทำเป็น end-to-end test
- test แบบนี้ไม่ได้รับประกันว่าพฤติกรรมเดิมถูกต้อง แต่ช่วยลด regression เมื่อมีการเปลี่ยนแปลง
- หากมี test suite อยู่แล้วแต่บางส่วน fail ให้ disable ไว้ก่อน แล้วทำให้ test ทั้งหมดอยู่ในสถานะที่ผ่าน แม้จะใช้เวลาหลายชั่วโมงก็ตาม
ทำเอกสารวิธี build และ test
- README ควรระบุวิธี build และ test แอป
- รูปแบบในอุดมคติคือมีคำสั่ง build หนึ่งคำสั่งและคำสั่ง test หนึ่งคำสั่ง
- หากช่วงแรกขั้นตอนซับซ้อน สามารถห่อคำสั่งไว้ใน
build.sh,test.shเพื่อซ่อนความซับซ้อนได้ - เป้าหมายคือให้แม้แต่คนที่ไม่ใช่ผู้เชี่ยวชาญ C++ ก็สามารถรัน build และ test ได้โดยไม่ต้องถาม
- เอกสารโครงสร้างโปรเจกต์หรือสถาปัตยกรรมควรทำภายหลัง หลังจากลบโค้ดที่ไม่จำเป็นออกแล้วจะดีกว่า
ลดเวลา build และ test ด้วยงานที่ทำง่าย
- ไม่ต้องยกเครื่องระบบ build หรือทำ optimization แบบฮีโร่ แต่ควรเริ่มวัดจาก งานที่ทำได้ง่าย ก่อน
- รายการที่ควรตรวจสอบก่อน
- ตรวจสอบว่ามีการ build และรัน test ของ dependency ทุกครั้งหรือไม่
- เคยมีกรณีใช้
unittest++เป็น CMake subproject แล้ว test ของ testing framework เองก็ถูก build/รันทุกครั้ง
- เคยมีกรณีใช้
- ตรวจสอบว่ามีการ build และรันโปรแกรมตัวอย่างของ dependency หรือไม่
mbedtlsก็สามารถปิดการ build ตัวอย่างได้ด้วยตัวแปร CMake
- ตรวจสอบว่าเมื่อโปรเจกต์ของคุณถูก include เป็น subproject ของโปรเจกต์อื่น มีการ build test และตัวอย่างเป็นค่า default หรือไม่
- แนะนำให้ตั้งตัวแปร build อย่าง
MYPROJECT_TESTให้ปิดโดย default และเปิดเฉพาะตอนพัฒนาโดยตรง
- แนะนำให้ตั้งตัวแปร build อย่าง
- ตรวจสอบว่ามีการ build dependency จาก third-party ทั้งหมด ทั้งที่จริงใช้เพียงส่วนเล็ก ๆ หรือไม่
mbedtlsมี compile-time flag จำนวนมากสำหรับปิดส่วนที่ไม่จำเป็น
- ตรวจสอบว่า target dependency ผิดพลาดจนการเปลี่ยนแปลงเล็ก ๆ ทำให้ต้อง build ใหม่ทั้งหมดหรือไม่
- build system จำนวนมากสามารถ output dependency graph ได้
- ทดลอง linker ที่เร็วอย่าง
mold - หากเป็นไปได้ ให้เปรียบเทียบ compiler ด้วย
- บางโปรเจกต์พบว่า clang เร็วกว่า gcc 2 เท่า ขณะที่บางโปรเจกต์ไม่มีความต่าง
- ตรวจสอบว่ามีการ build และรัน test ของ dependency ทุกครั้งหรือไม่
- รายการที่ทดลองเพิ่มได้ แต่ผลประโยชน์อาจน้อยหรือเป็นลบ
- ปิด/เปิด/thin LTO
- แยก debug information
- เปรียบเทียบ Make กับ Ninja
- ประเภทและการตั้งค่าของ file system
- หาก build ใช้เวลานานเกินไป การแก้โค้ดเองก็ไม่สมจริงแล้ว
ลบโค้ดที่ไม่จำเป็น
- เคยมีกรณีที่มากกว่า 30% ของโค้ดเบสเป็นโค้ดตายสนิท และโค้ดแบบนี้เพิ่มต้นทุน compile และ refactor อย่างต่อเนื่อง
- วิธีลบ
- ใช้ warning
-Wunused-xxxของ compiler- ตัวอย่าง:
-Wunused-function - ส่วนใหญ่ลบแล้ว rebuild/test ก็จบ แต่ในบางกรณีที่พบได้ยาก อาจเป็นอาการของ bug ที่เรียกฟังก์ชันผิด จึงต้องระวังการทำอัตโนมัติเต็มรูปแบบ
- ตัวอย่าง:
- ใช้ linter อย่าง
cppcheckหา function หรือ field ของ class ที่ไม่ได้ใช้- ในกรณี inheritance และ virtual function อาจมี false positive มาก แต่ก็สามารถหาสิ่งที่ compiler หาไม่เจอได้
- มีวิธีให้ linker ใส่แต่ละ function ไว้คนละ section แล้ว output การลบ section ที่ไม่ได้ใช้ แต่ noise จากฟังก์ชันของ standard library อาจมากจนไม่ practical
- วิธีเปรียบเทียบ assembly ที่ generated กับ source code ไม่เหมาะกับ virtual function นัก
- ใช้ warning
- ใช้รายการแพลตฟอร์มที่รองรับเพื่อลบ โค้ดสำหรับแพลตฟอร์มที่ไม่รองรับ
- โค้ดรองรับ Solaris เก่าของโปรเจกต์ที่จริง ๆ รันเฉพาะบน FreeBSD
- โค้ดที่มี random number generator ของตัวเอง ทั้งที่แพลตฟอร์มจริงมีตัวสร้างเลขสุ่มเสมอ
- โค้ดเตรียมรับมือ POSIX 2001 ที่ไม่รองรับ ทั้งที่รันเฉพาะบน Linux และ macOS สมัยใหม่
- โค้ดตรวจว่า CPU เป็น big-endian หรือไม่แล้ว byte swap
- โค้ดที่ถูกเพิ่มไว้หลายปีก่อนแต่ไม่เคยต่อยอดเป็นฟีเจอร์จริง
- PR ที่ลบโค้ดจำนวนมากสามารถแสดงได้พร้อมกันทั้งเวลา build ที่ลดลงและต้นทุนบำรุงรักษาที่ลดลง
ใส่ linter, formatter และ sanitizer
- linter ไม่ควรเปิดกฎมากเกินไป แต่ควรเริ่มนำกฎพื้นฐานไม่กี่ข้อเข้าสู่ lifecycle การพัฒนา
clang-tidy,cppcheckอาจมีประโยชน์ แต่ช้าและมี noise มากได้- การไม่มี linter เลยไม่ใช่ทางเลือก และเมื่อรันครั้งแรก มักพบปัญหาจริงจำนวนมากที่ compiler warning อย่างเดียวจับไม่ได้
- ควรใช้ code formatting ทั้งหมดในคราวเดียวเมื่อถึงเวลาที่เหมาะสม
- ควรรอช่วงที่ไม่มี active branch เพื่อหลีกเลี่ยง merge conflict อันเลวร้าย
- อย่าเสียเวลากับการถกเถียงเรื่อง style ให้ใช้เครื่องมืออย่าง
clang-formatformat ทั้งโค้ดเบสโดยไม่มีข้อยกเว้น - commit configuration ไปพร้อมกันด้วย
- sanitizer จำเป็นสำหรับจับ bug ที่หายากแต่กระทบ production ได้จริง
- แนะนำ
-fsanitize=address,undefinedเป็นค่า default - โดยทั่วไปแทบไม่มี false positive ดังนั้นหากตรวจพบก็ควรแก้
- ควรรัน test โดยเปิด sanitizer เพื่อหาปัญหาด้วย
- หาก performance budget อนุญาต อาจพิจารณาการรันใน production โดยเปิด sanitizer บางตัว
- แนะนำ
- แม้ compiler สำหรับ deploy จะไม่รองรับ sanitizer ก็สามารถใช้ compiler อย่าง clang ในการพัฒนาและทดสอบได้
- เมื่อเปิด sanitizer อาจเผย bug และ memory leak ที่ซ่อนอยู่มานาน และการแก้อาจต้องใช้ทั้งงานจำนวนมากและ refactoring
- หากเป็นไปได้ ตอน test ควร compile third-party dependency โดยเปิด sanitizer ด้วย เพื่อหา ปัญหาภายใน dependency
ทำให้เป็นอัตโนมัติด้วย CI
- CI ทำให้ linter, formatting, test ฯลฯ ที่ตั้งค่าไว้จนถึงตอนนี้รันอัตโนมัติในสภาพแวดล้อมสะอาด
- ควรสามารถสร้าง production binary ได้ทุกการเปลี่ยนแปลง
- ระบบ CI ส่วนใหญ่รองรับ matrix หลายแพลตฟอร์ม จึงสามารถตรวจสอบว่ารายการแพลตฟอร์มที่รองรับใน README build ได้จริงหรือไม่
- pipeline ทั่วไปอาจเรียบง่ายอย่าง
make all test lint fmt - ปัญหาที่ linter และ sanitizer รายงานควรทำให้ pipeline fail มิฉะนั้นจะไม่มีใครแก้
ทำให้โค้ดเรียบง่ายขึ้นทีละน้อย
- หลังจากมีลูป build/test/verification ที่เสถียรแล้ว ก็สามารถทำให้โค้ดเรียบง่ายขึ้นทีละน้อยได้
- หาก class ที่ซับซ้อนจริง ๆ แล้วทำหน้าที่แค่ allocate pointer และตรวจว่าเป็น null หรือไม่ ก็มีกรณีที่แทนได้ด้วย boolean ในทางปฏิบัติ
- ขั้นตอนนี้กำหนดกรอบเวลาได้ยาก เพราะยิ่งทำให้เรียบง่าย ก็ยิ่งเปิดจุดให้ทำให้เรียบง่ายต่อ จึงควรตัดสินอย่างระมัดระวัง
- เป้าหมายควรเป็นคุณค่าที่เป็นรูปธรรม เช่น ความปลอดภัย ความถูกต้อง และประสิทธิภาพ ไม่ใช่เกณฑ์เชิงอัตวิสัยอย่าง “clean code”
- การอัปเกรดมาตรฐาน C++ ไม่ใช่เป้าหมาย แต่เป็นเครื่องมือ
- ตัวอย่างเช่น อาจช่วยเปลี่ยนโค้ดที่เพิ่ม iterator ด้วยมือเป็น loop แบบ
for (auto x : items)ได้ - หากสิ่งที่ต้องการมีเพียง
std::clampตัวเดียว การเขียนเองอาจดีกว่า
- ตัวอย่างเช่น อาจช่วยเปลี่ยนโค้ดที่เพิ่ม iterator ด้วยมือเป็น loop แบบ
การเขียนใหม่ด้วยภาษาที่ปลอดภัยด้านหน่วยความจำต้องพิจารณาแยกต่างหาก
- ตัวเลือกในการเขียนบางส่วนใหม่ด้วยภาษาที่ปลอดภัยด้านหน่วยความจำนั้นเป็นไปได้ แต่มีข้อควรระวังมาก
- ควรดำเนินการเฉพาะเมื่อมีเหตุผลที่หนักแน่นเท่านั้น
การจัดการ dependency ควรเน้น build จาก source
- C++ ไม่มีการจัดการ dependency ที่สอดคล้องกัน และหลายโปรเจกต์ใช้ system package manager
- ปัญหาของแนวทางพึ่งพา system package
- คำแนะนำการติดตั้งขึ้นอยู่กับ OS, distribution และเวอร์ชันของ distribution
- เมื่อย้ายจาก Ubuntu 20.04 ไป 22.04 อาจเกิดสถานการณ์ที่ package version เปลี่ยน ทำให้ต้องอัปเกรด dependency 100 ตัวพร้อมกัน
- third-party dependency ที่ไม่มีเป็น package ก็ต้อง build จาก source อยู่ดี
- package ไม่ได้ build ด้วย flag ที่ต้องการ
- sanitizer, LTO,
-march, debug information, frame pointer และความต่างของ C++ ABI อาจกลายเป็นปัญหา
- sanitizer, LTO,
- ยากที่จะดู source ของเวอร์ชันที่ใช้อยู่จริงในเวลาตรวจสอบ พัฒนา หรือ debug
- patch dependency แล้ว build ใหม่ได้ยาก
- ใช้ package version เดียวกันแบบเป๊ะ ๆ ระหว่าง macOS, Ubuntu และ FreeBSD ได้ยาก
- ทำ BOM อัตโนมัติได้ยากขึ้น
- อาจไม่มีรูปแบบ package ที่ต้องการ เช่น static/dynamic library
- package manager สำหรับ C++ อย่าง Conan, vcpkg ก็อาจเป็นการปรับปรุงได้ แต่มีข้อจำกัด
- CI อาจซับซ้อนและช้าลงเพราะ external dependency
- อาจไม่มีทุก package version
- mbedtls ของ Conan มีตัวอย่างที่ข้ามจาก
2.16.12ไป2.23.0
- mbedtls ของ Conan มีตัวอย่างที่ข้ามจาก
- อาจไม่รองรับ OS หรือ architecture ที่ต้องการ
- แนวทางที่แนะนำคือดึง dependency ด้วย git submodule แล้ว compile จาก source
- เรียบง่าย
- ใช้ประโยชน์จาก git history และ diff ได้ดีกว่าการ vendoring แบบ manual
- รู้เวอร์ชัน dependency ได้แม่นยำระดับ commit
- อัปเกรด dependency เดี่ยวได้ด้วย
git checkout - ทำงานได้บนทุกแพลตฟอร์ม
- เลือก compile flag และ compiler แยกตาม dependency ได้
- แม้นักพัฒนาจะไม่มีประสบการณ์ C++ ก็ยังรู้จัก git
- ทำงานแบบ recursive ได้
- สามารถ build submodule แต่ละตัวได้ด้วย
add_subdirectoryของ CMake หรือgit submodule foreach make - หาก submodule ยากเกินไป ก็ใช้วิธีที่ script เดียวไปดึงและ build dependency แบบ Neovim ได้
- หาก dependency graph ใหญ่มาก อาจต้องใช้ระบบ build แบบ hybrid local/remote เช่น Buck2 และการ reuse build artifact
- package manager ของภาษาคอมไพล์อย่าง Go, Rust ใช้แนวทาง compile จาก source
ประเด็นที่เสริมจากข้อเสนอของผู้อ่าน
- ควรเน้นเรื่อง test มากขึ้น และ test suite ของ C++ ต้องรันภายใต้ sanitizer เพื่อไม่ให้เกิดความรู้สึกปลอดภัยแบบผิด ๆ
- vcpkg อาจเป็นตัวเลือกที่ดีกว่า git submodule หากตอบโจทย์ requirement และ cross-compilation
- Nix สามารถทำหน้าที่เป็น dependency manager ของ C++ ได้ แต่มีปัญหาเรื่องความซับซ้อนและความช้า
- หากปีหนึ่งแก้ bug เพียงตัวเดียว การลงทุน refactor ขนาดใหญ่เป็นเรื่องที่ต้องใช้ดุลยพินิจ แต่การลบ dead code และใช้ sanitizer ก็ยังมีคุณค่าแม้แก้ไขไม่บ่อย
- การลบโค้ดควรมุ่งไปที่กรณีที่ยืนยันด้วย static analysis ได้ว่าไม่ถูกเรียกใช้ และหากสงสัยก็ปลอดภัยกว่าที่จะไม่ลบ
- virtual method ถูกตัดสินเป้าหมายการเรียกใน runtime จึงลบด้วย static analysis ได้ยาก
- การคุย 15 นาทีกับฝ่ายขาย product manager และผู้ใช้เพื่อถามว่ามีการใช้ฟีเจอร์/แพลตฟอร์มหรือไม่ อาจลดงานเทคนิคลงได้มาก
- การใส่โค้ดลงใน LLM แล้วถามคำถามควรทำเมื่อปลอดภัยทางกฎหมาย และถ้าเป็นไปได้ให้รันใน local พร้อมรับผลลัพธ์อย่างระมัดระวัง
- มีไอเดียใช้เครื่องมือวิเคราะห์โค้ดสร้าง diagram และความสัมพันธ์ของ class เพื่อทำความเข้าใจโครงสร้างโดยรวม
- หากไม่มีระบบ source control ขั้นที่ 0 คือใส่โค้ดเข้า VCS
- มีความเห็นว่าควรมอง CI เป็นขั้นที่ 1 ด้วย และแม้ local จะเร็วกว่า ก็เป็นมุมมองที่สมเหตุสมผล
- หากลบแพลตฟอร์มที่แทบไม่ถูกใช้ จะลดความซับซ้อนเชิง combinatorial และทำให้ simplify ได้มาก
- build ที่ reproducible อย่างสมบูรณ์อาจไม่ realistic สำหรับโค้ดเบส C++ ทั่วไป แต่ build ที่เชื่อถือได้ นั้น realistic
- commit ที่ format โค้ดทั้งชุดสามารถตั้งค่าให้ git ignore commit นั้นจาก blame ได้ เพื่อลดภาระในการตามประวัติ
- มีไอเดียใช้สถิติจากประวัติ VCS เพื่อหาส่วนที่เปลี่ยนบ่อยและไฟล์ที่มักเปลี่ยนพร้อมกัน
- แนวทางนี้ใช้ได้ไม่เฉพาะ C++ แต่กับโค้ดเบสเลกาซีภาษาอื่นด้วย เพียงตัดส่วนเฉพาะ C++ อย่าง sanitizer ออก
Working effectively with Legacy Codeเป็นหนังสือที่แนะนำซึ่งมีคำแนะนำที่เกี่ยวข้อง- ควรมุ่งที่คุณค่าจริง แต่เมื่อมองโค้ดเบส C++ ขนาดใหญ่จากมุมความปลอดภัย อาจพบช่องโหว่จำนวนมาก และคุณค่านี้คือการลดความเสี่ยง ไม่ใช่ผลประโยชน์ทางการเงิน
1 ความคิดเห็น
ความเห็นจาก Hacker News
มีทั้งคำแนะนำที่ดีและคำแนะนำที่ค่อนข้างชวนถกเถียงอยู่บ้าง จากการเคยรับช่วงต่อ โปรเจ็กต์ C++ ขนาดใหญ่มาหลายตัว งานที่ให้ผลคุ้มมากอย่างชัดเจนในช่วงแรกดูจะมีอยู่ไม่กี่อย่าง
ควรเริ่มจากทำ build ที่ทำซ้ำได้ แล้วห่อสภาพแวดล้อมการ build ด้วย Docker หรือเครื่องมือแพ็กเกจจิงที่คุณถนัด เพื่อให้เครื่องมือและ dependency ถูกระบุอย่างชัดเจนและทำซ้ำได้
ถ้าทำให้ build ผ่านแบบไม่มี warning ด้วย
-Wallได้ ก็จะช่วยเผยโค้ดที่แย่, พฤติกรรมที่ไม่กำหนดไว้, และบั๊กต่าง ๆ ออกมา และหลังจากนั้นเมื่อฉันใส่โค้ดที่น่าสงสัยเข้าไป ก็จะเห็น warning ได้ทันทีการใช้เครื่องมืออย่าง
valgrindตั้งแต่ต้นเพื่อตรวจข้อผิดพลาดการอ่าน/เขียนก็เป็นผลลัพธ์ที่ได้ง่ายในแง่ความเสถียรเช่นกัน และก่อนจะเข้าใจโครงสร้างทั้งหมดดี การจำกัดการรีแฟกเตอร์ไว้เฉพาะจุดย่อย ๆ จะปลอดภัยกว่าการอ้างอิง image tag ที่เปลี่ยนได้โดยไม่มี integrity hash หรือ Dockerfile ที่เรียก
apt-getเป็นเรื่องที่พบได้บ่อย และทั้งสองอย่างนี้เปิดช่องกว้างให้ผลลัพธ์จบลงในสถานะที่ต่างกันแบบละเอียดอ่อนแม้จะเรียนรู้ยากกว่า แต่แนวทางอย่าง Bazel หรือ Nix ให้รากฐานที่ดีกว่ามาก
ขั้นที่ 2 คือรันทุกเทสต์ภายใต้ sanitizer แล้วติดป้ายตัวที่ล้มเหลว ขั้นที่ 3 คือแก้ sanitizer failure ทั้งหมดนั้น และขั้นที่ 4 คืองานที่เหลือ
คอนเทนเนอร์นั้นดี แต่ถ้าใช้เป็นวิธีทำเป็นว่าปัญหาไม่มีอยู่ก็น่ากลัวมาก
คุณควรแพ็ก dependency ให้ครบ และหลีกเลี่ยงสภาพที่ทุกอย่างจะทำงานได้ก็ต่อเมื่อมีของเป็นพันล้านชิ้นติดตั้งไว้ใน
/usr/libด้วยเวอร์ชันที่ถูกต้องพอดี-Wallนั้นโอเค แต่ผมคัดค้านอย่างหนักกับการใส่-Wall -Werrorใน production buildcompiler warning บางอย่างมีลักษณะเป็นเรื่องของความเห็นมากกว่า และถ้า compiler เวอร์ชันใหม่เพิ่ม warning ใหม่ โค้ดที่เคยสะอาดก็อาจถูกปฏิเสธขึ้นมาทันที
ถ้าจำเป็นต้องใช้
-Werrorก็ควรใช้เฉพาะใน debug build จะดีกว่าผมจะสลับลำดับของข้อ 2 กับข้อ 3 โดยให้ CI, linting, การจัดรูปแบบอัตโนมัติ และสิ่งทำนองนั้นมาก่อนการรื้ออะไรออกไป
เพราะตอนนี้ยังไม่รู้ว่าควรลบอะไร และถ้าลบแล้วผลจะเป็นอย่างไร
linter และเครื่องมือวิเคราะห์แบบสถิติมอบข้อมูลเชิงลึกมากมายว่าควรลงมือแก้ตรงส่วนไหนของโปรแกรม
จุดที่เครื่องมือวิเคราะห์แบบสถิติชี้วันนี้ มักเป็นจุดที่ภายหลังคุณอาจลบฟังก์ชัน, คลาส, หรือไฟล์ทั้งก้อนได้ เพราะมันเป็นการ reimplement แนวคิดใน STL
คุณอาจแทนที่ไลบรารี iterator ที่ทำเอง, smart pointer ที่ทำเอง, หรือการใช้ฟังก์ชันสตริงแบบ C ด้วย STL algorithm, smart pointer จริง ๆ, และคลาสสตริงของ C++ ได้
แต่ก่อนสแกนโค้ดก็มักมองไม่เห็นสิ่งเหล่านี้ชัดนัก และก่อนจะผลักไปทาง build ทดสอบที่เร็วหรือการ deploy ก็ประเมินผลลัพธ์ได้ยาก
ตรงไหนที่ขาดก็เพิ่ม smoke test พื้นฐานเข้าไป
งานนี้ไม่ได้ทำให้ช้าลง แต่กลับทำให้เร็วขึ้นด้วยซ้ำ เพราะเมื่อมี coverage ที่พอเหมาะแล้ว คุณจะรีแฟกเตอร์ได้เร็วขึ้นมาก ถือเป็นการลงทุนเล็กน้อยที่คุ้มมาก
โปรเจ็กต์ C++ ขนาดใหญ่มักไม่ได้มีแค่สคริปต์สร้างโค้ดเท่านั้น แต่ยังมีสคริปต์ที่ parse โค้ดเพื่อรวบรวมข้อมูลสำหรับการสร้างโค้ดด้วย
การจัดรูปแบบอัตโนมัติอาจทำให้สิ่งเหล่านี้พังได้ และผมยังเคยเห็นโปรเจ็กต์อาถรรพ์ที่ผู้ใช้ parse ไฟล์ public header ด้วยสคริปต์เปราะบางอีกด้วย
มีโอกาสสูงที่คุณจะต้องไปแตะหลายจุด และเมื่อมีการเปลี่ยนแปลงสะสมอยู่ในเครื่องมากขึ้น ก็อาจเผลอสร้าง commit ที่ปนหลายประเด็นเข้าด้วยกัน
ตอนนั้น CI จะเป็นผู้กอบกู้
ถ้าจะไปทางนั้น อย่างน้อยผมแนะนำว่าอย่าเพิ่งลบทันที แต่ให้คอมเมนต์ทิ้งไว้ก่อนจนกว่าคุณจะพร้อม squash แล้ว merge branch เพื่อให้ diff ยังเรียบง่าย
ยังขาดเครื่องมือและเทคนิคสำหรับทำความเข้าใจโค้ด ผมเคยใช้เครื่องมือชื่อ Source Navigator ที่เขียนด้วย Tcl/Tk ซึ่งดีมากสำหรับการทำดัชนีโค้ดเบส
มันดูลำดับชั้นการเรียกของเมธอดปัจจุบันได้ และใช้สร้าง UML sequence diagram จากสิ่งนั้นได้
เครื่องมือคล้ายกันอีกตัวก็คือ Source Insight ด้านล่าง
และการจดโน้ตก็สำคัญมาก หัวใจคือเขียนให้เหมือนกำลังสอนใครสักคน
ตลอดหลายปีที่ผ่านมา ผมทำความเข้าใจโค้ดได้ค่อนข้างดี และช่วงหนึ่งก็เคยซัพพอร์ตและพัฒนาโค้ดเบส Java สำหรับ algorithmic trading อย่างจริงจังอยู่คนเดียว ซึ่งเทรดวันละราว 200 ล้านดอลลาร์ผ่านตลาด 4-5 แห่ง
เอกสารของโค้ดนั้นมีขนาดถึง 35MB และถ้าไม่นับความเสี่ยงด้านบุคลากรหลัก มันก็ให้ความรู้สึกรับผิดชอบที่ดี
พูดตามตรง โค้ดเบสขนาดใหญ่ส่วนมากมีทั้งการออกแบบเกินจำเป็นและความซ้ำซ้อน
[1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...
ตอนเรียนวิชาสถาปัตยกรรมคอมพิวเตอร์ในมหาวิทยาลัย เราถูกบังคับให้ใช้โบราณวัตถุชิ้นนี้ เพราะมันมีชุดฟีเจอร์ประหลาดที่ที่อื่นไม่มี รวมถึงการรองรับ ARM emulator
พวกเราใช้มันกับการเขียนโปรแกรม bare-metal ARM assembly
ตอนที่บอกว่าใส่
std::cmakeเข้าไปในไลบรารีมาตรฐานจะเป็นตัวเปลี่ยนเกมนี่ทำเอารู้สึกอิหลักอิเหลื่อมากผมเข้าใจคำแนะนำที่ว่าให้ใช้เลื่อยยนต์รื้อทุกอย่างที่ไม่จำเป็นอย่างยิ่งต่อการส่งมอบฟีเจอร์ที่บริษัทหรือโปรเจกต์โอเพนซอร์สใช้โฆษณาและขายออกไป แต่แบบนั้นอันตรายมาก
โดยแก่นแล้วมันคือปัญหา รั้วของเชสเตอร์ตัน ดังนั้นถ้ายังไม่เข้าใจอย่างถ่องแท้ว่าทำไมบางอย่างถึงถูกใส่เข้ามาในซอฟต์แวร์ และปัจจุบันซอฟต์แวร์ถูกใช้อย่างไร ก็ไม่ควรเอาออก
กรณีแย่ที่สุดคือปล่อยรีลีสไปสักเดือนแล้วผู้ใช้ค่อยมาพบว่าฟีเจอร์สำคัญบางอย่างพังแบบจับสังเกตได้ยาก และคุณต้องเสียเวลาเป็นวัน ๆ ไล่หาว่ามันพังอย่างไรกันแน่
การเพิ่ม CI, linter, fuzzing, auto-formatting ฯลฯ เป็นความคิดที่ดี แต่ก็ทำได้ยากเหมือนกัน
ถ้าคนหนึ่งใช้ VIM เป็นหลัก อีกคนใช้ emacs อีกคนใช้ QTCreator และอีกคนใช้ VSCode ก็ยากมากที่จะทำให้ทุกคนไปในทิศทางเดียวกัน
ถ้าเป็นขั้นตอนเสริมที่ต้องติดตั้งเครื่องมือใหม่ มันก็มักจะไม่เกิดขึ้นจริง และ linter ก็ไม่ช่วยอะไรถ้าเปิดโปรเจกต์มาปุ๊บเจอคำเตือนเกิน 2000 รายการทันที
cpplintหรือclang-tidyหรือ fuzzer ที่ผสานอยู่ใน IDE สำหรับสภาพแวดล้อมพัฒนา C++ และมันก็ช้าเกินกว่าจะรันอัตโนมัติทุกครั้งที่กดแป้นพิมพ์มีแค่ auto-formatting ที่บางครั้งค่อยมีการผสานเข้าไป
ของพวกนี้รันจากบรรทัดคำสั่งได้โดยไม่ขึ้นกับสภาพแวดล้อมพัฒนาของแต่ละคน เพราะงั้นอย่าเพิ่งถอดใจตั้งแต่ยังไม่เริ่มเพียงเพราะมีคนใช้ text editor อยู่สองตัว อย่างน้อยก็ควรลองทำดู
ถ้าในโลก C++ ทีมไม่ยอมติดตั้งเครื่องมืออะไรเลย ก็เตรียมตัวลำบากได้เลย และก็ควรพิจารณาวิธีทำให้มันง่ายขึ้นด้วยการทำเครื่องมือเหล่านี้ให้อยู่ในคอนเทนเนอร์
เราควรทำงานกันแบบมืออาชีพ และถ้างานนั้นต้องเริ่มจากมี IDE และ toolchain ที่เหมาะสม ก็ต้องเรียนรู้แล้วใช้งานมัน
ถ้ามันคอมไพล์และทำงานได้แค่บนคอมของผม ใน IDE ของผม และในวิธีที่ผมชอบ นั่นใกล้เคียงกับงานหัตถกรรมมากกว่าซอฟต์แวร์
ต่อให้การติดตั้ง local hook จะเป็นทางเลือก ถ้าคุณทำให้ PR ตกใน CI คนก็ต้องทำอยู่ดี
เรื่องพวกนี้พื้นฐานมากจริง ๆ แต่ดูเหมือนช่องว่างด้านความรู้เรื่องการจัดการ CI และอินฟรามีอยู่มากพอสมควร
ควรเปิดให้รัน test, lint, fuzzing, formatting, การตรวจรูปแบบ YAML, การตรวจว่ามีไฟล์ใดขาด newline ตอน EOF ฯลฯ ได้จากในเครื่อง เพื่อช่วยให้นักพัฒนาป้องกันไม่ให้ CI ล้มก่อนจะ push
ถ้า linter ปล่อยคำเตือนเป็นพันตอนเปิดโปรเจกต์ นักพัฒนาที่เพิ่ม linter เข้ามาก็ควรทำให้คำเตือนเหลือ 0 ก่อนที่การเปลี่ยนแปลงนั้นจะถูกรวมเข้าไป
จะปิดบางคำเตือนหรือบางไฟล์ไว้ก่อน แก้บางส่วน หรือผสมหลายวิธีเข้าด้วยกันก็ได้เพื่อให้บรรลุจุดนั้น
ผมคิดว่าขั้นตอนแรกคือไปติดต่อผู้ดูแลคนก่อน แวะไปหา ซื้อชาหรือเบียร์ให้สักแก้ว แล้วสุดท้ายก็คุยกันเรื่องโค้ดเบส
เหล่า อดีตผู้ดูแล ที่เป็นเหมือนพ่อมดพวกนั้นบอกอะไรได้เยอะมาก
ข้อเสนออื่น ๆ ที่เหลือ เช่น ทำให้มันรันได้หลายแพลตฟอร์ม หรือทำให้เทสต์ผ่านทั้งหมด ผมคิดว่าเป็น stress test ที่ดีซึ่งนำไปสู่ความแข็งแรงและความเข้าใจ
ถึงอย่างนั้นผมก็คงไปเก็บผลไม้ที่อยู่ต่ำก่อน ด้วยการคุยกับคนที่เคยผ่านมันมาก่อน
ถ้าลงมือเองก่อนสักหน่อย ไปติดอยู่หลายจุด แล้วค่อยคุยกับผู้ดูแลคนก่อน มันจะได้ประสิทธิผลกว่ามาก
พวกเขาเองก็น่าจะซาบซึ้งกับความพยายามนั้นด้วย
ถ้าคุณรับช่วงโค้ดเบสที่ไม่มีเทสต์ บิลด์พังทุก ๆ สองครั้ง ข้อมูล dependency ก็ไม่ชัดเจน และยังคอมไพล์ได้แค่บนเซิร์ฟเวอร์เครื่องเดียวที่ลง OS เก่าแบบหนักมาก ก็อาจไม่แน่ใจนักว่าผู้ดูแลคนก่อนเป็นพ่อมดตัวจริงหรือเปล่า
ต้องแยกให้ออกด้วยว่าปัญหาทั้งหมดเกิดจากเวลาไม่พอ หรือเพราะเป็น “พ่อมด” ที่จงใจปล่อยให้มันพังเพื่อรักษาความมั่นคงในงาน หรือแค่ไม่อยากเรียนรู้ของใหม่
นั่นยังเป็นกรณีที่ดีที่สุด ส่วนกรณีที่พบได้บ่อยคือทุกคำถามจะได้คำตอบแค่ว่า “ตอนฉันรับมาก็มันเป็นแบบนี้อยู่แล้ว” เท่านั้น
ระหว่างทำงานผมเจอบั๊กและปัญหาหลายอย่าง เลยพยายามติดต่อเหล่านักพัฒนาที่เขียนซอฟต์แวร์ซึ่งทำขึ้นเฉพาะให้กับนายจ้างของผม
ปรากฏว่ามีผู้รับเหมาคนเดียวเป็นคนเขียน และเขาเสียชีวิตไปเมื่อหลายปีก่อนแล้ว
ในอุตสาหกรรมป้องกันประเทศเรื่องแบบนี้เกิดขึ้นบ่อย มีของเฉพาะทางที่ทำครั้งเดียวจบสำหรับระบบใดระบบหนึ่งอยู่มาก และโดยเฉพาะฝั่งฮาร์ดแวร์ก็ไม่ใช่เรื่องแปลกที่วิศวกรผู้สร้างอุปกรณ์จะลาออกหรือเกษียณไปนานแล้ว
มันช่วยได้มาก
จะบอกว่าโค้ดเบส C++ มีเทสต์อยู่ด้วยนี่ ผมว่ามองโลกในแง่ดีไปหน่อย
ไม่ค่อยเข้าใจว่าทำไมถึงโฟกัสกับการรีแฟกเตอร์หรือปรับปรุงกันมากขนาดนี้ ถ้าสามารถเพิ่มฟีเจอร์เข้าไปในโค้ดได้แบบเสริมทับ ก็ทำแบบนั้นโดยไม่ไปแตะส่วนอื่นก็พอ
ถ้าเป็นการเปลี่ยนแปลงที่ใหญ่พอ ก็แยกสิ่งที่ต้องใช้จากโค้ด legacy ออกมาด้วยการเรียกฟังก์ชันภายนอก เพิ่มชั้น network หรือแยกโค้ดเดียวกันออกเป็นไลบรารี แล้วที่เหลือค่อยทำใน สภาพแวดล้อมใหม่
ถ้าไม่ใช่กรณีที่จากนี้จะมีหลายคนต้องมาทำงานกับโค้ดนั้น และทีมจำเป็นต้องมีสมมติฐานหรือมาตรฐานบางอย่างเพื่อให้ร่วมงานกันได้ง่าย ก็คงไม่พยายามทำการรีแฟกเตอร์ครั้งใหญ่
วิธีแบบค่อย ๆ แปะเพิ่มนั้นใช้ได้ถึงจุดหนึ่ง แต่สุดท้ายจะมีช่วงที่พอเปลี่ยนอะไรแล้วจู่ ๆ ก็มีบางอย่างพัง และเพราะโค้ดรกมากจึงใช้เวลานานเกินจำเป็นกว่าจะหาสาเหตุเจอ
ใจความของโพสต์ต้นฉบับใกล้เคียงกับการบอกว่าให้หลีกเลี่ยงการเขียนใหม่ครั้งใหญ่ แต่ควร จัดระเบียบ เพื่อคงความเป็นเอกภาพของโค้ดและทำให้เปลี่ยนแปลงได้ง่าย
บางครั้งก็ต้องเพิ่มฟีเจอร์เดียวกันให้ทั้งสองเวอร์ชัน
วิธีรับมือกับโค้ดที่รู้ว่าใกล้ออกรุ่นสุดท้ายแล้ว กับโค้ดที่จะต้องดูแลและเพิ่มฟีเจอร์ต่อไปอีกหลายสิบปีนั้นต่างกันมาก
แต่ในความเป็นจริง ฟีเจอร์ใหม่มักเป็นการเปลี่ยนพฤติกรรมที่มีอยู่แล้ว และอยู่ ๆ ก็ต้องทำรีแฟกเตอร์หนัก ๆ หลายจุด
เป็นเธรดที่มีคำแนะนำดี ๆ เยอะ ถ้าจะเสริมนอกเหนือจาก C/C++ อย่างหนึ่งคือ ถ้ามีทรัพยากรพอจะใช้ ระบบควบคุมเวอร์ชัน ก็ควรดึงคุณค่าจากมันให้เต็มที่
หลายทีมใช้มันเป็นแค่เครื่องมือทำงานร่วมกัน แต่จริง ๆ ทำได้มากกว่านั้น
แค่นำประวัติมาสร้างฐานข้อมูลแบบง่าย ๆ ก็ได้ ไม่จำเป็นต้องเป็น RDB จะเริ่มจากไฟล์ JSON หรือสเปรดชีตก็ได้
แค่ใช้แนวทางที่ขับเคลื่อนด้วยข้อมูลก็สามารถดึงข้อมูลที่มีประโยชน์ได้มากแทบจะทันที
ไฟล์และฟังก์ชันที่ถูกเปลี่ยนบ่อยมีแนวโน้มสูงว่าจะเป็น hotspot ของงานในอนาคต ดังนั้นถ้าจะเพิ่ม unit test หรือลด merge conflict ก็ควรโฟกัสตรงนั้น
ถ้าไฟล์ที่ดูเหมือนห่างกันมากถูกเปลี่ยนพร้อมกันบ่อย ๆ ก็อาจบ่งชี้ถึงโครงสร้างแฝงที่ดูจากโค้ดอย่างเดียวไม่เห็น
โมเดล ownership ที่แท้จริงของแต่ละโมดูลก็อนุมานได้จากประวัติ และ ownership ที่ไม่ชัดเจนอาจเป็นสัญญาณว่าจำเป็นต้องรีแฟกเตอร์
ใน C/C++ การปรับปรุงเวลา build ก็ควรใช้ข้อมูลช่วยโฟกัสที่โมดูลสำคัญ แทนที่จะสุ่มลบ dependency ของไฟล์ต่าง ๆ ก็ควรแยกโมดูลที่เปลี่ยนบ่อย และอาจให้คะแนนผลกระทบต่อเวลา build จริงโดยรวมข้อมูลกับ header dependency
ถ้าผสาน VCS เข้ากับเครื่องมือพัฒนาอื่น ๆ ก็ทำอะไรได้มากขึ้น และในยุค LLM ก็ดูเหมือนว่าจะสามารถป้อนประวัติโปรเจกต์กับ metadata ให้โมเดลแล้วถามหาข้อมูลเชิงลึกที่น่าสนใจได้
ถ้าจะทำโดยไม่มี context window ขนาดใหญ่อาจต้องมีงานวิศวกรรมโมเดลเฉพาะทาง แต่ก็รู้สึกว่าน่าลอง
คำแนะนำให้เพิ่ม CI, linter, fuzzing, auto-formatting ฯลฯ ควรแยกดูเป็นเรื่อง ๆ
CI ควรช่วยให้มั่นใจว่าสามารถ build ได้ไม่ใช่แค่บนเครื่องของฉัน แต่รวมถึงที่อื่นด้วย เพื่อป้องกัน regression ด้านการคอมไพล์
compiler warning และ static analyzer โดยทั่วไปฉลาดกว่าฉัน ดังนั้นถ้ามันเตือนว่ามีการทำอะไรแปลก ๆ กับ pointer จนน่ากลัว นั่นคือสัญญาณแรง ๆ ว่าควรตรวจสอบ
unit test ควรตรวจว่าโค้ดส่วนสำคัญทำงานในระดับล่างได้ตรงตามที่คาดจริงหรือไม่ และมีโอกาสสูงที่จะไม่เป็นเช่นนั้น จึงต้องเข้าใจเหตุผล
พอแก้อะไรอย่างหนึ่งแล้วอย่างอื่นอาจพังได้ เพราะโค้ดเดิมอาจถูกเขียนโดยอิงกับพฤติกรรมที่มีบั๊กอยู่แล้ว
การจัดรูปแบบอัตโนมัติไม่ใช่เรื่องที่มาก่อน และมองว่าควรตามสไตล์ของผู้ดูแลเดิมมากกว่า
แนวคิดที่ว่าขั้นสุดท้ายของโค้ดเบส C++ ที่รับช่วงมาคือการเขียนใหม่ด้วยภาษา memory-safe ก็มักจะไม่ค่อยเข้ากัน
เพราะยากที่จะได้ทรัพยากรเพิ่มเพื่อไปทำงานกับสิ่งที่ยังไม่พัง และนอกจาก C++ แล้วยังต้องมีความรู้ภาษาเพิ่มเติม ทำให้การทดสอบซับซ้อนขึ้นได้
อีกทั้งด้วยข้อจำกัดด้านหน่วยความจำหรือประสิทธิภาพ ก็มีโอกาสสูงที่จะไม่เหมาะกับการเขียนหลายภาษา และตั้งแต่แรกที่ต้องรับช่วงโค้ดเบส legacy มาก็แทบเท่ากับยอมรับอยู่แล้วว่าทรัพยากรอย่างเวลา เงิน หรือความรู้นั้นมีไม่พอจะเขียนใหม่
การ “เขียนใหม่ด้วย X” แค่เพิ่มความซับซ้อนเพราะเป็นกระแส
ถ้ากำลังเขียนส่วนใหญ่ของโค้ดเบสใหม่เป็น C++ อยู่แล้ว ก็ควรใช้ C++ แบบ subset ที่จำกัดมากขึ้น ซึ่งมองว่า High Integrity C++ ก็ค่อนข้างดี
ถ้าหามาตรฐาน MISRA รุ่นล่าสุดได้ ก็น่าจะดีเช่นกัน
แทนที่จะให้ทั้งทีมต้องมาเรียนรู้มุมอันตรายของภาษาใหม่อีกครั้ง ควรใช้ภาษาที่ทุกคนรู้อยู่แล้ว แต่บังคับใช้แนวทางที่ช่วยหลีกเลี่ยงกับดักที่รู้กันอยู่
หลังจากที่ผู้เขียนวิจารณ์เรื่อง automation ของ BOM, การจัดการเวอร์ชันแพ็กเกจ, แหล่งที่มาของ dependency ฯลฯ ไปมากแล้ว การเสนอว่า git submodules ดีกว่า package manager ก็ดูแปลก
ก่อนจะวิจารณ์แบบนี้ควรลองใช้ vcpkg ก่อน
มันมีมุมคม ๆ อยู่บ้าง แต่แทบทั้งหมดตอบโจทย์ได้อย่างเป็นธรรมชาติด้วย vcpkg
การอัปเดต dependency ยากกว่า git submodules เล็กน้อย แต่ผมมองว่านี่ใกล้เคียงกับฟีเจอร์มากกว่าจะเป็นบั๊ก โดย dependency จะถูก build ใน sandbox แยกแล้วติดตั้งไปยังไดเรกทอรีที่กำหนด
vcpkg สามารถตั้ง internal repository แทน official repository เป็น registry ได้ เพื่อคงลักษณะแบบ vendored-in เอาไว้ และยัง chain-load toolchain เพื่อให้คอมไพล์ทุกอย่างด้วยชุดแฟล็กที่ตรึงไว้ รวมถึงอนุญาตให้ปรับแต่งเป็นรายพอร์ตได้
ก็เพราะ abstraction แบบนี้มีประโยชน์ package manager จึงได้รับความนิยม และไม่จำเป็นที่ทุกคนจะต้องมาจัดการสตริงยาวไม่รู้จบที่เต็มไปด้วย compiler flag, macro, warning ฯลฯ ด้วยตัวเอง
บทความอ่านสนุกและได้เรียนรู้อะไรด้วย แต่อยากรู้ว่าตอนที่คนพูดว่า “ภาษาแบบ memory-safe” เขาหมายถึงภาษาไหน
หมายถึงให้เขียนบางส่วนใหม่ด้วย Go, Java, C# หรือเป็นการเหน็บแนมแบบคลุมเครือว่าให้เขียนใหม่ด้วย Rust กันแน่
ข้อสรุปคือขึ้นอยู่กับทีมและข้อจำกัดทั้งหมด เช่น ใช้ garbage collection ได้หรือไม่ ถ้าได้ Go จะเป็นตัวเลือกที่ดีไหม หรือความปลอดภัยเป็นสิ่งสำคัญสูงสุดหรือเปล่า
นักพัฒนา C++ ส่วนใหญ่มักจะใช้ Rust ได้ไม่ยากและได้ประสิทธิภาพในระดับเทียบเท่ากัน
แต่ก็มีกรณีที่ตั้งแต่แรกโปรเจ็กต์ไม่ได้มีเหตุผลที่ดีพอว่าทำไมต้องเป็น C++ และผมก็เคยเห็นเคสที่เขียนใหม่ด้วย Java แล้วสำเร็จ
Apple ก็กำลังเขียนโค้ด C++ บางส่วนใหม่ด้วย Swift
ภาษาอะไรก็ตามที่ทีมหรือบริษัทใช้งานได้อย่างสบายใจ มักเป็นกฎคร่าว ๆ ที่ดี
ตอนนี้เหมือนจะมาถึงขั้นสงสัยแล้วว่าผู้เขียนเป็นโปรแกรมเมอร์ Rust สายมืดที่แฝงตัวมา เพราะคงบ่นใส่ Rust ตรง ๆ ไม่ได้ตั้งแต่แรกเนื่องจากบทความไม่ได้พูดถึง Rust เลย