4 คะแนน โดย GN⁺ 2024-10-30 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ทันทีที่เขียนโค้ดขึ้นมา โค้ดก็สร้าง ต้นทุนการบำรุงรักษา ดังนั้นโครงสร้างที่ลบหรือแทนที่ได้ง่ายในภายหลังจึงมักสำคัญกว่าการนำกลับมาใช้ซ้ำ
  • ยิ่งมีผู้ใช้ API มาก ต้นทุนการเปลี่ยนแปลงก็ยิ่งสูง และยิ่งพึ่งพา API ของบุคคลที่สามลึกเท่าไร การเปลี่ยนแปลงจากภายนอกก็ยิ่งทำให้ codebase สั่นคลอนมากขึ้นเท่านั้น
  • การทำซ้ำ, boilerplate, การแบ่งเลเยอร์, ก้อนโค้ดขนาดใหญ่, การแยกโมดูล และ feature flag ล้วนเป็นเครื่องมือ จัดการ dependency ได้ตามสถานการณ์
  • การแยกส่วนที่ดีไม่ใช่การรวมฟังก์ชันร่วมกันไว้ด้วยกัน แต่ใกล้เคียงกับการซ่อน การตัดสินใจด้านการออกแบบ ที่เปลี่ยนยากหรือมีโอกาสเปลี่ยนสูงออกจากกัน
  • โค้ดที่ดีไม่ใช่โค้ดที่สมบูรณ์แบบตั้งแต่แรก แต่เป็น legacy code ที่รบกวนน้อยลงแม้เวลาผ่านไป และท้ายที่สุดคือ โค้ดที่ลบง่าย

โค้ดคือต้นทุน และการลบคือการลดต้นทุน

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

ขั้นที่ 0: ไม่เขียนโค้ด

  • จำนวนบรรทัดโค้ดไม่ได้บอกทุกอย่าง แต่ขนาดอย่าง 50 บรรทัด, 500 บรรทัด, 5,000 บรรทัด, 10,000 บรรทัด, 25,000 บรรทัดนั้นสำคัญ
  • monolith 1 ล้านบรรทัดต้องใช้เวลา ต้นทุน และความพยายามในการแทนที่มากกว่า monolith 10,000 บรรทัด
  • ยิ่งมีโค้ดมากก็ยิ่งกำจัดยาก แต่การลดโค้ดเพียงหนึ่งบรรทัดแทบไม่ประหยัดอะไรเลย
  • โค้ดที่ลบง่ายที่สุดคือโค้ดที่ ไม่ได้เขียนตั้งแต่แรก

ขั้นที่ 1: คัดลอก-วาง

  • โค้ดที่นำกลับมาใช้ซ้ำได้ ทำได้ง่ายกว่าหากสร้างหลังจากมีกรณีใช้งานหลายแบบเกิดขึ้นจริงแล้ว แทนที่จะคาดเดาการใช้งานในอนาคตแล้วสร้างไว้ล่วงหน้า
  • การลอง คัดลอก-วาง ใน codebase สักสองสามครั้งช่วยให้เข้าใจรูปแบบการใช้งานจริงได้ดี
  • ทันทีที่ทำโค้ดบางส่วนให้เป็น API ที่ใช้ร่วมกัน โค้ดนั้นก็เปลี่ยนแปลงได้ยากขึ้น
  • โค้ดที่เรียกใช้ฟังก์ชันจะไม่ได้พึ่งพาแค่พฤติกรรมที่มีเอกสารกำกับไว้ แต่ยังพึ่งพาพฤติกรรมทั้งที่ตั้งใจและไม่ตั้งใจซึ่งสังเกตได้จาก implementation ด้วย
  • การลบโค้ดภายในฟังก์ชันนั้นง่ายกว่าการลบตัวฟังก์ชันเอง

ขั้นที่ 2: หยุดคัดลอก-วาง

  • โค้ดที่ถูกทำซ้ำมากพอแล้ว คือเวลาที่ควรยกขึ้นเป็นฟังก์ชัน
  • โค้ดเชิง utility ที่จำเป็นต้องใช้บ่อยบนมาตรฐานไลบรารี เช่น โค้ดเปิดไฟล์ตั้งค่าแล้วคืน hash table หรือโค้ดลบไดเรกทอรี อยู่ในกลุ่มนี้
  • util ควรเป็นไดเรกทอรีมากกว่าไฟล์เดียว และควรแยก utility ต่างชนิดกันไว้คนละไฟล์
    • ไฟล์ util เดี่ยว ๆ จะโตขึ้นเรื่อย ๆ และเมื่อใหญ่เกินไปแล้วก็แยกได้ยาก
  • โค้ดที่เจาะจงกับแอปพลิเคชันหรือโปรเจกต์น้อยกว่า จะนำกลับมาใช้ซ้ำได้ง่ายกว่า และมีโอกาสเปลี่ยนหรือลบน้อยกว่า
    • โค้ดไลบรารีอย่าง logging, API ของบุคคลที่สาม, file handle, process อยู่ในกลุ่มนี้
    • list, hash table, collection มักไม่ถูกลบง่าย ๆ ไม่ใช่แค่เพราะมี interface เรียบง่าย แต่เพราะขอบเขตไม่ขยายใหญ่ขึ้นตามเวลา
  • หัวใจคือการวางส่วนที่ลบยากให้อยู่ไกลจากส่วนที่ลบง่ายมากที่สุด

ขั้นที่ 3: เขียน boilerplate ให้มากขึ้น

  • การสร้างไลบรารีช่วยหลีกเลี่ยงการคัดลอก-วางได้ แต่ในทางปฏิบัติ เรามักต้องเขียน boilerplate จำนวนมากเพื่อใช้ไลบรารีนั้น
  • boilerplate คล้ายกับการคัดลอก-วาง ตรงที่ทุกครั้งต้องเปลี่ยนตำแหน่งบางจุดเล็กน้อย
  • การทำซ้ำแบบนี้คือการยอมรับความยืดยาวเพื่อแลกกับการลด dependency และได้ความยืดหยุ่น
  • ไลบรารีที่ต้องใช้ boilerplate มักเป็นกรณีที่ยากจะผสมนโยบายกับโปรโตคอล เช่น network protocol, wire format, เครื่องมือ parsing
    • โปรโตคอลว่าด้วยสิ่งที่โปรแกรมทำได้
    • นโยบายว่าด้วยสิ่งที่โปรแกรมควรทำ
  • โค้ดแบบนี้มักเป็นข้อกำหนดเพื่อสื่อสารกับคอมพิวเตอร์เครื่องอื่นหรือจัดการไฟล์อื่น จึงลบได้ยาก
  • สิ่งสำคัญคือไม่กระจาย business logic ไปปะปนในโค้ดเหล่านี้
  • แม้ต้องเขียนบรรทัดมากขึ้น ก็ควรเขียนบรรทัดเหล่านั้นในส่วนที่ลบง่ายกว่า

ขั้นที่ 4: ไม่เขียน boilerplate

  • เมื่อ boilerplate มากเกินไป ก็ถึงเวลาสร้างไลบรารีที่ห่อไลบรารียืดหยุ่นไว้ และมีความเห็นชัดเจนเรื่องนโยบาย เวิร์กโฟลว์ และสถานะ
  • การสร้าง API ที่ใช้ง่ายนั้นใกล้เคียงกับการทำ boilerplate ให้เป็น ไลบรารี
  • Python HTTP client requests เป็นตัวอย่างที่ให้ interface เรียบง่ายบน urllib3 ที่ยืดยาวกว่า
    • requests จัดการเวิร์กโฟลว์ทั่วไปของการใช้ HTTP และซ่อนรายละเอียดเชิงปฏิบัติ
    • urllib3 ให้ความสามารถอย่าง pipelining และ connection management และไม่ได้ซ่อนรายละเอียดจากผู้ใช้
  • การห่อไลบรารีหนึ่งด้วยอีกไลบรารีหนึ่งไม่ใช่แค่การซ่อนรายละเอียด แต่เป็น การแยก concern
  • ไม่ควรใส่ business logic ในไดเรกทอรี util และควรวางไลบรารีที่ใช้ง่ายทับบนไลบรารีที่ implement ได้เรียบง่าย
  • บางครั้งการห่อไลบรารีของบุคคลที่สามก็เป็นทางเลือกที่ดี
    • เพื่อไม่ให้ทั้งโปรเจกต์ถูกล็อกกับตัวเลือกเฉพาะหนึ่ง ๆ และสามารถสร้างไลบรารีที่เข้ากับโค้ดของตนเองได้
  • API ที่ใช้งานดีและ API ที่ขยายได้มักขัดแย้งกัน
  • การแบ่งเลเยอร์ไม่ใช่การเขียนโค้ดที่จะลบในภายหลังเท่านั้น แต่ใกล้เคียงกับการทำให้โค้ดที่ลบยากใช้งานได้ดีโดยไม่ทำให้มันปนเปื้อนด้วย business logic

ขั้นที่ 5: เขียนก้อนโค้ดขนาดใหญ่

  • แม้จะคัดลอก-วาง, refactor, แบ่งเลเยอร์ และประกอบกันอย่างไร สุดท้ายโค้ดก็ต้องทำบางอย่าง ดังนั้นบางครั้งจึงต้องมี ก้อนโค้ดขนาดใหญ่ ที่ยึดส่วนที่เหลือไว้ด้วยกัน
  • business logic อาจมีลักษณะเป็น edge case ไม่รู้จบและการ hack แบบเร่งด่วน
  • โค้ดเกมหรือโค้ดของผู้ก่อตั้งสตาร์ทอัพก็อาจมองเป็นโค้ดประเภทเดียวกันที่เลือกทางลัดเพื่อประหยัดเวลาอย่างมาก
  • บางครั้งการลบความผิดพลาดใหญ่หนึ่งอย่างง่ายกว่าการกำจัดความผิดพลาดเล็ก ๆ 18 อย่างที่พันกันอยู่
  • งานเขียนโปรแกรมจำนวนมากเป็นการสำรวจ ดังนั้นการลองผิดสองสามครั้งแล้ว iterate อาจเร็วกว่าการพยายามทำให้ถูกตั้งแต่แรก
  • ตอนสร้างเกมแรก อย่าเริ่มจากการสร้างเอนจิน และก่อนเขียนแอปพลิเคชันก็ไม่ควรเริ่มจากการสร้างเว็บเฟรมเวิร์ก
  • Monorepo ก็เป็นการประนีประนอมแบบคล้ายกัน
    • เป็นเรื่องยากที่จะรู้ล่วงหน้าว่าควรแบ่งโค้ดอย่างไร และความผิดพลาดใหญ่หนึ่งอย่าง deploy ง่ายกว่า 20 ส่วนที่ผูกกันแน่น
  • หากรู้ว่าเป็นโค้ดที่จะถูกทิ้ง ลบ หรือแทนที่ได้ง่ายในไม่ช้า ก็สามารถเลือกทางลัดได้มากขึ้น
  • เป้าหมายไม่ใช่การทำซ้ำก้อนโคลนเดิมสิบครั้งเพื่อทำให้ความผิดพลาดสมบูรณ์แบบ แต่คือการทำความผิดพลาดใหม่ทุกครั้ง รับความเสี่ยงใหม่ และสะสมผ่านการ iterate
  • สุดท้ายโปรเจกต์จะล้มเหลวหรือกลายเป็น legacy code และความล้มเหลวเกิดบ่อยกว่าความสำเร็จ
  • การลบทั้งก้อนมักง่ายกว่าการลบโค้ดทีละชิ้น

ขั้นที่ 6: แบ่งโค้ดเป็นชิ้น ๆ

  • ก้อนโคลนขนาดใหญ่สร้างง่ายที่สุด แต่มีต้นทุนการบำรุงรักษาสูงที่สุด
  • การเปลี่ยนแปลงที่ดูเรียบง่ายอาจทำให้ต้องแตะเกือบทุกส่วนของ codebase แบบแก้ขัด
  • โค้ดที่เคยลบง่ายเมื่อมองทั้งก้อน กลับลบยากเมื่อแยกเป็นชิ้น ๆ
  • ควรแบ่งโมดูลตามสิ่งที่ไม่ได้แชร์กับส่วนอื่นและ การตัดสินใจด้านการออกแบบ ที่ต้องซ่อน ไม่ใช่ตามฟังก์ชันร่วมกัน
  • ตามเกณฑ์ของ D. Parnas เราสามารถลิสต์การตัดสินใจด้านการออกแบบที่ยากหรือมีโอกาสเปลี่ยนสูง และออกแบบให้แต่ละโมดูลซ่อนการตัดสินใจเหล่านั้นจากโมดูลอื่น
  • โมดูลไม่ได้สร้างขึ้นเพื่อการนำกลับมาใช้ซ้ำ แต่เพื่อ ความสามารถในการเปลี่ยนแปลง
  • หลักการความรับผิดชอบเดียวอาจมองได้ว่า “แต่ละโมดูลควรจัดการปัญหายากเพียงหนึ่งอย่าง” แต่สิ่งที่สำคัญกว่าคือ “ปัญหายากแต่ละอย่างควรถูกจัดการในโมดูลเดียวเท่านั้น”
  • หากโมดูลหนึ่งทำสองอย่าง มักเป็นเพราะการเปลี่ยนส่วนหนึ่งจำเป็นต้องเปลี่ยนอีกส่วนด้วย
  • คอมโพเนนต์ที่แย่มากแต่มี interface เรียบง่ายเพียงตัวเดียว อาจง่ายกว่าคอมโพเนนต์สองตัวที่ต้องปรับจูนร่วมกันอย่างละเอียด

การผูกกันแบบหลวมและ interface ร่วม

  • ระบบที่สามารถลบบางส่วนได้โดยไม่ต้องเขียนส่วนอื่นใหม่ มักถูกเรียกว่า การผูกกันแบบหลวม
  • การผูกกันแบบหลวมใกล้เคียงกับสภาวะที่เมื่อเปลี่ยนความคิดแล้วไม่ต้องแก้โค้ดมากเกินไป
  • แม้แต่การ hardcode ตัวแปรหนึ่งครั้ง หรือการใช้ command-line flag แทนตัวแปร ก็อาจเป็นการผูกกันแบบหลวมได้ในบางกรณี
  • Microsoft Windows บรรลุเป้าหมายนี้ด้วยการแยก API ภายนอกและ API ภายใน
    • API ภายนอกผูกกับวงจรชีวิตของโปรแกรมเดสก์ท็อป
    • API ภายในผูกกับ kernel พื้นฐาน
    • การซ่อน API ช่วยให้ได้ความยืดหยุ่นโดยไม่ทำให้ซอฟต์แวร์จำนวนมากพัง
  • HTTP ก็ให้ตัวอย่างของการผูกกันแบบหลวม
    • สามารถวาง cache ไว้หน้า HTTP server ได้
    • ย้ายรูปภาพไปยัง CDN แล้วเปลี่ยนแค่ลิงก์ เบราว์เซอร์ก็ไม่พัง
    • HTTP error code ให้รหัสเฉพาะกับปัญหาร่วมกัน ทำให้ client จัดการ error จำนวนมากแทนได้
  • ควรพิจารณาวิธีจัดการ failure ไปพร้อมกับการแบ่งโค้ดเป็นชิ้นเล็ก ๆ

การจัดการ failure และระดับการผูกกัน

  • Erlang/OTP ใช้วิธีจัดการ failure ที่ค่อนข้างเฉพาะตัวด้วย supervision tree
  • แต่ละ process ในระบบ Erlang โดยคร่าว ๆ จะถูกเริ่มและเฝ้าดูโดย supervisor
    • หาก process มีปัญหา ก็จะ terminate
    • เมื่อ process terminate แล้ว supervisor จะเริ่มใหม่
    • supervisor ถูกเริ่มโดย bootstrap process และหาก supervisor มี failure bootstrap process จะเริ่มใหม่
  • แนวคิดหลักคือการ fail fast แล้ว restart มักเร็วกว่าการจัดการ error
  • failure ชั่วคราวบางกรณีถูกกดไว้ได้ด้วยวิธีปิดแล้วเปิดใหม่
  • การจัดการ error และการกู้คืนควรทำที่เลเยอร์ด้านนอกของ codebase ซึ่งเป็นที่รู้จักในชื่อ end-to-end principle
  • การจัดการ failure ที่ปลายทั้งสองด้านง่ายกว่าตรงกลางของการเชื่อมต่อ และแม้จะจัดการภายใน สุดท้ายก็ยังต้องมีการตรวจสอบระดับบนสุดอยู่ดี
  • การจัดการ error เป็นหนึ่งในหลายวิธีที่ทำให้ระบบผูกกันแน่น

IMAP, ระบบไฟล์, SQL, middleware

  • IMAP ทำให้การจัดการ error เจ็บปวด เพราะแทบทุกงานอยู่ในรูปแบบข้อยกเว้นที่มีตัวเลือกและการจัดการเฉพาะตัว
  • ใน IMAP error อาจปรากฏอยู่กลางผลลัพธ์ของงานอื่นได้
  • แทนที่จะใช้ UUID จะสร้าง token เฉพาะเพื่อระบุแต่ละข้อความ และ token นี้ก็อาจเปลี่ยนกลางผลลัพธ์ของงานได้เช่นกัน
  • งานจำนวนมากใน IMAP ไม่เป็น atomic
  • ใช้เวลามากกว่า 25 ปีกว่าจะมีวิธีย้ายอีเมลจากโฟลเดอร์หนึ่งไปอีกโฟลเดอร์หนึ่งได้อย่างน่าเชื่อถือ
  • ยังมี encoding แบบ UTF-7 พิเศษและ encoding แบบ base64 เฉพาะตัวด้วย
  • ระบบไฟล์และฐานข้อมูลเป็นกรณีเปรียบเทียบที่ดีกว่าสำหรับ remote storage
    • ระบบไฟล์มีชุดการทำงานที่คงที่และมีหลาย object
    • SQL ดูเหมือน interface ที่กว้างกว่าระบบไฟล์ แต่เป็นไปตามแพตเทิร์นของการทำงานหลายแบบบน set และหลาย row
  • ฐานข้อมูลไม่ได้สลับแทนกันได้เสมอไป แต่การหาสิ่งที่ทำงานกับ SQL ได้ง่ายกว่าภาษา query ที่สร้างเอง
  • Finagle ของ Twitter ใช้ API ร่วมสำหรับ service เพื่อให้เพิ่มการจัดการ timeout, กลไก retry และการตรวจสอบ authentication ลงในโค้ด client และ server ได้ง่าย
  • ตัวอย่างที่ดีของการผูกกันแบบหลวมมักเป็นตัวอย่างของ interface ที่สม่ำเสมอ ด้วย
  • codebase ที่แข็งแรงไม่จำเป็นต้อง modular อย่างสมบูรณ์แบบ แต่ควรมีระยะห่างเพียงพอระหว่างส่วนที่เคลื่อนไหว
  • โค้ดที่ผูกกันหลวมไม่ได้แปลว่าจะลบง่ายเสมอไป แต่แทนที่และเปลี่ยนแปลงได้ง่ายกว่ามาก

ขั้นที่ 7: เขียนโค้ดต่อไป

  • หากเขียนโค้ดใหม่ได้โดยไม่ต้องยุ่งกับโค้ดเก่า การทดลองไอเดียใหม่ ๆ ก็ง่ายขึ้นมาก
  • ประเด็นหลักไม่ใช่ว่าเป็น microservice หรือ monolith แต่คือควรสามารถวางการทดลองหนึ่งหรือสองอย่างบนระบบได้ระหว่างที่กำลังเรียนรู้ว่ามันทำอะไร
  • feature flag เป็นวิธีที่ทำให้เปลี่ยนความคิดได้ภายหลัง
  • feature flag ไม่ได้ใช้แค่ทดลองฟีเจอร์ แต่ยังทำให้ deploy การเปลี่ยนแปลงได้โดยไม่ต้อง deploy ซอฟต์แวร์ใหม่
  • Google Chrome พบว่าส่วนที่ยากที่สุดในรอบ release ปกติคือเวลาที่ใช้ในการ merge feature branch ที่อยู่ยาวนาน
  • หากเปิดปิดโค้ดใหม่ได้โดยไม่ต้อง recompile ก็สามารถแบ่งการเปลี่ยนแปลงใหญ่เป็น merge เล็ก ๆ และอาจไม่กระทบโค้ดเดิม
  • เมื่อฟีเจอร์ใหม่ปรากฏใน codebase เดียวกันเร็วขึ้น ก็จะมองเห็นผลกระทบของการพัฒนาฟีเจอร์ระยะยาวต่อส่วนอื่นได้ชัดเจนขึ้น
  • feature flag ไม่ใช่แค่ command-line switch ธรรมดา แต่เป็นวิธีแยกการ release ฟีเจอร์ออกจากการ merge branch และการ deploy โค้ด
  • เมื่อการ deploy ซอฟต์แวร์ใหม่อาจใช้เวลาหลายชั่วโมง หลายวัน หรือหลายสัปดาห์ ความสามารถในการเปลี่ยนความคิดตอน runtime ก็ยิ่งสำคัญขึ้น

โค้ดที่ดีคือ legacy code ที่ไม่ขวางทาง

  • สิ่งที่สำคัญกว่าการ iterate คือการมี feedback loop
  • แทนที่จะสร้างโมดูลเพื่อการนำกลับมาใช้ซ้ำ หัวใจคือการแยกคอมโพเนนต์เพื่อรองรับการเปลี่ยนแปลง
  • การตอบสนองต่อการเปลี่ยนแปลงรวมถึงการลบฟีเจอร์เก่า ไม่ใช่แค่การพัฒนาฟีเจอร์ใหม่
  • การเขียนโค้ดที่ขยายได้คือการหวังว่าอีก 3 เดือนข้างหน้า ตัวเลือกแรกยังถูกต้องอยู่
  • โค้ดที่ลบได้เริ่มจากสมมติฐานตรงกันข้าม
  • การแบ่งเลเยอร์, การแยก isolation, interface ร่วม และการประกอบกัน เป็นวิธีสร้างซอฟต์แวร์ที่เปลี่ยนแปลงได้ตามเวลา มากกว่าการสร้างซอฟต์แวร์ที่ดีในตัวเอง
  • ไม่จำเป็นต้องทิ้งทุกอย่าง แต่บางส่วนต้องถูกลบ
  • โค้ดที่ดีไม่ใช่โค้ดที่ถูกต้องตั้งแต่แรก แต่เป็น legacy code ที่ไม่ขวางทาง
  • โค้ดที่ดีคือ โค้ดที่ลบง่าย

1 ความคิดเห็น

 
GN⁺ 2024-10-30
ความคิดเห็นจาก Hacker News
  • คำพูดที่ผมชอบคือ ความเรียบง่ายคือความแข็งแรงทนทาน
    คล้ายกับกฎการเปลี่ยนแปลงอย่างต่อเนื่องของ Lehman หมายความว่ายิ่งระบบมีความซับซ้อนต่ำ ก็ยิ่งเปลี่ยนแปลงได้ง่าย
    แทนที่จะเขียนโค้ดให้ขยายได้เพื่อเตรียมรับอนาคต ผมคิดว่าการเตรียมอนาคตด้วย โค้ดที่เข้าใจได้โดยสัญชาตญาณ ดีกว่า
    เช่น ทำ abstraction เฉพาะเมื่อจำเป็นจริง ๆ, ยอมให้มีการซ้ำซ้อนแบบง่าย ๆ, เริ่มจาก monolith ก่อน และให้ vertical scaling มาก่อน horizontal scaling
    ผมเคยสร้างระบบจาก 0→1 มาหลายระบบ และรูปแบบร่วมกันทั้งหมดก็ไปทางนี้
    https://en.m.wikipedia.org/wiki/Lehman%27s_laws_of_software_...

    • ถูกต้อง แต่เวลาใช้หลัก ความเรียบง่ายคือความแข็งแรงทนทาน ต้องเข้าใจความซับซ้อนที่มีอยู่โดยเนื้อแท้ด้วย
      การไม่จัดการ edge case ไม่ได้ทำให้โค้ดแข็งแรงขึ้น ต่อให้มันดูเรียบง่ายกว่าก็ตาม
    • กฎที่ผมทำตามคือ: ครั้งแรกก็เขียนไปเลย ครั้งที่สองให้คัดลอก ครั้งที่สามค่อยพิจารณา refactor
    • เห็นด้วย แต่ไม่แน่ใจว่าคำว่า simple is robust จะเข้าใจได้โดยสัญชาตญาณพอหรือไม่
      มันเปิดให้ถกเถียงว่า “ความเรียบง่าย” คืออะไรและนำไปใช้กับระบบอย่างไร ซึ่งก็เป็นคำถามที่ซับซ้อนพอให้ Rich Hickey หยิบมาพูดถึง
      บางที “สิ่งที่โง่ ๆ นั้นแข็งแรงทนทาน” หรือ “สิ่งที่ตรงไปตรงมานั้นแข็งแรงทนทาน” อาจสื่อเจตนาได้ดีกว่า
    • เห็นด้วยอย่างแรงจริง ๆ ขยะจำนวนมากเกินไปในซอฟต์แวร์เกิดจากการพยายามแก้ ปัญหาในจินตนาการ
      แค่เขียนโค้ดที่ทำสิ่งที่จำเป็นก็พอ อย่าสร้างปัญหาเรื่องการขยายตัวที่สมมติขึ้นมา อย่าสร้าง abstraction ฉลาด ๆ เพื่อให้ดูเก่ง เขียนเป็น monolith แล้วเอาขึ้น VM ก็พร้อมรันใน production ได้ทันที
      ถ้ามีปัญหาก็ค่อยแก้ตอนนั้น และถ้าเป็นไปได้ควรเป็นหลังจากกระแสเงินสดเป็นบวกแล้ว
      สตาร์ทอัพ “AirBnb สำหรับสุนัข” ที่มีผู้ใช้ 0 คนจะกังวลเรื่อง C100K ไปทำไม? ที่ AWS โน้มน้าวให้คุณจ่ายเงินกับ serverless นั้นเพื่อประโยชน์ของคุณ หรือเพื่อรีดเงินจากคุณกันแน่
    • ความซับซ้อนของ business logic ไม่ได้หายไปเพราะอยากให้หาย ถ้ามันใหญ่และเกี่ยวพันกันมาก โค้ดก็จะเป็นแบบนั้นด้วย
  • บทความที่เกี่ยวข้อง:
    Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=24989351 - พ.ย. 2020 (30 ความคิดเห็น)
    Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=23914486 - ก.ค. 2020 (109 ความคิดเห็น)
    Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=18761739 - ธ.ค. 2018 (2 ความคิดเห็น)
    Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=11093733 - ก.พ. 2016 (133 ความคิดเห็น)

  • ถ้าสรุปสั้น ๆ ถึงความผิดพลาดที่เคยทำตอนยังหนุ่ม ตอนนี้ผมกลับมาเชื่อใน การออกแบบเพื่อการลบ
    เมื่อก่อนผมคิดว่าสามารถคาดการณ์ทุกสถานการณ์และสร้างผลงานศิลปะสุดเจ๋งที่ตอบสนองทุกความต้องการได้ แต่ไม่มีใครคาดการณ์ความต้องการในอนาคตได้ดีขนาดนั้น
    สักวันหนึ่ง สิ่งที่ผมสร้างอาจกลายเป็น “ไอ้ของโง่ ๆ นั่น” สำหรับใครบางคน และไม่ว่าตอนนี้ผมจะภูมิใจกับมันแค่ไหน การที่พวกเขาทุบมันทิ้งทั้งหมดก็อาจเป็นเรื่องสมเหตุสมผล
    ดังนั้นการทุ่มแรงให้มันลบออกได้ง่ายจึงดีกว่า แบบนี้มักลด coupling ลงได้บ่อย แต่ประเด็นสำคัญคือมันต่างจากการ decouple แบบนักพัฒนาหนุ่มไฟแรงที่อยากแยกทุกอย่างออกเป็นเฟรมเวิร์กที่ตั้งค่าเชิงเมตาได้
    บางครั้ง การผูกกันแน่น ที่เข้าใจง่ายก็อาจดีกว่า
    https://news.ycombinator.com/item?id=41219130

    • คุณอาจทำให้มันลบออกได้ง่าย แต่คนอื่น ๆ ก็อาจเต็มใจสร้าง abstraction และ logic แล้วฝังไว้ในแกนกลางของโปรเจกต์ จนภายหลังมันแข็งตัวถึงขั้นแทบเอาออกไม่ได้
      ตัวอย่างเช่น สิ่งอย่าง CommonExcelFileParser, CommonExcelFileParserUtilities, HasExcelParseStatus, ProductImportExcelParser, ProductImportExcelParserView, ProductImportExcelParserResultHandler โผล่ขึ้นมา แล้วกลายเป็นฐานของโค้ดรอบ ๆ
      คล้ายกับเวลาที่เริ่มโปรเจกต์ frontend ด้วย React หรือ Angular แล้วการย้ายไปอย่างอื่นกลายเป็นภารกิจแบบซิซิฟัส
      ในความเป็นจริง ผู้คนมักสร้างทั้งแพลตฟอร์มขึ้นมา และแม้จะมีทางเลือกที่จะก่อปัญหาในอนาคต แต่เพราะ coupling ทำให้ refactor ยากกว่า codebase ที่ abstract น้อยเกินไปมาก
      ดูเหมือนผู้คนจะชอบทำเรื่องแบบนี้มากกว่าการใช้ KISS และ YAGNI แล้วสร้างโค้ดที่ลบง่าย ผมเลยไม่รู้ว่าควรทำอย่างไรในสถานการณ์แบบนั้น
    • ถึงอย่างนั้นก็ขึ้นอยู่กับบริบท ถ้าเป็น แอปพลิเคชันสำหรับงานธุรกิจ ก็ใช่ และใช่แบบสิบครั้ง
      requirement ทางธุรกิจเปลี่ยนและเคลื่อนไหวอยู่เสมอ ดังนั้นอย่าพยายามคาดการณ์ แต่ควรเขียนสิ่งที่เปลี่ยนแทนหรือทิ้งได้ง่าย
      framework และ library จะแตกต่างออกไปเล็กน้อย แม้ต้องปรับให้เข้ากับความเปลี่ยนแปลงของโลก แต่ทำได้ด้วยจังหวะที่นุ่มนวลกว่ามาก
      ปัญหาใหญ่ที่สุดคือในแอปพลิเคชันธุรกิจที่ใช้ framework อย่าง Rails หรือ Asp.Net อยู่แล้ว นักพัฒนายังอยากสร้าง “framework” ขึ้นมาอีก
    • บางอย่างเปลี่ยนไป และบางครั้งเราก็เลือก abstraction ที่ผิด
      ถ้าคุณไม่ได้เขียน Linux kernel ก็ไม่ควรเขียนเหมือน Linux kernel
  • ค่อนข้างแปลกที่บทความนี้ไม่พูดถึง การทดสอบและความสามารถในการสังเกตการณ์ เลย
    การทดสอบเองก็มีต้นทุนในการบำรุงรักษา แต่ช่วยลดความเสี่ยงที่จะทำอะไรพังเมื่อเอาบางอย่างออกไป
    ยิ่งถ้าคุณเปิดบริการให้ผู้เรียกจากภายนอกใช้งาน ก็ต้องมีทั้งวิธีที่แข็งแรงพอในการทำเครื่องหมายบางการเรียกว่าจะเลิกใช้และค่อยลบในภายหลัง และวิธีสังเกตว่ายังมีการเรียกอยู่ไหม รวมถึงใครเป็นคนเรียก
    ไม่นานมานี้เราเพิ่งลบ GraphQL resolver ที่เปิดเผยออกสู่ภายนอกเป็นครั้งแรกแบบกึ่งอัตโนมัติ โดยมี metrics อยู่แล้วว่า resolver แต่ละตัวถูกใช้บ่อยแค่ไหน จึง parse มันเพื่อดึงรายการ resolver ที่ยังลบไม่ได้ออกมาได้
    GraphQL มี annotation deprecated อยู่แล้ว แต่บริการของเราไม่ได้จัดการ annotation นั้นเป็นพิเศษ
    ดังนั้นเราจึงเพิ่มความสามารถในการสังเกตการณ์ที่บอกเมื่อมีการเรียกฟังก์ชันที่ deprecated แล้ว แล้วปล่อยให้รันใน production นานพอ จากนั้นจึงลบโค้ดที่เปิดเผยต่อภายนอกได้อย่างปลอดภัย

    • ถ้าพูดให้ง่ายลงหน่อย การทำสิ่งที่ ลบได้ง่าย จะช่วยไม่ให้สร้างบั๊กโดยไม่ตั้งใจตอนลบ
      พอเริ่มทำให้ซับซ้อนเกินไป ทุกอย่างก็จะกลายเป็นกองยุ่งเหยิงที่เชื่อมถึงกันหมด และนักพัฒนาจะไม่รู้ว่าการเปลี่ยนแปลงจะกระทบอะไรบ้าง
      แน่นอนว่ามีหลายวิธีที่จะทำให้พังได้ อาจทำตามหลัก “best practices” โง่ ๆ หรือทำ “microservices” ในแบบที่ไม่รู้ว่าใคร consume service ไหนอยู่ก็ได้ แต่ถ้าเป็นอย่างนั้นก็ไม่ได้ทำให้มันลบง่าย
      การ consume จากภายนอกเป็นตัวอย่างที่ดี การเตือน consumer อย่างสมเหตุสมผลว่า service จะถูกเลิกใช้นั้นเป็นเรื่องที่เหมาะสม แต่ถ้าอยากปิดจริง ๆ เมื่อไรก็ปิดไม่ได้ นั่นก็ไม่ใช่ระบบที่ออกแบบมาให้ลบได้ง่าย
      ถ้าแนวทางนั้นเหมาะ ก็ทำแบบนั้นได้ เพียงแต่การคาดหวังให้การทดสอบและ observability บอกว่ามีอะไรพังหรือไม่ อาจไม่ค่อยได้ผลนัก
      ไม่ได้ต่อต้านการทดสอบเอง แต่คงพูดได้ยากว่ามันเป็นกลไกป้องกันที่ยอดเยี่ยมสำหรับบอกว่าเราไปทำอะไรพังในโซ่ที่ยาวและซับซ้อนหรือไม่ เพราะในทางปฏิบัติ การมีขอบเขตการทดสอบที่จะปกป้องได้จริงนั้นทำได้ยากมาก
    • ถ้ามีบรรทัดโค้ดมาก ก็พอคาดได้ว่าจะมีบรรทัดทดสอบจำนวนหนึ่งตามสัดส่วน
      ถ้าลบโค้ดบางส่วน ก็ลบเทสต์บางส่วนได้เช่นกัน
      อาจมองได้ว่าบทความพูดถึงแต่โค้ด และรวมผลกระทบที่เกี่ยวข้องกับการทดสอบไว้โดยปริยาย
      การที่บทความไม่ได้พูดถึงการทดสอบ ไม่ได้แปลว่าสามารถสรุปได้ว่าไม่ควรเขียนเทสต์
    • การทดสอบเป็นสิ่งที่ดี แต่การเขียนโปรแกรมไม่ได้จบแค่การเขียนเทสต์ ไม่จำเป็นต้องพูดถึงการทดสอบในทุกบทความ
  • เมื่อดูส่วนนี้แล้วรู้สึกว่าชื่อเรื่องไม่ได้ถูกเสมอไป: โค้ดที่ลบง่ายมักเป็น โค้ดที่ขยายได้ง่าย ด้วยเช่นกัน
    เพราะมันถูกแบ่งเป็นชั้น เป็นโมดูล และแยกชิ้นส่วนต่าง ๆ ออกจากกันผ่าน abstraction อย่าง interface หรือ contract ของ type อื่น ๆ

  • ผมเคยบอกนักศึกษาฟิสิกส์เชิงคำนวณว่า การคำนวณที่ดีที่สุด คือการคำนวณที่ไม่จำเป็นต้องทำตั้งแต่แรก

  • ส่วนตัวผมแบ่งโค้ดออกเป็นสองส่วนคือ business logic กับ implementation จริง
    business logic โดยธรรมชาติอาจมีความซ้ำกันได้ แต่รายละเอียดเชิงเทคนิคไม่ควรซ้ำกันมากเกินไป
    implementation จริงจะรกแค่ไหนก็ได้ ตราบใดที่ไม่ใส่ business logic ลงไปโดยตรงและคงความเป็นอิสระจากแอปพลิเคชัน
    ถ้าทำไว้อย่างนั้น เมื่อรู้ว่าบางอย่างเละเทะและทำงานไม่ดี เราจะมีทางเลือกในการลบ implementation ทั้งหมดทิ้ง แทนที่จะต้องไล่ย้อนจาก implementation ไปหาสเปกจริงแล้วฝืนแก้

  • ประโยคในย่อหน้าแรกที่ว่า “ปัญหาของ การใช้โค้ดซ้ำ คือมันขัดขวางการเปลี่ยนใจในภายหลัง” เป็นความผิดพลาดชัดเจน
    พูดโดยทั่วไปแล้วมันผิด ถ้าเปลี่ยนใจแล้วโค้ดถูก copy-paste ไว้สิบที่ ก็ต้องแก้สิบที่
    ตรงกันข้าม ถ้ามันอยู่ในฟังก์ชัน ก็แก้ครั้งเดียวพอ ต่อให้ภายหลังพบว่าหนึ่งในสิบจุดที่เรียกไม่ควรถูกเปลี่ยน ก็สามารถ copy-paste ตอนนั้น หรือทำให้ฟังก์ชันทั่วไปขึ้นได้
    เหมือนการข้ามถนนโดยไม่มอง การ copy-paste แทบจะเป็นความคิดที่แย่เสมอ

    • จากประสบการณ์ของผม โค้ด copy-paste ที่แย่จบลงด้วยการ ชำระหนี้ทางเทคนิค และแก้ไขในช่วงบ่ายที่น่าหงุดหงิดสักครึ่งวัน
      แต่ abstraction ที่แย่จะนำไปสู่การชำระหนี้ทางเทคนิคหลายเดือน
      แน่นอนว่าคำตอบคือ “อย่าสร้าง abstraction ที่แย่” แต่ทุกคนก็รู้ว่ามันเป็นอย่างไรในทีมและท่ามกลางความต้องการของผลิตภัณฑ์ที่เปลี่ยนไป
    • โค้ดที่ถูก reuse มักเป็นโค้ดที่ถูกต้องในหลาย ๆ จุด ดังนั้นถ้าจะเปลี่ยน ก็ต้องชะลอความเร็วลงและแยกจุดเหล่านั้นออกจากกัน
      มี git submodule ที่รวม widget UI ร่วมกันอยู่ ตอนนี้การเปลี่ยนหนึ่งในนั้นแทบเป็นไปไม่ได้ จนการคัดลอก component เข้ามาในโปรเจกต์แล้วแก้ใน local ง่ายกว่า
      นี่เป็นปัญหา โค้ดที่แชร์ร่วมกัน ควรถูกทำให้น้อยที่สุดเท่าที่เป็นไปได้ และการแชร์เองทำให้การเปลี่ยนแปลงยากขึ้น
    • ถ้าใน function call 10 จุด มี 3 จุดที่ต้องเปลี่ยนไปแบบหนึ่ง 5 จุดต้องเปลี่ยนไปอีกแบบ และอีก 2 จุดที่เหลือไม่ใช้ abstraction เดิมอีกต่อไปจนต้องเขียนใหม่ทั้งหมด จะเกิดอะไรขึ้น
      ถ้าทุกอย่างอยู่ในฟังก์ชันเดียว นักพัฒนาส่วนใหญ่จะพยายามเปลี่ยนฟังก์ชันนั้นให้รองรับทั้ง 10 กรณี ทั้งที่ตั้งแต่แรกมันไม่ควรเป็นฟังก์ชันเดียว
      การแก้โค้ดที่ถูก copy-paste ไว้สิบที่ ง่ายกว่าการแก้ ปมที่ผูกผิด ซึ่งถูกมัดไว้ครั้งเดียวแล้วไปยึดชิ้นส่วนต่าง ๆ ของระบบเอาไว้มาก
    • ถ้าเป็นผู้เขียนบทความ เขาน่าจะตอบว่าโค้ดนั้นควรถูกย้ายไปอยู่ในโมดูลหรือฟังก์ชัน
      มองเผิน ๆ อาจดูเหมือนขัดแย้งกับตัวเองในประเด็นนี้ แต่ถ้าอ่านช้า ๆ จะเห็นว่าเขาใช้การ copy-paste เป็นสัญญาณบอกว่าโค้ดใดควรถูก abstract และอะไรคือ pattern ที่ควรทำตามจริง ๆ
  • แปลกดีที่หลักบัญญัติสารพัดเกี่ยวกับซอฟต์แวร์ หรือหลักการที่แทบจะเป็น ศาสนา ถูกพูดซ้ำไปซ้ำมา
    บนกระดาษทุกอย่างดูยอดเยี่ยมและเหมือนสามัญสำนึก แต่ผ่านไป 50 ปี ซอฟต์แวร์ใน 90% ของกรณีก็ยังเป็นขยะ
    ถึงอย่างนั้นก็ยังหยิบเรื่องพวกนี้ขึ้นมาราวกับเป็น insight อัจฉริยะหรือกระสุนเงินอยู่เรื่อย ๆ

    • ผมคิดว่า 90% ที่เป็นขยะนั้นเป็นเพราะมันถูกเขียนโดยคนที่ไม่อ่านหรือไม่เขียนบทความแบบนี้
  • เรื่องนี้มี corollary ที่ยอดเยี่ยม: โค้ดที่แย่ อยู่ได้นาน เพราะมันลบออกได้ยากกว่ามาก