1 คะแนน โดย GN⁺ 2024-03-01 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หากคุณต้องรับผิดชอบ โค้ดเบส 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 dependency จาก third-party ทั้งหมด ทั้งที่จริงใช้เพียงส่วนเล็ก ๆ หรือไม่
      • mbedtls มี compile-time flag จำนวนมากสำหรับปิดส่วนที่ไม่จำเป็น
    • ตรวจสอบว่า target dependency ผิดพลาดจนการเปลี่ยนแปลงเล็ก ๆ ทำให้ต้อง build ใหม่ทั้งหมดหรือไม่
      • build system จำนวนมากสามารถ output dependency graph ได้
    • ทดลอง linker ที่เร็วอย่าง mold
    • หากเป็นไปได้ ให้เปรียบเทียบ compiler ด้วย
      • บางโปรเจกต์พบว่า clang เร็วกว่า gcc 2 เท่า ขณะที่บางโปรเจกต์ไม่มีความต่าง
  • รายการที่ทดลองเพิ่มได้ แต่ผลประโยชน์อาจน้อยหรือเป็นลบ
    • ปิด/เปิด/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 นัก
  • ใช้รายการแพลตฟอร์มที่รองรับเพื่อลบ โค้ดสำหรับแพลตฟอร์มที่ไม่รองรับ
    • โค้ดรองรับ 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-format format ทั้งโค้ดเบสโดยไม่มีข้อยกเว้น
    • 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 ตัวเดียว การเขียนเองอาจดีกว่า

การเขียนใหม่ด้วยภาษาที่ปลอดภัยด้านหน่วยความจำต้องพิจารณาแยกต่างหาก

  • ตัวเลือกในการเขียนบางส่วนใหม่ด้วยภาษาที่ปลอดภัยด้านหน่วยความจำนั้นเป็นไปได้ แต่มีข้อควรระวังมาก
  • ควรดำเนินการเฉพาะเมื่อมีเหตุผลที่หนักแน่นเท่านั้น

การจัดการ 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 อาจกลายเป็นปัญหา
    • ยากที่จะดู 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
    • อาจไม่รองรับ 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 ความคิดเห็น

 
GN⁺ 2024-03-01
ความเห็นจาก Hacker News
  • มีทั้งคำแนะนำที่ดีและคำแนะนำที่ค่อนข้างชวนถกเถียงอยู่บ้าง จากการเคยรับช่วงต่อ โปรเจ็กต์ C++ ขนาดใหญ่มาหลายตัว งานที่ให้ผลคุ้มมากอย่างชัดเจนในช่วงแรกดูจะมีอยู่ไม่กี่อย่าง
    ควรเริ่มจากทำ build ที่ทำซ้ำได้ แล้วห่อสภาพแวดล้อมการ build ด้วย Docker หรือเครื่องมือแพ็กเกจจิงที่คุณถนัด เพื่อให้เครื่องมือและ dependency ถูกระบุอย่างชัดเจนและทำซ้ำได้
    ถ้าทำให้ build ผ่านแบบไม่มี warning ด้วย -Wall ได้ ก็จะช่วยเผยโค้ดที่แย่, พฤติกรรมที่ไม่กำหนดไว้, และบั๊กต่าง ๆ ออกมา และหลังจากนั้นเมื่อฉันใส่โค้ดที่น่าสงสัยเข้าไป ก็จะเห็น warning ได้ทันที
    การใช้เครื่องมืออย่าง valgrind ตั้งแต่ต้นเพื่อตรวจข้อผิดพลาดการอ่าน/เขียนก็เป็นผลลัพธ์ที่ได้ง่ายในแง่ความเสถียรเช่นกัน และก่อนจะเข้าใจโครงสร้างทั้งหมดดี การจำกัดการรีแฟกเตอร์ไว้เฉพาะจุดย่อย ๆ จะปลอดภัยกว่า

    • ถ้าคุณต้องการความชัดเจนและความทำซ้ำได้จริง ๆ ก็ควรหลีกเลี่ยงการเริ่มจาก Docker ก่อน ถ้าไม่ใส่ใจมากพอ Docker จะให้เพียงความทำซ้ำได้ในรูปแบบที่เจือจางที่สุด และอาจทำให้เข้าใจผิดว่าปลอดภัยแล้ว
      การอ้างอิง image tag ที่เปลี่ยนได้โดยไม่มี integrity hash หรือ Dockerfile ที่เรียก apt-get เป็นเรื่องที่พบได้บ่อย และทั้งสองอย่างนี้เปิดช่องกว้างให้ผลลัพธ์จบลงในสถานะที่ต่างกันแบบละเอียดอ่อน
      แม้จะเรียนรู้ยากกว่า แต่แนวทางอย่าง Bazel หรือ Nix ให้รากฐานที่ดีกว่ามาก
    • ขั้นที่ 0 คือ build ที่ทำซ้ำได้ และขั้นที่ 1 คือรันทุกเทสต์แล้วติดป้าย เทสต์ที่ไม่นิ่ง
      ขั้นที่ 2 คือรันทุกเทสต์ภายใต้ sanitizer แล้วติดป้ายตัวที่ล้มเหลว ขั้นที่ 3 คือแก้ sanitizer failure ทั้งหมดนั้น และขั้นที่ 4 คืองานที่เหลือ
    • Docker ใช้ได้ดีกับไมโครเซอร์วิส แต่ก็ไม่ได้หมายความว่าจะ deploy ทุกอย่างด้วย Docker ได้เสมอ โดยเฉพาะกับคนที่ต้องใช้แอปจากภายใน Docker และ Docker-in-Docker คือสถานการณ์ที่ไม่ควรมีอยู่
      คอนเทนเนอร์นั้นดี แต่ถ้าใช้เป็นวิธีทำเป็นว่าปัญหาไม่มีอยู่ก็น่ากลัวมาก
      คุณควรแพ็ก dependency ให้ครบ และหลีกเลี่ยงสภาพที่ทุกอย่างจะทำงานได้ก็ต่อเมื่อมีของเป็นพันล้านชิ้นติดตั้งไว้ใน /usr/lib ด้วยเวอร์ชันที่ถูกต้องพอดี
    • การ build ให้สะอาดด้วย -Wall นั้นโอเค แต่ผมคัดค้านอย่างหนักกับการใส่ -Wall -Werror ใน production build
      compiler warning บางอย่างมีลักษณะเป็นเรื่องของความเห็นมากกว่า และถ้า compiler เวอร์ชันใหม่เพิ่ม warning ใหม่ โค้ดที่เคยสะอาดก็อาจถูกปฏิเสธขึ้นมาทันที
      ถ้าจำเป็นต้องใช้ -Werror ก็ควรใช้เฉพาะใน debug build จะดีกว่า
  • ผมจะสลับลำดับของข้อ 2 กับข้อ 3 โดยให้ CI, linting, การจัดรูปแบบอัตโนมัติ และสิ่งทำนองนั้นมาก่อนการรื้ออะไรออกไป
    เพราะตอนนี้ยังไม่รู้ว่าควรลบอะไร และถ้าลบแล้วผลจะเป็นอย่างไร
    linter และเครื่องมือวิเคราะห์แบบสถิติมอบข้อมูลเชิงลึกมากมายว่าควรลงมือแก้ตรงส่วนไหนของโปรแกรม
    จุดที่เครื่องมือวิเคราะห์แบบสถิติชี้วันนี้ มักเป็นจุดที่ภายหลังคุณอาจลบฟังก์ชัน, คลาส, หรือไฟล์ทั้งก้อนได้ เพราะมันเป็นการ reimplement แนวคิดใน STL
    คุณอาจแทนที่ไลบรารี iterator ที่ทำเอง, smart pointer ที่ทำเอง, หรือการใช้ฟังก์ชันสตริงแบบ C ด้วย STL algorithm, smart pointer จริง ๆ, และคลาสสตริงของ C++ ได้
    แต่ก่อนสแกนโค้ดก็มักมองไม่เห็นสิ่งเหล่านี้ชัดนัก และก่อนจะผลักไปทาง build ทดสอบที่เร็วหรือการ deploy ก็ประเมินผลลัพธ์ได้ยาก

    • ผมทำงานเอเจนซีและเคยเข้าไปกู้โค้ดเบสมาหลายครั้ง สิ่งแรกที่ทำคือรัน unit test และดู coverage
      ตรงไหนที่ขาดก็เพิ่ม smoke test พื้นฐานเข้าไป
      งานนี้ไม่ได้ทำให้ช้าลง แต่กลับทำให้เร็วขึ้นด้วยซ้ำ เพราะเมื่อมี coverage ที่พอเหมาะแล้ว คุณจะรีแฟกเตอร์ได้เร็วขึ้นมาก ถือเป็นการลงทุนเล็กน้อยที่คุ้มมาก
    • ในทางกลับกัน การจัดรูปแบบอัตโนมัติ ทำลายประวัติเวอร์ชัน และทำให้วิเคราะห์ได้ยากว่าบรรทัดนี้ถูกเพิ่มเข้ามาเมื่อไรและเพราะอะไร
    • ผมไม่แนะนำอย่างยิ่งให้ใช้การจัดรูปแบบอัตโนมัติกับโค้ดเบสแบบเลกาซี
      โปรเจ็กต์ C++ ขนาดใหญ่มักไม่ได้มีแค่สคริปต์สร้างโค้ดเท่านั้น แต่ยังมีสคริปต์ที่ parse โค้ดเพื่อรวบรวมข้อมูลสำหรับการสร้างโค้ดด้วย
      การจัดรูปแบบอัตโนมัติอาจทำให้สิ่งเหล่านี้พังได้ และผมยังเคยเห็นโปรเจ็กต์อาถรรพ์ที่ผู้ใช้ parse ไฟล์ public header ด้วยสคริปต์เปราะบางอีกด้วย
    • สำหรับ CI นั้นต่างจากข้ออื่น ขั้นแรกอย่างน้อยที่สุดคือต้องมี test harness บนเส้นทางหลักที่รันด้วยผลลัพธ์ที่ทำซ้ำได้และทำงานกับทุก commit ซึ่งยังช่วยให้เข้าใจโค้ดเบสด้วย
      มีโอกาสสูงที่คุณจะต้องไปแตะหลายจุด และเมื่อมีการเปลี่ยนแปลงสะสมอยู่ในเครื่องมากขึ้น ก็อาจเผลอสร้าง commit ที่ปนหลายประเด็นเข้าด้วยกัน
      ตอนนั้น CI จะเป็นผู้กอบกู้
    • ในรายละเอียด สิ่งที่ต้นฉบับบอกให้รื้อออกนั้นจำกัดอยู่ที่ dead code ซึ่งถูกชี้นำโดย compiler warning และสถาปัตยกรรมที่ไม่รองรับ
      ถ้าจะไปทางนั้น อย่างน้อยผมแนะนำว่าอย่าเพิ่งลบทันที แต่ให้คอมเมนต์ทิ้งไว้ก่อนจนกว่าคุณจะพร้อม 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...

    • ไม่น่าเชื่อเลยว่าจะได้เจอคนที่เคยใช้ Source Navigator จริง ๆ ในโลกภายนอก
      ตอนเรียนวิชาสถาปัตยกรรมคอมพิวเตอร์ในมหาวิทยาลัย เราถูกบังคับให้ใช้โบราณวัตถุชิ้นนี้ เพราะมันมีชุดฟีเจอร์ประหลาดที่ที่อื่นไม่มี รวมถึงการรองรับ ARM emulator
      พวกเราใช้มันกับการเขียนโปรแกรม bare-metal ARM assembly
  • ตอนที่บอกว่าใส่ std::cmake เข้าไปในไลบรารีมาตรฐานจะเป็นตัวเปลี่ยนเกมนี่ทำเอารู้สึกอิหลักอิเหลื่อมาก
    ผมเข้าใจคำแนะนำที่ว่าให้ใช้เลื่อยยนต์รื้อทุกอย่างที่ไม่จำเป็นอย่างยิ่งต่อการส่งมอบฟีเจอร์ที่บริษัทหรือโปรเจกต์โอเพนซอร์สใช้โฆษณาและขายออกไป แต่แบบนั้นอันตรายมาก
    โดยแก่นแล้วมันคือปัญหา รั้วของเชสเตอร์ตัน ดังนั้นถ้ายังไม่เข้าใจอย่างถ่องแท้ว่าทำไมบางอย่างถึงถูกใส่เข้ามาในซอฟต์แวร์ และปัจจุบันซอฟต์แวร์ถูกใช้อย่างไร ก็ไม่ควรเอาออก
    กรณีแย่ที่สุดคือปล่อยรีลีสไปสักเดือนแล้วผู้ใช้ค่อยมาพบว่าฟีเจอร์สำคัญบางอย่างพังแบบจับสังเกตได้ยาก และคุณต้องเสียเวลาเป็นวัน ๆ ไล่หาว่ามันพังอย่างไรกันแน่
    การเพิ่ม CI, linter, fuzzing, auto-formatting ฯลฯ เป็นความคิดที่ดี แต่ก็ทำได้ยากเหมือนกัน
    ถ้าคนหนึ่งใช้ VIM เป็นหลัก อีกคนใช้ emacs อีกคนใช้ QTCreator และอีกคนใช้ VSCode ก็ยากมากที่จะทำให้ทุกคนไปในทิศทางเดียวกัน
    ถ้าเป็นขั้นตอนเสริมที่ต้องติดตั้งเครื่องมือใหม่ มันก็มักจะไม่เกิดขึ้นจริง และ linter ก็ไม่ช่วยอะไรถ้าเปิดโปรเจกต์มาปุ๊บเจอคำเตือนเกิน 2000 รายการทันที

    • สิ่งที่ผู้เขียนไล่มาส่วนใหญ่ไม่ใช่ การผสานเข้ากับ IDE ตามปกติ ผมไม่เคยเห็น cpplint หรือ clang-tidy หรือ fuzzer ที่ผสานอยู่ใน IDE สำหรับสภาพแวดล้อมพัฒนา C++ และมันก็ช้าเกินกว่าจะรันอัตโนมัติทุกครั้งที่กดแป้นพิมพ์
      มีแค่ auto-formatting ที่บางครั้งค่อยมีการผสานเข้าไป
      ของพวกนี้รันจากบรรทัดคำสั่งได้โดยไม่ขึ้นกับสภาพแวดล้อมพัฒนาของแต่ละคน เพราะงั้นอย่าเพิ่งถอดใจตั้งแต่ยังไม่เริ่มเพียงเพราะมีคนใช้ text editor อยู่สองตัว อย่างน้อยก็ควรลองทำดู
      ถ้าในโลก C++ ทีมไม่ยอมติดตั้งเครื่องมืออะไรเลย ก็เตรียมตัวลำบากได้เลย และก็ควรพิจารณาวิธีทำให้มันง่ายขึ้นด้วยการทำเครื่องมือเหล่านี้ให้อยู่ในคอนเทนเนอร์
    • ผมคิดว่าเหตุผลที่ว่าคนหนึ่งใช้ VIM อีกคนใช้ emacs, QTCreator, VSCode นั้นไม่ใช่คำตอบที่พอรับได้อีกต่อไป
      เราควรทำงานกันแบบมืออาชีพ และถ้างานนั้นต้องเริ่มจากมี IDE และ toolchain ที่เหมาะสม ก็ต้องเรียนรู้แล้วใช้งานมัน
      ถ้ามันคอมไพล์และทำงานได้แค่บนคอมของผม ใน IDE ของผม และในวิธีที่ผมชอบ นั่นใกล้เคียงกับงานหัตถกรรมมากกว่าซอฟต์แวร์
    • คุณทำ auto-formatting นอก editor ก็ได้ แค่ตั้งค่า pre-commit แล้วรันใน CI เป็นเรื่องเล็กน้อยมาก
      ต่อให้การติดตั้ง local hook จะเป็นทางเลือก ถ้าคุณทำให้ PR ตกใน CI คนก็ต้องทำอยู่ดี
      เรื่องพวกนี้พื้นฐานมากจริง ๆ แต่ดูเหมือนช่องว่างด้านความรู้เรื่องการจัดการ CI และอินฟรามีอยู่มากพอสมควร
    • ดูเหมือนคุณจะพลาดบันทึกที่บอกว่าคุณไม่สามารถปฏิเสธข้อกำหนดพื้นฐานที่หัวหน้าต้องการได้ง่าย ๆ หมายถึงคนที่จ่ายเงินเดือนคุณนั่นแหละ
    • pre-commit hook ที่เป็นทางเลือกในเครื่องโลคัล ควรมีขั้นตอนบังคับใน CI คอยหนุนหลัง
      ควรเปิดให้รัน test, lint, fuzzing, formatting, การตรวจรูปแบบ YAML, การตรวจว่ามีไฟล์ใดขาด newline ตอน EOF ฯลฯ ได้จากในเครื่อง เพื่อช่วยให้นักพัฒนาป้องกันไม่ให้ CI ล้มก่อนจะ push
      ถ้า linter ปล่อยคำเตือนเป็นพันตอนเปิดโปรเจกต์ นักพัฒนาที่เพิ่ม linter เข้ามาก็ควรทำให้คำเตือนเหลือ 0 ก่อนที่การเปลี่ยนแปลงนั้นจะถูกรวมเข้าไป
      จะปิดบางคำเตือนหรือบางไฟล์ไว้ก่อน แก้บางส่วน หรือผสมหลายวิธีเข้าด้วยกันก็ได้เพื่อให้บรรลุจุดนั้น
  • ผมคิดว่าขั้นตอนแรกคือไปติดต่อผู้ดูแลคนก่อน แวะไปหา ซื้อชาหรือเบียร์ให้สักแก้ว แล้วสุดท้ายก็คุยกันเรื่องโค้ดเบส
    เหล่า อดีตผู้ดูแล ที่เป็นเหมือนพ่อมดพวกนั้นบอกอะไรได้เยอะมาก
    ข้อเสนออื่น ๆ ที่เหลือ เช่น ทำให้มันรันได้หลายแพลตฟอร์ม หรือทำให้เทสต์ผ่านทั้งหมด ผมคิดว่าเป็น stress test ที่ดีซึ่งนำไปสู่ความแข็งแรงและความเข้าใจ
    ถึงอย่างนั้นผมก็คงไปเก็บผลไม้ที่อยู่ต่ำก่อน ด้วยการคุยกับคนที่เคยผ่านมันมาก่อน

    • ผมคงไม่ทำสิ่งนั้นเป็นขั้นตอนแรก เพราะมีโอกาสสูงที่จะไปเสียเวลาของพวกเขา
      ถ้าลงมือเองก่อนสักหน่อย ไปติดอยู่หลายจุด แล้วค่อยคุยกับผู้ดูแลคนก่อน มันจะได้ประสิทธิผลกว่ามาก
      พวกเขาเองก็น่าจะซาบซึ้งกับความพยายามนั้นด้วย
    • อาจจะดีกว่าถ้าไล่ดูโค้ดเบสอย่างรวดเร็วก่อน หา WTF ที่ใหญ่ที่สุดให้เจอ แล้วค่อยตั้งคำถาม
      ถ้าคุณรับช่วงโค้ดเบสที่ไม่มีเทสต์ บิลด์พังทุก ๆ สองครั้ง ข้อมูล dependency ก็ไม่ชัดเจน และยังคอมไพล์ได้แค่บนเซิร์ฟเวอร์เครื่องเดียวที่ลง OS เก่าแบบหนักมาก ก็อาจไม่แน่ใจนักว่าผู้ดูแลคนก่อนเป็นพ่อมดตัวจริงหรือเปล่า
      ต้องแยกให้ออกด้วยว่าปัญหาทั้งหมดเกิดจากเวลาไม่พอ หรือเพราะเป็น “พ่อมด” ที่จงใจปล่อยให้มันพังเพื่อรักษาความมั่นคงในงาน หรือแค่ไม่อยากเรียนรู้ของใหม่
    • ถ้าคุณเคยถามผู้ดูแลคนก่อนเกี่ยวกับโค้ด legacy มาก่อนก็น่าจะรู้ว่า ต่อให้ส่งมอบงานกันเมื่อวาน สิ่งที่มีประโยชน์ที่เขาทำไว้เกิน 6 เดือนไปแล้วก็มักอธิบายไม่ได้แทบทั้งนั้น
      นั่นยังเป็นกรณีที่ดีที่สุด ส่วนกรณีที่พบได้บ่อยคือทุกคำถามจะได้คำตอบแค่ว่า “ตอนฉันรับมาก็มันเป็นแบบนี้อยู่แล้ว” เท่านั้น
    • ผมเคยรับผิดชอบงาน deploy ซอฟต์แวร์ลงบน OS ปรับแต่งรุ่นเก่าภายในเครือข่ายปิดสำหรับงานทหาร โปรแกรมไม่ได้ใหญ่มาก ประมาณ 50,000 บรรทัด
      ระหว่างทำงานผมเจอบั๊กและปัญหาหลายอย่าง เลยพยายามติดต่อเหล่านักพัฒนาที่เขียนซอฟต์แวร์ซึ่งทำขึ้นเฉพาะให้กับนายจ้างของผม
      ปรากฏว่ามีผู้รับเหมาคนเดียวเป็นคนเขียน และเขาเสียชีวิตไปเมื่อหลายปีก่อนแล้ว
      ในอุตสาหกรรมป้องกันประเทศเรื่องแบบนี้เกิดขึ้นบ่อย มีของเฉพาะทางที่ทำครั้งเดียวจบสำหรับระบบใดระบบหนึ่งอยู่มาก และโดยเฉพาะฝั่งฮาร์ดแวร์ก็ไม่ใช่เรื่องแปลกที่วิศวกรผู้สร้างอุปกรณ์จะลาออกหรือเกษียณไปนานแล้ว
    • มันไม่ง่ายอย่างที่พูดหรอก ขั้นตอนที่ 0 ของผมคือเอาเข้า เครื่องมือ UML เพื่อดึง class diagram และไดอะแกรมอื่น ๆ ออกมาก่อน
      มันช่วยได้มาก
      จะบอกว่าโค้ดเบส C++ มีเทสต์อยู่ด้วยนี่ ผมว่ามองโลกในแง่ดีไปหน่อย
  • ไม่ค่อยเข้าใจว่าทำไมถึงโฟกัสกับการรีแฟกเตอร์หรือปรับปรุงกันมากขนาดนี้ ถ้าสามารถเพิ่มฟีเจอร์เข้าไปในโค้ดได้แบบเสริมทับ ก็ทำแบบนั้นโดยไม่ไปแตะส่วนอื่นก็พอ
    ถ้าเป็นการเปลี่ยนแปลงที่ใหญ่พอ ก็แยกสิ่งที่ต้องใช้จากโค้ด legacy ออกมาด้วยการเรียกฟังก์ชันภายนอก เพิ่มชั้น network หรือแยกโค้ดเดียวกันออกเป็นไลบรารี แล้วที่เหลือค่อยทำใน สภาพแวดล้อมใหม่
    ถ้าไม่ใช่กรณีที่จากนี้จะมีหลายคนต้องมาทำงานกับโค้ดนั้น และทีมจำเป็นต้องมีสมมติฐานหรือมาตรฐานบางอย่างเพื่อให้ร่วมงานกันได้ง่าย ก็คงไม่พยายามทำการรีแฟกเตอร์ครั้งใหญ่

    • ต้นฉบับกำลังคัดค้านการรีแฟกเตอร์ครั้งใหญ่ สิ่งที่เสนอคือการทำแบบค่อยเป็นค่อยไปเพื่อให้โค้ดจัดการได้ง่ายขึ้นเรื่อย ๆ
      วิธีแบบค่อย ๆ แปะเพิ่มนั้นใช้ได้ถึงจุดหนึ่ง แต่สุดท้ายจะมีช่วงที่พอเปลี่ยนอะไรแล้วจู่ ๆ ก็มีบางอย่างพัง และเพราะโค้ดรกมากจึงใช้เวลานานเกินจำเป็นกว่าจะหาสาเหตุเจอ
      ใจความของโพสต์ต้นฉบับใกล้เคียงกับการบอกว่าให้หลีกเลี่ยงการเขียนใหม่ครั้งใหญ่ แต่ควร จัดระเบียบ เพื่อคงความเป็นเอกภาพของโค้ดและทำให้เปลี่ยนแปลงได้ง่าย
    • คำตอบขึ้นอยู่กับอนาคต เคยทำงานกับโค้ด C++ ที่มีของทดแทนออกสู่ตลาดแล้ว แต่ยังต้องออกรุ่นของโค้ดเก่าอีกหลายครั้ง
      บางครั้งก็ต้องเพิ่มฟีเจอร์เดียวกันให้ทั้งสองเวอร์ชัน
      วิธีรับมือกับโค้ดที่รู้ว่าใกล้ออกรุ่นสุดท้ายแล้ว กับโค้ดที่จะต้องดูแลและเพิ่มฟีเจอร์ต่อไปอีกหลายสิบปีนั้นต่างกันมาก
    • ในบางกรณีที่โชคดี ก็สามารถแค่แปะฟีเจอร์เพิ่มเข้าไปได้
      แต่ในความเป็นจริง ฟีเจอร์ใหม่มักเป็นการเปลี่ยนพฤติกรรมที่มีอยู่แล้ว และอยู่ ๆ ก็ต้องทำรีแฟกเตอร์หนัก ๆ หลายจุด
  • เป็นเธรดที่มีคำแนะนำดี ๆ เยอะ ถ้าจะเสริมนอกเหนือจาก 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 มาก็แทบเท่ากับยอมรับอยู่แล้วว่าทรัพยากรอย่างเวลา เงิน หรือความรู้นั้นมีไม่พอจะเขียนใหม่

    • ที่จริงแล้ว แค่กำจัด C++ ที่ไม่ memory-safe และบังคับใช้แนวทางปฏิบัติก็อาจบรรลุเป้าหมายนี้ได้ภายในโค้ดเบส C++ เอง
      การ “เขียนใหม่ด้วย 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
      ภาษาอะไรก็ตามที่ทีมหรือบริษัทใช้งานได้อย่างสบายใจ มักเป็นกฎคร่าว ๆ ที่ดี
    • มันเป็นบทความเกี่ยวกับ C++ และไม่มีคำว่า “Rust” โผล่มาแม้แต่ครั้งเดียว มีแค่การพูดถึงภาษา “memory safe” หลายสิบภาษา แต่ก็ยังน่าสนใจที่มีการยัดมีม Rust เข้ามาเพื่อใช้กดมันลง
      ตอนนี้เหมือนจะมาถึงขั้นสงสัยแล้วว่าผู้เขียนเป็นโปรแกรมเมอร์ Rust สายมืดที่แฝงตัวมา เพราะคงบ่นใส่ Rust ตรง ๆ ไม่ได้ตั้งแต่แรกเนื่องจากบทความไม่ได้พูดถึง Rust เลย