5 คะแนน โดย GN⁺ 2025-06-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เน้นผ่านประสบการณ์ของนักพัฒนา “สมองเล็ก” ที่เขียนโปรแกรมมายาวนานว่า ศัตรูที่ใหญ่ที่สุดของการพัฒนาซอฟต์แวร์คือ ความซับซ้อน และท่าทีในการลดมันควรเป็นแกนหลักของงานจริง
  • อาวุธพื้นฐานในการกันความซับซ้อนคือ การพูดว่า “no” และเมื่อจำเป็นต้องประนีประนอม ให้หาทางออกที่เป็นจริงด้วยวิธี 80/20, โปรโตไทป์, การรีแฟกเตอร์เล็ก ๆ และการเลื่อนการทำ abstraction ออกไป
  • การทดสอบควรเน้น integration test หลังจากโค้ดเริ่มนิ่งในระดับหนึ่ง ใช้ unit test และ end-to-end test จำนวนเล็กน้อยเป็นส่วนเสริม และเมื่อเจอบั๊กให้ทำซ้ำด้วย regression test ก่อนแล้วค่อยแก้
  • เครื่องมือ, type system, logging, debugger และ API ที่เรียบง่ายช่วยลดภาระความจำและการใช้เหตุผลของนักพัฒนาได้ แต่ generics, callback, microservices และ frontend framework ที่มากเกินไปอาจเพิ่มความซับซ้อนได้
  • ควรเข้าหาโค้ดเดิมและกระบวนการขององค์กรด้วยความถ่อมตน ไม่ลบโค้ดที่ยังไม่เข้าใจอย่างพลการ และต้องมีวัฒนธรรมที่สามารถพูดได้ว่า “ซับซ้อนเกินไป”

ความซับซ้อนคือศัตรูนิรันดร์ของนักพัฒนา

  • ศัตรูที่อันตรายที่สุดในการพัฒนาซอฟต์แวร์คือ ความซับซ้อน
    • ความซับซ้อนค่อย ๆ แทรกเข้าไปใน codebase และทำให้เมื่อแก้จุดหนึ่ง สิ่งที่ดูเหมือนไม่เกี่ยวข้องกันก็พังตามไปด้วย
    • Grug เปรียบมันกับ “ปีศาจ” ที่มองไม่เห็น เป็นสิ่งที่นักพัฒนาไม่อาจมองเห็นหรือปราบได้ง่าย ๆ ด้วยตนเอง
  • ความซับซ้อนอาจเข้ามาผ่านนักพัฒนาหรือ project manager ที่มีเจตนาดีได้เช่นกัน
    • ยิ่งฟีเจอร์, abstraction และ process เพิ่มขึ้น โค้ดก็ยิ่งเข้าใจยากขึ้น
    • Grug เองก็ยอมรับว่าบางครั้งเขาก็เป็นคนที่นำความซับซ้อนเข้ามาเอง

วิธีใช้ “no” และ “ok”

  • อาวุธที่แข็งแกร่งที่สุดในการกันความซับซ้อนคือ “no
    • ไม่สร้างฟีเจอร์ที่ไม่จำเป็น
    • ไม่สร้าง abstraction ที่ไม่จำเป็น
  • อย่างไรก็ตาม แม้ “no” จะเป็นคำแนะนำด้านวิศวกรรมที่ดี แต่ในแง่อาชีพก็ไม่ได้เป็นประโยชน์เสมอไป
    • “yes” อาจนำไปสู่ผลตอบแทนที่มากขึ้นหรือตำแหน่งบริหาร
    • ถึงอย่างนั้น หากต้องการซื่อตรงต่อตนเองในฐานะนักพัฒนา “no” ก็สำคัญ
  • เมื่อต้องประนีประนอม ให้ตอบว่า “ok” แล้วหาทางออกแบบ 80/20
    • 80/20 solution คือแนวทางที่ตอบสนองความต้องการ 80% ด้วยโค้ด 20%
    • แม้จะไม่ได้มีฟีเจอร์ตกแต่งครบทั้งหมด แต่ก็ส่งมอบคุณค่าส่วนใหญ่และยับยั้งความซับซ้อนได้
    • บางครั้งมองว่าการแก้ด้วยวิธีเรียบง่ายโดยไม่ต้องอธิบายรายละเอียดการ implement ทั้งหมดให้ project manager ฟังอาจดีกว่า

การจัดโครงสร้างโค้ดและการรีแฟกเตอร์

  • ไม่ควรแบ่งแอปพลิเคชันเร็วเกินไปในช่วงต้นของโปรเจกต์
    • ช่วงแรก รูปร่างของระบบยังไม่ชัดเจน และยังไม่เข้าใจอย่างสมบูรณ์ว่ากำลังสร้างอะไร
    • เมื่อเวลาผ่านไป จุดตัด (cut point) ที่ดีก็จะปรากฏออกมา
  • จุดตัดที่ดีมี interface กับส่วนที่เหลือของระบบแคบ
    • ซ่อนความซับซ้อนภายในด้วยฟังก์ชันหรือ abstraction จำนวนน้อย
    • Grug เปรียบสิ่งนี้กับการขังปีศาจแห่งความซับซ้อนไว้ในคริสตัล
  • เมื่อนักพัฒนาสมองใหญ่พยายามสร้าง abstraction จำนวนมากตั้งแต่ช่วงต้นของโปรเจกต์ ต้องใช้วิธีลดความเสียหาย
    • อาจเบี่ยงไปเป็นผลงานที่ไม่ทำร้ายโค้ดโดยตรง เช่น UML diagram
    • หากขอ “เดโมที่รันได้พรุ่งนี้” ก็จะได้เห็นโค้ดที่ทำงานจริงและความเป็นจริงเร็วขึ้น
    • แนวทางเดโมนี้อาจเรียกว่า โปรโตไทป์
  • การรีแฟกเตอร์มีประโยชน์หลังจากโค้ดเริ่มนิ่งในช่วงท้ายของโปรเจกต์
    • ยิ่งเป็นการรีแฟกเตอร์ขนาดใหญ่ โอกาสล้มเหลวยิ่งสูง
    • ชอบแนวทางทำเป็นขั้นเล็ก ๆ โดยรักษาให้ระบบยังทำงานอยู่ตลอด
    • end-to-end test เป็นเส้นชีวิตระหว่างรีแฟกเตอร์ แต่เมื่อมันพัง อาจเข้าใจสาเหตุได้ยาก
  • abstraction ที่มากเกินไปอาจนำไปสู่ความล้มเหลวของการรีแฟกเตอร์และของระบบ
    • J2EE ถูกยกเป็นตัวอย่างของ abstraction ที่มากเกินไป
    • การนำ OSGi มาใช้เป็นความพยายามลดความซับซ้อน แต่กลับสร้างความซับซ้อนที่รุนแรงกว่า และต้องใช้แรงงานแก้งานใหม่หลายคน-ปี

กลยุทธ์การทดสอบ

  • การทดสอบช่วยประหยัดเวลาได้มาก แต่ยังสงสัยกับแนวคิด “ต้อง test ก่อนเสมอ”
    • เมื่อยังไม่เข้าใจโดเมน ก็ยากที่จะรู้ว่าควรทดสอบอะไร
    • Grug ชอบเขียนการทดสอบส่วนใหญ่หลังจากโปรโตไทป์แล้ว เมื่อโค้ดเริ่มนิ่ง
  • แม้จะเขียนการทดสอบทีหลัง ก็ยังต้องมี วินัย
    • ห้ามข้ามการทดสอบด้วยเหตุผลว่า “เครื่องฉันรันได้”
    • ไม่มีหลักประกันว่ามันจะรันได้บนเครื่องอื่น หรือบนเครื่องเดิมในอนาคต
  • แยกบทบาทของ unit test, integration test และ end-to-end test
    • unit tests มีประโยชน์ในช่วงต้นของโปรเจกต์ แต่พังง่ายตามการเปลี่ยน implementation และอาจทำให้การรีแฟกเตอร์ยากขึ้น
    • end to end test แสดงพฤติกรรมของทั้งระบบ แต่เมื่อพังจะหาสาเหตุได้ยาก และหากพังบ่อยก็อาจถูกเมิน
    • integration tests สูงพอจะตรวจสอบความถูกต้องของระบบ และต่ำพอจะดูสาเหตุด้วย debugger ได้ จึงใกล้กับ “sweet spot”
  • รูปแบบที่เหมาะของชุดทดสอบมีดังนี้
    • ช่วงแรกมี unit test บางส่วน
    • เมื่อเกิดจุดตัดและระบบเสถียรมากขึ้น ให้มี integration test ที่แข็งแรง
    • มี end-to-end test ขนาดเล็กและดูแลง่าย ครอบคลุมเฉพาะฟีเจอร์ UI ที่พบบ่อยที่สุดและ edge case สำคัญ
  • ชอบใช้ mocking เฉพาะนาน ๆ ครั้ง และเมื่อจำเป็นก็ใช้เฉพาะในหน่วยใหญ่ เช่น ขอบเขตของระบบ
  • เมื่อแก้บั๊ก ให้ทำซ้ำบั๊กด้วย regression test ก่อน เป็นข้อยกเว้น แล้วค่อยแก้

ท่าทีต่อ process, Agile และโค้ดเดิม

  • Agile ไม่ใช่สิ่งที่แย่ที่สุด แต่ก็ยากจะมองว่าดีเสมอไป
    • อาจใช้จัดระเบียบทีมพัฒนาได้ในระดับหนึ่ง
    • ระวังผู้เชี่ยวชาญ Agile ที่ทุกครั้งที่ล้มเหลวจะพูดว่า “ไม่ได้ทำ Agile อย่างถูกต้อง”
  • สิ่งที่สำคัญต่อความสำเร็จมากกว่าคือโปรโตไทป์, เครื่องมือ และการจ้างนักพัฒนาที่ดี
    • Agile process อาจช่วยได้ แต่ถ้าจริงจังกับมันมากเกินไปก็อาจเป็นโทษ
    • no silver club คือไม่มีกระสุนเงินที่แก้ปัญหาซอฟต์แวร์ทั้งหมดได้
  • Chesterton’s Fence ถูกใช้เป็นคำเตือนเรื่องการลบโค้ดเดิม
    • หากไม่รู้ว่ารั้วมีไว้ทำอะไร ต้องเข้าใจก่อน และไม่ควรรื้อทิ้งทันที
    • แม้โค้ดจะดูน่าเกลียด แต่ถ้าวันนี้มันยังทำงานอยู่ ก็ควรให้ความเคารพ
    • โดยเฉพาะระบบขนาดใหญ่ ยิ่งควรเข้าใจก่อนแล้วค่อยปรับปรุง
  • การทดสอบอาจเป็นเบาะแสที่บอกได้ว่า “รั้ว” บางอย่างมีอยู่เพราะอะไร

เครื่องมือและ type system

  • เครื่องมือช่วยลดภาระที่นักพัฒนาต้องจำและใช้เหตุผลเอง
    • เมื่อเข้าสู่สภาพแวดล้อมใหม่ การใช้เวลาเรียนรู้เครื่องมือรอบตัวอาจช่วยเพิ่ม productivity ได้
    • หากไม่มีเอกสาร อาจต้องถามนักพัฒนาคนอื่นเพื่อทำความเข้าใจ
  • code completion ของ IDE ช่วยให้ไม่ต้องจำ API ทั้งหมด
    • กล่าวว่าการเขียนโปรแกรม Java แทบเป็นไปไม่ได้หากไม่มี code completion
  • debugger ที่ดีมีความสำคัญมาก
    • ควรเรียนรู้ฟีเจอร์อย่าง conditional breakpoint, การประเมิน expression และการไล่ดู stack ให้ลึก
    • มองว่าการเรียน debugger สำหรับนักพัฒนาใหม่อาจสอนเรื่องคอมพิวเตอร์ได้มากกว่าคลาสมหาวิทยาลัย
  • คุณค่าที่ใหญ่ที่สุดของ type system คือ “เมื่อกดจุด (.) แล้วมีสิ่งที่ทำได้โผล่ขึ้นมา”
    • ความถูกต้องของ type ก็ดี แต่สำหรับ Grug แล้ว การสนับสนุนจากเครื่องมือและ code completion มีค่ามากกว่า
    • type abstraction หรือ generics ที่มากเกินไปอาจทำให้โค้ดงานจริงยากขึ้น
    • ชอบจำกัดการใช้ generics ไว้กับ container class เป็นหลัก

expression, DRY, separation of concerns

  • ชอบ โค้ดที่ debug ง่าย มากกว่าโค้ดสั้น
    • แทนที่จะเขียนเงื่อนไขซับซ้อนในบรรทัดเดียว การแยกเป็นตัวแปรกลางช่วยให้ดูผลและความหมายของแต่ละ expression ได้ง่ายขึ้น
    • แม้จำนวนบรรทัดจะเพิ่มขึ้น แต่การเข้าใจและ debug เงื่อนไขจะง่ายขึ้น
  • DRY เป็นคำแนะนำที่ดี แต่ต้องมีสมดุล
    • โค้ดซ้ำที่เรียบง่ายและชัดเจนอาจดีกว่า callback, closure หรือ object model ที่ซับซ้อน
    • หากพยายามกำจัดความซ้ำแล้วความซับซ้อนเพิ่มขึ้น ก็อาจเสียมากกว่าได้
  • มอง Separation of Concern ในเชิงวิจารณ์มากกว่า
    • ตัวอย่างคลาสสิกของเว็บดีเวลลอปเมนต์คือการแยก CSS, HTML และ JavaScript
    • Grug ชอบ locality of behavior เป็นทางเลือก
    • หากวางโค้ดที่เกี่ยวข้องไว้ใกล้กับสิ่งที่มันทำงานด้วย เมื่อดูสิ่งนั้นก็จะรู้ทันทีว่ามันทำอะไร
  • closure มีประโยชน์กับการใช้งานที่ถูกต้อง เช่น abstraction ของงานบน collection
    • แต่เหมือนเกลือ, type system และ generics คือใช้นิดหน่อยก็พอ มากเกินไปเป็นโทษ
    • “callback hell” ของ JavaScript ถูกยกเป็นตัวอย่างของการใช้ closure เกินขอบเขต

logging, concurrency, optimization

  • logging สำคัญมาก โดยเฉพาะในสภาพแวดล้อม deploy บน cloud
    • บันทึก log ในทุกจุดแตกแขนงสำคัญของ logic
    • หาก request กระจายข้ามหลายเครื่อง ให้ใส่ request ID ใน log ทั้งหมดเพื่อให้เชื่อมโยงกันได้
    • หากทำได้ ให้ปรับ log level แบบ dynamic
    • หากทำได้ ให้ปรับ log level รายผู้ใช้ได้ด้วย
  • ไลบรารี logging ของ Java อาจซับซ้อน แต่หากลงทุนกับ logging infrastructure อย่างเหมาะสม ภายหลังจะได้ผลตอบแทนสูง
  • concurrency เป็นสิ่งที่ควรกลัว
    • ใช้โมเดลที่เรียบง่ายเท่าที่ทำได้ เช่น web request handler แบบ stateless และคิวงานระยะไกลที่เป็นอิสระ
    • มองว่า optimistic concurrency ทำงานได้ดีในโลกเว็บ
    • thread local variable ใช้บ้างเป็นครั้งคราว โดยหลักเมื่อเขียนโค้ด framework
    • แม้แต่โครงสร้างข้อมูล concurrency อย่าง ConcurrentHashMap ของ Java ก็ยังต้องใช้ด้วยความระมัดระวัง
  • optimization ควรเริ่มเมื่อมี performance profile จริง
    • เห็นด้วยกับ “premature optimization is the root of all evil”
    • คอขวดจริงอาจไม่ตรงกับที่คาดไว้
    • ไม่ควรมองแค่ CPU เพราะ network call อาจเทียบเท่ากับ CPU หลายล้าน cycle จึงควรลดให้มากที่สุดเท่าที่ทำได้

API, parsing, visitor pattern

  • API ที่ดีทำให้นักพัฒนาไม่ต้องคิดมาก
    • API ที่แย่มักเกิดจากการออกแบบโดยยึดภายใน implementation หรือ domain model เป็นศูนย์กลาง หรือ abstract เกินไป
    • ชอบ การทำเป็นชั้น (layering) ที่ให้ API เรียบง่ายสำหรับกรณีง่าย ๆ และ API ที่ซับซ้อนขึ้นสำหรับกรณีซับซ้อน
  • มองว่า API แบบ object-oriented ควรผูกฟังก์ชันไว้กับ object เป้าหมาย
    • ยก flow ใน Java ที่ต้องแปลง list เป็น Stream เพื่อ filter แล้ว collect กลับเป็น List เป็นตัวอย่างที่ไม่ดี
    • มองว่างานที่พบบ่อยอย่าง filter() ควรอยู่บน list และคืนค่าเป็น list
  • parser แบบ recursive descent เป็นวิธีที่สนุกและงดงาม
    • วิจารณ์ parser generator ว่าเข้าใจยากและ debug ยาก
    • อ้างว่า parser สำหรับ production จริงแทบทั้งหมดเป็นแบบ recursive descent
  • แนะนำ Crafting Interpreters ของ Bob Nystrom
    • อ่านออนไลน์ได้ฟรี แต่ก็แนะนำให้ซื้อหนังสือด้วย
    • อย่างไรก็ตาม กล่าวถึง visitor pattern ว่าเป็นกับดัก
  • การประเมิน Visitor pattern สั้น ๆ คือ “bad”

frontend, microservices และกระแส

  • microservices อันตรายตรงที่ “เพิ่ม network call เข้าไปในปัญหาที่ยากที่สุดอย่างการแบ่งระบบให้ถูกต้อง”
  • ในการพัฒนา frontend ความซับซ้อนรุนแรงเป็นพิเศษ
    • วิจารณ์สถานการณ์ที่แม้แต่การบันทึกฟอร์มง่าย ๆ หรือเว็บไซต์โบรชัวร์ก็ยังใช้ SPA library, GraphQL JSON API และ HTTP backend
    • กล่าวว่าหากแยก frontend กับ backend ก็จะมีที่อยู่ของความซับซ้อนสองแห่ง
  • Grug ระบุว่าเขาสร้าง htmx และ hyperscript เพื่อลดความซับซ้อน
    • ชอบแนวทางรักษา HTML ที่เรียบง่ายและไม่ใช้ JavaScript มาก
    • ยอมรับว่า React อาจดีกว่าสำหรับการหางานและแอปพลิเคชันบางประเภท
  • การพัฒนามีกระแสมากมาย โดยเฉพาะใน frontend
    • กล่าวว่าฝั่ง backend น่าเบื่อมากขึ้น และไอเดียแย่ ๆ จำนวนมากก็ถูกลองมาแล้ว
    • ให้เข้าหาแนวทางใหม่ที่ปฏิวัติวงการด้วยความระมัดระวังเหมือนโรยเกลือเม็ดเดียว
    • ไอเดียจำนวนมากเคยถูกลองมาแล้วอย่างน้อยครั้งหนึ่ง และไอเดียแย่ ๆ ที่ถูกรีไซเคิลอาจทำให้เสียเวลาได้

ความกลัวและ imposter syndrome

  • การที่ senior developer พูดต่อหน้าคนอื่นว่า “อันนี้ซับซ้อนเกินไป” เป็นสิ่งที่ดี
    • นักพัฒนามักพูดยากว่าตนไม่เข้าใจ เพราะ Fear Of Looking Dumb(FOLD)
    • หาก senior ยอมรับก่อน junior ก็สามารถพูดถึงความซับซ้อนและความไม่เข้าใจได้เช่นกัน
  • FOLD เป็นแหล่งพลังหลักที่ทำให้ความซับซ้อนมีอำนาจ โดยเฉพาะกับนักพัฒนารุ่นใหม่
    • มองว่าอารมณ์ขันและท่าทีที่จดจำความล้มเหลวในอดีตช่วยได้
  • imposter syndrome ก็พบได้ทั่วไปในการพัฒนา
    • Grug อธิบายว่าตนอยู่ระหว่างความรู้สึกว่าควบคุมทุกอย่างได้กับความรู้สึกว่าไม่รู้ว่ากำลังทำอะไรอยู่
    • แม้ htmx และ _hyperscript จะมี ผลงานโอเพนซอร์ส แต่ก็ยังกลัวความผิดพลาดและความล้มเหลว
  • หากทุกคนรู้สึกถึง imposter syndrome ก็ควรยอมรับในทำนองว่าไม่มีใครเป็นตัวปลอมจริง ๆ

สิ่งที่แนะนำให้อ่านและบทสรุป

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

 
GN⁺ 2025-06-18
ความคิดเห็นจาก Hacker News
  • ถ้าอาจารย์ Carson กำลังอ่านคอมเมนต์นี้อยู่ อยากขอขอบคุณจากใจจริงสำหรับทุกสิ่งที่ท่านได้สร้างคุณูปการมาตลอด
    ตอนเรียนมหาวิทยาลัยผมไม่เข้าใจว่าทำไมเราถึงเรียน HTMX หรือทำไมอาจารย์ถึงตื่นเต้นกับมันนัก แต่พอผ่านไปหลายปีตอนนี้ก็เข้าใจแล้วว่า HTML over the wire นั่นแหละคือทั้งหมด
    ผมทำงานเป็น Staff Ruby on Rails Engineer และได้เห็นผลงานของอาจารย์ใน Hotwire อีกทั้งยังรู้สึกยอดเยี่ยมทุกครั้งที่เห็นอาจารย์โผล่มาใน Hacker News เป็นครั้งคราว หรือพูดคุยกับนักพัฒนา Hotwire บน GitHub ท่านเป็นเหมือนแสงสว่างของชุมชนโปรแกรมมิงที่ได้รับทั้งความเคารพและความขอบคุณอย่างยิ่ง

  • เห็นด้วยกับประโยคที่ว่า “ดีบักเกอร์ที่ดีมีค่าเท่ากับก้อนหินแวววาว หรือจริง ๆ แล้วมีค่ามากกว่านั้น”
    ผมผ่านมาทั้งสตาร์ตอัปเล็ก ๆ และทีมบิ๊กเทค ‘ระดับหัวกะทิ’ แต่เกือบทุกครั้งผมเป็นคนเดียวในทีมที่ใช้ ดีบักเกอร์ ดูเหมือนว่าในโลกความเป็นจริง อย่างน้อยในสายเว็บเทคโนโลยี คนส่วนใหญ่ยังดีบักด้วย print กัน
    การหยุดที่บรรทัดโค้ดที่น่าสนใจระหว่างรันเทสต์ แล้วดู call stack ที่พามาถึงจุดนั้น ง่ายกว่าการพยายามจำลองการทำงานของโค้ดไปข้างหน้าในหัวมาก สำหรับ grug รุ่นเยาว์ ทักษะนี้เหมือนพลังพิเศษเล็ก ๆ และถ้าทำได้ก็คุ้มที่จะลงทุนเวลาเพื่อทำให้มันใช้ได้กับโค้ดเบสของตัวเอง

    • มีการถกเถียงที่ดีเกี่ยวกับเรื่องนี้เมื่อหลายปีก่อน และคอมเมนต์บนสุดได้ยกคำพูดของ Brian Kernighan กับ Rob Pike มาแชร์ ซึ่งทั้งคู่คงเรียกว่าเป็น grug รุ่นเยาว์ได้ยาก
      พวกเขาบอกว่าไม่ค่อยใช้ดีบักเกอร์ในระดับที่ลึกไปกว่าการดู stack trace หรือค่าแปรหนึ่งสองตัว เพราะมันทำให้หลงไปกับรายละเอียดของโครงสร้างข้อมูลและการไหลควบคุมที่ซับซ้อนได้ง่าย และมองว่าการใส่คำสั่งพิมพ์ค่าและโค้ดตรวจสอบตัวเองไว้ตรงจุดสำคัญมีประสิทธิผลมากกว่า
      ผมเองก็เห็นด้วยเป็นส่วนใหญ่ สำหรับงานแทบทุกอย่างที่ผมทำ วงจรแบบ ตั้งสมมติฐาน-เขียนล็อก-รัน พาไปถึงคำตอบได้เร็วกว่ามาก ไม่ใช่ว่าผมกำลังพยายามรันโค้ดในหัว แต่เป็นเพราะผมมีแบบจำลองการทำงานของโค้ดอยู่แล้ว และถ้าแบบจำลองนั้นถูกต้อง ผมก็จะรู้ว่าควรได้เอาต์พุตแบบไหน และสามารถจับความผิดปกติจากเอาต์พุตที่ไม่ตรงได้อย่างรวดเร็ว
      [0] The unreasonable effectiveness of print debugging (349 points, 354 comments) April 2021 https://news.ycombinator.com/item?id=26925570
    • ผมก็อยากใช้ดีบักเกอร์จริง ๆ แต่จากมุมของคนที่ทำงานมาแต่ในบริษัทใหญ่ มันไม่ใช่ตัวเลือกเลย
      ในสถาปัตยกรรมแบบ microservice mesh แค่จะรันอะไรให้ครบในเครื่องตัวเองยังยาก และสภาพแวดล้อมสำหรับเทสต์ก็มักไม่ได้ตั้งค่าให้ต่อกับดีบักเกอร์แบบ step-through ได้ สุดท้ายก็เหลือแค่ดีบักด้วย print และถ้าระบบล็อกเองมีปัญหา หรือโปรแกรมตายก่อนจะ flush log ก็ใช้ไม่ได้แม้แต่วิธีนั้น
    • เวลาเข้าไปในโค้ดเบส สิ่งแรก ๆ ที่ผมทำอย่างหนึ่งคือจัดเตรียม สภาพแวดล้อม IDE/เอดิเตอร์ ให้สามารถรันโปรแกรมด้วยดีบักเกอร์ได้อย่างรวดเร็ว แม้ตอนนั้นยังไม่ได้กำลังแก้ปัญหาอะไรเฉพาะหน้า
      อีกไม่นานมันจะจำเป็นขึ้นมาเอง แต่ที่ทำให้ผมอึ้งคือแม้ตอนทำงานร่วมกับคนอื่น ก็ยังเจอคนที่ไม่รู้แม้แต่วิธีใช้ดีบักเกอร์ พอพูดว่า “ตั้ง breakpoint ตรงนั้น” “ตอนนี้ step into เข้าไปในฟังก์ชันแล้วดูสถานะตัวแปร” “อันนั้นข้ามไป” ก็ได้แต่สีหน้างงกลับมาทุกครั้ง
    • ผมเองก็ออกไปทางไม่ค่อยใช้ดีบักเกอร์เหมือนกัน
      ถ้าจะพูดให้เป๊ะ ผมไม่ได้ใส่ print แล้วลบทิ้ง แต่จะใส่ โค้ด logging ที่ตั้งใจให้คงอยู่ต่อไปแทน ถ้าเป็นอินเทอร์เฟซหลัก ปกติผมจะเริ่มจากบันทึกการเข้า/ออกของฟังก์ชันและค่าพารามิเตอร์ที่ระดับ INFO แล้วพอใช้งานระบบไปและเห็นจุดที่ควรส่องเพิ่ม ก็จะเติมล็อกที่ละเอียดขึ้น
      ผมยังใส่ใจกับรูปแบบของล็อกพอสมควรด้วย ตอนทำงานกับระบบกระจาย การจัด prefix ของแต่ละบรรทัดล็อกให้ตรงเป๊ะมีประโยชน์มาก ผมจะใส่ node ID, pid และ timestamp แบบความกว้างคงที่ทั้งหมด เพื่อให้เมื่อดาวน์โหลดล็อกทั้งคลัสเตอร์มาเรียงลำดับแล้ว จะเห็นการทำงานของหลายโหนดสลับกันอยู่ในไฟล์เดียว
    • การใช้ดีบักเกอร์กับโค้ดของตัวเองนั้นง่ายและผมชอบมาก
      แต่พอดีบักเกอร์ไหลลึกเข้าไปใน ไลบรารีหรือเฟรมเวิร์ก ที่ผมใช้อยู่เมื่อไร ผมจะหลงทางและเริ่มไม่ชอบทันที เฟรมเวิร์กหรือไลบรารีพวกนั้นเป็นสิ่งที่สะสมแรงงานนับหลายหมื่นชั่วโมงคนไว้ จนให้ความรู้สึกว่าเกินระดับของผมไปมาก
  • มีประโยคดี ๆ ราวกับอัญมณีอยู่หลายประโยค แต่สำหรับ microservices ผมชอบประโยคนี้ที่สุด: “grug สงสัยว่าทำไมคนฉลาดถึงเอาปัญหาที่ยากที่สุดในการแยกระบบให้ถูกต้อง มาใส่ network call เพิ่มเข้าไปอีก”

    • พยายามอธิบายเรื่องนี้กับทีมพัฒนาเล็ก ๆ ที่มีแค่ 1~2 คนอยู่เรื่อย ๆ
      ยอมแยกเว็บแอปจิ๊บจ๊อยที่มีฟอร์มสักห้าฟอร์มออกเป็น “microservices”, ทำให้มันแชร์ฐานข้อมูลเดียวกัน, แปะทั้งชั้นจัดการ API, คิวสำหรับงาน batch ปริมาณมากระดับเมกะไบต์, ระบบแจ้งเตือนอีเมล, ไปจนถึงแพลตฟอร์ม observability ที่ทำเอง แล้วก็เปลี่ยนเว็บฟอร์มเรียบ ๆ ให้เป็น SPA พร้อมบอกว่า “แบบนี้ง่ายกว่า”
      ตอนนี้เข้าใจแล้วว่า “architecture” กับ “patterns” เป็นโครงการสร้างงานให้กับนักพัฒนาที่ไร้ประโยชน์ ไม่งั้นก็คงต้องไปยืนถือป้ายข้างถนนว่า “ให้แซนด์วิชแล้วจะเขียน JavaScript ให้”
    • ดูเหมือนบางคนจะไม่รู้จักวิธีอื่นในการแยกระบบออกเป็นส่วนที่เล็กลง
      สำหรับคนแบบนั้น ถ้าไม่ถูกเปิดออกมาผ่าน API call มันก็เป็นแค่ก้อนโค้ดทึบ ๆ ที่ทั้งเข้าใจไม่ได้และนำกลับมาใช้ซ้ำไม่ได้
    • network boundary ให้เครื่องมือสำหรับการแยกส่วนที่ระบบโมดูลของภาษาส่วนใหญ่ไม่มี
      หลายแพ็กเกจสามารถทำงานร่วมกันภายในได้ แต่เปิดเผยแค่ API เล็ก ๆ ให้ส่วนอื่นของ codebase เห็น ความเป็น network บังคับให้โมดูลแลกเปลี่ยนกันด้วยข้อมูลเท่านั้น ไม่ใช่ callback หรือพฤติกรรม และกดดันให้อินเทอร์เฟซพัฒนาไปแบบรองรับย้อนหลังได้ จึงช่วยให้ “hot reload” โมดูลต่างเวลากันได้โดยไม่ระเบิด
      ดูเหมือนว่าสิ่งเหล่านี้ส่วนใหญ่จะได้มาโดยไม่ต้องมี network hop จริง ๆ แต่ยังไม่เคยเห็นความพยายามที่จริงจัง
    • มีทฤษฎีสมคบคิดว่า microservices เป็น pattern ที่บริษัทคลาวด์ผลักดัน เพื่อให้คนสร้างแอปแบบนี้
      มันรันไม่ได้ถ้าไม่มี orchestrator อย่าง K8S, และติดตั้งกับดูแลยาก จึงเอื้อต่อการขาย managed cloud มันทำให้ใช้ network bandwidth กับ CPU มากขึ้น ซึ่งทั้งสองอย่างคิดเงินได้
      มันทำให้การแชร์และคงสภาพ state ที่ซับซ้อนหรือขนาดใหญ่ภายในแอปทำได้ยาก จนต้องใช้บริการฐานข้อมูลหรือ event queue แบบ managed เป็นตัวแทน ถ้าเป็น monolith ก็แค่ใช้คิวหรือ channel ได้ แต่ถ้าเป็น microservices ก็จะอยากได้สัตว์ประหลาดขนาดยักษ์อย่าง Kafka
      การรันบนเครื่อง local ก็ยากขึ้นจนต้องมีสภาพแวดล้อมพัฒนาบนคลาวด์ และอาจต้องมีหลายชุดทั้ง dev/test environment อีกทั้งยังไปพึ่งพาคุณลักษณะเฉพาะอย่างวิธีทำ networking ของคลาวด์เจ้าใดเจ้าหนึ่ง ทำให้ cloud lock-in หนักขึ้น
      ไม่รู้ว่ายังจำยุคที่คลาวด์ถูกขายว่าเป็นวิธีลดต้นทุน IT ได้ไหม มันตลกมาก และผมก็รู้ตั้งแต่ยุค 2000 แล้วว่ามันเป็นเรื่องเหลวไหล สุดท้ายก็จะยิ่งทำให้ต้นทุนทุกอย่างสูงขึ้น
    • มองว่านี่มีไว้เพื่อแบ่งระบบระหว่างทีมเป็นหลัก
      ใช้วิธีนี้เพราะจัดการง่ายกว่า และมันใกล้เคียงกับ วิธีจัดองค์กรพัฒนา มากกว่าจะเป็นการตัดสินใจทางเทคนิค ถ้าทางเลือกคือ monorepo ส่วนตัวผมคิดว่าอันนั้นแย่กว่าอีก
  • “ถ้าให้เลือกระหว่างความซับซ้อนกับการสู้ตัวต่อตัวกับไทรันโนซอรัส grug จะเลือกไทรันโนซอรัส อย่างน้อย grug ก็ยังมองเห็นไทรันโนซอรัส”
    ผมนึกถึงประโยคนี้อย่างน้อยสัปดาห์ละครั้ง

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

    • อันนี้เข้ากับมีม bell curve เป๊ะ
      นักพัฒนามือใหม่เขียนโค้ดเรียบง่าย, นักพัฒนาระดับกลางเขียนโค้ดซับซ้อน, แล้วนักพัฒนาผู้เชี่ยวชาญก็กลับมาเขียน โค้ดเรียบง่าย อีกครั้ง
    • เวลาและสถานที่ที่จะใช้ความประณีตกับ abstraction คือเมื่อมันไม่ได้ทำให้เข้าใจได้ด้วยวิธีที่ต้องไปฟังเลกเชอร์พิเศษก่อน แต่ทำให้ เข้าใจโค้ดได้ง่ายขึ้นจริง ๆ
      จะสมมติได้แค่ไหนว่าคนอ่านเคยผ่านเลกเชอร์บางอย่างมาแล้ว ย่อมต่างกันไปตามสถานการณ์
    • “ทุกสิ่งควรถูกทำให้เรียบง่ายที่สุดเท่าที่จะเป็นไปได้ แต่ไม่ควรเรียบง่ายไปกว่านั้น”
  • หนึ่งในความย้อนแย้งของการพัฒนาซอฟต์แวร์สมัยใหม่คือ เรานำความซับซ้อนเข้ามาเพราะคิดว่า “สุดท้ายแล้วมันจะช่วยประหยัดเวลา”
    บางครั้งก็จริงและช่วยประหยัดเวลาได้จริง แต่ก็ไม่ใช่เสมอไป และอาจไม่ได้เกิดขึ้นบ่อยด้วย
    DRY บางครั้งนำไปสู่การทำ abstraction เร็วเกินไป ทันทีที่คิดว่า “แพตเทิร์นนี้น่าจะถูกใช้ที่อื่นด้วย งั้นต้องดึงส่วนที่ใช้ร่วมกันออกมา” ปีศาจแห่งความซับซ้อนก็เข้ามา
    เราอยากจับบั๊กให้ได้มากที่สุดตั้งแต่ตอน compile แต่ถ้าจะทำแบบนั้น compiler ก็ต้องรู้มากขึ้นว่าเราตั้งใจจะทำอะไรจริง ๆ และนั่นก็ทำให้ต้องสร้าง type ที่ซับซ้อนจนบั่นทอนความเข้าใจ
    เราสร้าง macro หรือ DSL ที่ซับซ้อนเพื่อเลี่ยง boilerplate แต่เพราะ กฎของ abstraction ที่รั่ว พอถึงเวลาที่ต้องรู้ implementation จริง หัวก็แทบระเบิด
    ความยากคือทุกตัวอย่างเหล่านี้บางครั้งก็เป็นความคิดที่ดี ผมมองว่าความสามารถในการตัดสินว่าเมื่อไรควรเพิ่มความซับซ้อนแล้วมันจะทำให้เรียบง่ายขึ้น คือสัญญาณของวิศวกรซอฟต์แวร์ที่เก่ง

    • จากประสบการณ์ ผมพบว่าคำขวัญ SPoT (single point of truth, แหล่งความจริงเพียงจุดเดียว) ดีกว่า DRY
      โดยอุดมคติแล้ว business logic ควรถูกนิยามไว้เพียงที่เดียว แต่สิ่งอื่น ๆ ถ้าจำเป็นจะซ้ำกันบ้างก็ได้ และตัวมันเองไม่ได้เลวร้าย
      ยังเน้น “กฎของ 3” เพื่อควบคุม DRY ด้วย โค้ดแบบคัดลอก/วางยังพอรับได้จนถึงสามครั้ง หลังจากนั้นค่อยเริ่มคิดเรื่อง abstraction แน่นอนว่าไม่มีหลักคร่าว ๆ ที่ใช้ได้กับทุกกรณี และเซนส์แบบนี้ก็สอนกันได้ยาก
    • โดยเฉลี่ยแล้วผมยังเชื่อว่าโค้ดส่วนใหญ่ยัง DRY ไม่พอ แต่ในโปรเจกต์ส่วนตัวช่วงหลังผมตั้งหลักไว้ว่า “ไม่มีแอปพลิเคชัน มีแต่ หน้าจอ” ที่น่าสนใจคือกำลังใช้ HTMX ซึ่งดูเหมือนจะเป็นสิ่งที่ผู้เขียนบล็อกคนนี้สร้างขึ้น
      ปกติผมสร้างเว็บแอปด้วยเฟรมเวิร์กสาย Sinatra อย่าง Flask แล้วเขียนฟังก์ชันที่ตอบสนองต่อแพตเทิร์นของ URL หนึ่ง “หน้าจอ” อาจประกอบด้วยฟังก์ชันตั้งแต่หนึ่งตัวขึ้นไปที่ทำงานร่วมกัน พร้อมด้วย HTML template ที่เกี่ยวข้อง
      ถ้าตัวแอปพลิเคชันตั้งค่าพวกการเชื่อมต่อฐานข้อมูล ตำแหน่งไฟล์ HTML header/footer แล้ว นอกนั้นแทบไม่มีการ coupling ระหว่างหน้าจอต่าง ๆ เลย ถ้าต้องการหน้าจอใหม่ ก็แค่คัดลอกหน้าจอเดิมมาแก้ หรือให้ LLM สร้างหน้าจอหรือ endpoint ให้ก็ได้ ถ้าผลลัพธ์ไม่ดีก็ทำใหม่
      ที่ทำงานเก่าเคยสร้างเฟรมเวิร์กชื่อ Themis สำหรับทำชุดฝึก ML โดยใช้ microservices, React, Docker ฯลฯ ความต้องการจริงคือการเพิ่มงานใหม่อย่างต่อเนื่อง และทำหน้าจอที่เรียบง่ายแต่ปรับจูนอย่างมากสำหรับแต่ละงานให้ได้เร็ว ถ้าต้องตัดสินใจ 20,000 ครั้ง การต้องคลิกครั้งเดียวต่อรอบก็ยังลำบาก แต่ถ้าต้องคลิกสี่ครั้งก็กลายเป็น 80,000 ครั้งจนเลิกทำไปเลย
      ในสถาปัตยกรรมตอนนั้น เราต้องเขียน API endpoint ของแอปพลิเคชัน JAXB และคอมโพเนนต์ของแอป React แบบ monolithic แล้วรอให้ TypeScript, Docker, javac รันอยู่ 20 นาที โชคดีก็บูตขึ้นมา ไม่งั้นก็เริ่มใหม่ตั้งแต่ต้น
      ผมเขียนคำวิจารณ์ Themis และออกแบบ Nemesis โดยตั้งเป้าให้พัฒนางานใหม่ได้รวดเร็ว แต่ที่ทำงานเก่าไม่ได้เลือกเส้นทางนี้ ถึงอย่างนั้น Nemesis กับผมก็จัดการ task instance ไปแล้วหลายล้านรายการนับแต่นั้น
    • มีสติกเกอร์หยินหยางติดอยู่บนโน้ตบุ๊ก โดยฝั่งหยินเขียนว่า DRY ส่วนฝั่งหยางเขียนว่า YAGNI
    • DRY ไม่ได้อันตรายขนาดนั้น
      มันไม่ได้หมายความว่าต้องดึง helper ที่ถูกใช้แค่ที่เดียวออกมา ถ้า logic จำนวนมากกองอยู่ในฟังก์ชันเดียว คลาสเดียว ไฟล์เดียว ฯลฯ แต่ไม่ได้ถูกคัดลอกมา ก็ยังถือว่า DRY อยู่
      การทำ abstraction เร็วเกินไปมีอยู่จริง และการที่วิชา CS มักสอนให้ทำแบบนั้นก็ไม่ได้ช่วยอะไร ถ้าให้ฐานข้อมูล MySQL กับมือใหม่ เขาอาจพยายามทำ abstraction ครอบ MySQL เป็นอย่างแรก
    • grug บางทีก็ใช้เวลา 100 ชั่วโมงสร้างเครื่องจักรให้ทำงานแทน ทั้งที่ทำด้วยมือใช้เวลาแค่ 1 ชั่วโมงก็เสร็จ หรืออาจใช้เวลา 1 ชั่วโมงสร้างเครื่องจักร แล้วเสียเวลาอีก 100 ชั่วโมงไปกับการซ่อมเครื่องงี่เง่านั้น
      ไม่ว่าจะซับซ้อนหรือเรียบง่าย ถ้าผลลัพธ์ ไม่ได้เพิ่มคุณค่า มันก็ไม่สำคัญ ควรโฟกัสที่การเพิ่มคุณค่าให้มากขึ้นก่อน มากกว่ากังวลเรื่องการลดทอนอะไร และค่อยไปกังวลเรื่องความซับซ้อนทีหลัง
  • หนึ่งในการใช้ LLM ที่ผมชอบคือเอา essay นี้ใส่เข้าไป แล้วขอให้มันคอมเมนต์ประเด็นที่ผมกำลังเจออยู่ใน persona แบบ grug-brained developer มันช่วยระบายความเครียดได้ดี

    • ผมยังไม่คุ้นกับ LLM เท่าไร แต่ฟังดูเจ๋งดี อยากรู้ว่าการ “ป้อน” essay นี้เข้าไปทำยังไง
      คือแค่เริ่ม prompt ว่า “ช่วยทำตัวเหมือน Grug Brained Developer จาก essay นี้” อะไรประมาณนั้นใช่ไหม
  • “ความซับซ้อนแย่มาก” เป็นคำพูดที่จริงมาก
    ตลอดหลายปีที่ทำงานด้านวิศวกรรมซอฟต์แวร์ แนวคิดนี้เป็นหนึ่งในไม่กี่หลักการที่พิสูจน์ตัวเองว่าเป็นจริงอย่างสม่ำเสมอในแทบทุกสถานการณ์ ปัญหาบางอย่างซับซ้อนโดยเนื้อแท้ แต่ถึงอย่างนั้น การใช้เวลาเพื่อไปให้ถึงวิธีแก้ที่เรียบง่ายที่สุดก็ดีกว่ามาก
    งานที่ได้ผลดีที่สุดของฉันมักเกิดขึ้นหลังจากตั้งคำถามกับแนวทางเดิมและกล้าตัดทอนให้เรียบง่ายลง อาจเสียความยืดหยุ่นที่เป็นไปได้ไปบ้าง แต่ส่วนใหญ่แล้วก็ไม่จำเป็นต้องใช้ความยืดหยุ่นมากอย่างที่คิดไว้
    ตัวอย่างเช่น หลังจากมี LLM ที่ค่อนข้างดีและทำงานแบบ agentic ได้แล้ว ผมก็หลีกเลี่ยง TypeScript type ที่ซับซ้อนเกินไป เปราะบาง และดีบักยาก แล้วเขียนโค้ดลักษณะคล้ายสเปกขึ้นมา จากนั้นให้ LLM สร้างโค้ดจริงจากพื้นฐานนั้นแบบ static
    dependency ของโปรเจกต์ที่เป็น ESLint พังอยู่เรื่อยหลังอัปเดตเวอร์ชัน หลายกฎก็ไม่ละเอียดพอจะหลีกเลี่ยง false positive และการดูแลให้มันทำงานร่วมกับ TypeScript และ VSCode อย่างถูกต้องก็ซับซ้อนมาก พอเปลี่ยนไปใช้ Biome.js ก็ง่ายกว่าและมีประสิทธิภาพเพียงพอ แต่ช่วงหลังก็เริ่มมีบั๊กอยู่ดี ถึงอย่างนั้นก็ทำให้ตระหนักว่า linting เป็นสิ่งที่มีก็ดี ไม่ใช่สิ่งที่ควรใช้เวลามากเกินไปในการคอยดูแล เลยเอามันออกจาก build toolchain และใน VSCode ก็ไม่จำเป็นต้องเปิดทิ้งไว้ตลอดอีกต่อไป แค่รัน Biome เป็นครั้งคราวเพื่อตรวจสอบ code style และ format ก็พอ
    ตอนทำเครื่องมือย้ายข้อมูลแบบกำหนดเองสำหรับโปรเจกต์ ผมมองว่า migration แบบไปข้างหน้าจำเป็น แต่ migration ย้อนกลับ ไม่คุ้มกับเวลาและความซับซ้อนที่จะต้องลงมือทำ ถ้าจำเป็นต้อง rollback ฐานข้อมูลที่มีข้อมูลอยู่ ก็ restore backup ได้เลย และถ้าไม่มีข้อมูลหรือไม่ใช่ production DB ก็เริ่มใหม่จากสถานะสะอาดด้วยสคริปต์ initialisation ที่ใส่เวอร์ชันไว้ได้

    • สองตัวอย่างแรกดูเหมือนแค่ใช้เครื่องมืออื่นมาซ่อนความซับซ้อนไว้หรือเปล่า
      ส่วนตัวอย่างที่สามก็ยังไม่ค่อยเข้าใจว่ามันง่ายกว่าตรงไหน ถ้ามองแบบคณิตศาสตร์ก็สร้างพื้นที่แบบ bijection ได้ไม่ยาก การจำลอง migration ย้อนกลับด้วยวิธีอื่นอาจยากกว่าเสียอีก แน่นอนว่ามันขึ้นอยู่กับรายละเอียด เลยไม่ใช่กฎทั่วไป
    • หลายคนพูดถึงความซับซ้อน แต่แทบไม่มีใครอธิบายว่าความซับซ้อนหมายถึงอะไร
      Rich ผู้ชาญฉลาดเคยบอกว่า complect คือการเอาสิ่งต่าง ๆ มาผูกเข้าด้วยกัน ซึ่งผมเห็นด้วย Rich บอกว่าความซับซ้อนเป็นสิ่งไม่ดี แต่ผมไม่เห็นด้วย การผูกสิ่งต่าง ๆ เข้าด้วยกันเป็นเรื่องจำเป็น ถ้าไม่มีความเชื่อมโยงกันเลยก็แก้ปัญหาไม่ได้
  • ไม่อยากเชื่อเลยว่าบทความนี้เขียนตั้งแต่ปี 2022
    เหมือนถ้าอ่านเมื่อ 10 ปีก่อนก็คงพูดได้อย่างมั่นใจว่าตอนนั้นมันก็เป็นงานคลาสสิกไปแล้ว

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