Reinventing the Wheel
เขียนเองสิ่งที่มีของสำเร็จรูปดี ๆ อยู่แล้ว
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Reinventing the Wheel คือแอนตี้ pattern ที่ทีมหรือองค์กร สร้างโซลูชันขึ้นเองใหม่ทั้งหมด สำหรับปัญหาที่มีคำตอบสำเร็จรูปอยู่แล้วในรูปของ library framework บริการ หรือเครื่องมือทั้ง open source และเชิงพาณิชย์ ชื่อของแอนตี้ pattern นี้ยืมมาจากอุปมาว่าล้อเป็นสิ่งประดิษฐ์พื้นฐานที่มนุษย์คิดค้นและขัดเกลามานับพันปี การไปนั่งคิดค้นล้อใหม่จึงเป็นสัญลักษณ์ของการเสียเวลาไปกับสิ่งที่ถูกแก้ไขปัญหาไปแล้วอย่างสมบูรณ์
Wikipedia ชี้ว่ายังมีตัวแปรที่แย่กว่านั้นเรียกว่า “reinventing the square wheel” — คือไม่เพียงสร้างของซ้ำ แต่สร้างออกมาได้ แย่กว่า ของที่มีอยู่เดิมเสียอีก เพราะทีมไม่มีเวลา ความรู้ หรือ battle-testing เท่ากับโซลูชันที่ผ่านการใช้งานจริงมานาน
ข้อควรระวังคือ นี่ไม่ใช่การบอกว่า “ห้ามเขียนอะไรเองเด็ดขาด” DevIQ เองก็ยกเว้นกรณีที่ feature นั้นเป็น แกนหลัก (core) ของผลิตภัณฑ์ — ถ้าไม่ได้ขายเครื่องมือ logging ก็ไม่จำเป็นต้องเขียน logging เอง แต่ถ้าธุรกิจคือการขายเครื่องมือ logging นั่นแหละคือสิ่งเดียวที่ควรทุ่มพลังลงไปให้ดีที่สุด ส่วนที่เหลือทั้งหมดควรใช้ของที่มีอยู่
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”- รู้สึกควบคุมได้เต็มที่ — code ที่เขียนเองไม่มี dependency ภายนอก ไม่ต้องกังวลเรื่อง breaking change ของ third-party หรือ license ที่ไม่แน่ใจเงื่อนไข
- ดูเหมือนง่ายกว่าที่คิด — function เล็ก ๆ เช่น parser, logger, retry policy มักดูเหมือนเขียนเองได้ใน “หนึ่งบรรทัด” หรือหนึ่งบ่ายวัน โดยมองไม่เห็น edge case ที่ library สำเร็จรูปแก้ไขมาแล้วนับสิบ ๆ ครั้งจากผู้ใช้จริง
- ของสำเร็จรูปไม่พอดีเป๊ะ — library ที่มีมักออกแบบมาให้ generic ครอบคลุมหลายเคส ทำให้ดูเทอะทะเกินความจำเป็นเมื่อเทียบกับเคสเฉพาะของทีม จึงล่อใจให้ “เขียนแบบพอดีตัว” เอง
- เป็นโอกาสเรียนรู้ที่ดี — Coding Horror ยกคำพูดของ James Hart และ Charles Moore ว่าการลงมือ reinvent เพื่อ เรียนรู้กลไกเบื้องหลัง เป็นสิ่งมีค่า และ 97 Things Every Programmer Should Know ก็แนะนำให้โปรแกรมเมอร์ reinvent เป็นวิธีฝึกฝนตัวเอง — แต่นั่นคือบริบท “การเรียนรู้ส่วนตัว” ไม่ใช่ “code production ที่ทีมต้องดูแลระยะยาว”
- ความไม่ไว้ใจ third-party — บางทีมกังวลเรื่องความปลอดภัย supply-chain หรือเคยโดน library ทิ้งร้าง (abandonware) จนเลือกเขียนเองเพื่อความมั่นใจ ทั้งที่ทางเลือกอื่นเช่นการ vendor/fork หรือเลือก library ที่ maintain ดีกว่าอาจตอบโจทย์ได้โดยไม่ต้องเขียนใหม่ทั้งหมด
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”- เสียเวลาและงบประมาณไปกับสิ่งที่ไม่สร้างมูลค่าทางธุรกิจ — ทุกชั่วโมงที่ใช้เขียน logger, ORM, หรือ date-time parser เองคือชั่วโมงที่ไม่ได้ใช้พัฒนาสิ่งที่ทำให้สินค้าแตกต่างจากคู่แข่ง
- คุณภาพและความน่าเชื่อถือด้อยกว่า — Wikipedia สรุปตรง ๆ ว่าเมื่อ reinvent โดยไม่รู้ตัว ผลลัพธ์มักจะ อ่านยากกว่า เชื่อถือได้น้อยกว่า ทดสอบน้อยกว่า และ optimize น้อยกว่า ของที่ผ่านการใช้งานจริงจากผู้ใช้นับพันนับหมื่นคนมาแล้ว
- bug และช่องโหว่ที่ควรถูกแก้ไปนานแล้ว — โซลูชันสำเร็จรูปที่โตแล้วผ่านการเจอ edge case, race condition, และช่องโหว่ด้านความปลอดภัยมานับไม่ถ้วนจนถูก patch หมดแล้ว ส่วน code ที่เขียนเองใหม่ต้องไปเจอปัญหาเหล่านั้นซ้ำอีกครั้งจากศูนย์ — กรณีที่อันตรายที่สุดคือการเขียน cryptography หรือ authentication เอง ซึ่งมักซ่อนช่องโหว่ที่ไม่มีใครมองเห็นจนถูกโจมตีจริง
- ภาระบำรุงรักษาตกอยู่กับทีมเพียงลำพัง — library open source มีชุมชนคอย patch, document, และตอบคำถาม ส่วน code homegrown มีแค่ทีมเดียวที่ต้อง debug เอง แก้เอง เขียน doc เอง และเมื่อคนที่เขียนมันลาออก ความรู้ก็หายไปด้วย
- Onboarding และต้นทุนซ่อนเร้นสูงขึ้น — พนักงานใหม่ที่เคยชินกับเครื่องมือมาตรฐานของอุตสาหกรรม (เช่น Serilog, Entity Framework) ต้องมาเรียนรู้ระบบ homegrown ที่ไม่มีเอกสารมาตรฐาน ไม่มี Stack Overflow ให้ค้น ทำให้ productivity ลดลงโดยไม่จำเป็น
- Erik Dietrich ใน Telerik Blogs ชี้ว่าจุดที่มักถูก reinvent ซ้ำ ๆ ในองค์กรคือ logger, utility function เล็ก ๆ, ORM, ตัวแปลง file CSV/JSON/XML และ UI control อย่าง grid/calendar — ล้วนเป็นจุดที่ “ดูเหมือนง่าย” แต่จริง ๆ มีความซับซ้อนซ่อนอยู่มาก
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติทีมพัฒนา e-commerce ต้องการบันทึก log การทำงานของระบบ แล้วตัดสินใจเขียน logger เองแบบง่าย ๆ แทนที่จะใช้ library logging ที่มีอยู่:
// แอนตี้ pattern: เขียน logger เองแบบ ad-hocpublic static class SimpleLogger{ private static readonly string LogPath = "app.log";
public static void Log(string message) { // เปิด file และเขียนทับใหม่ทุกครั้ง ไม่ thread-safe // ไม่มี log level ไม่มี log rotation ไม่มี structured logging // ไม่มี async I/O จึง block request thread ทุกครั้งที่เรียก File.AppendAllText(LogPath, $"{DateTime.Now}: {message}{Environment.NewLine}"); }}
public class OrderService{ public void PlaceOrder(Order order) { SimpleLogger.Log($"เริ่มสั่งซื้อ order id {order.Id}"); // ... โลจิกการสั่งซื้อ ... SimpleLogger.Log("สั่งซื้อสำเร็จ"); }}code นี้ดูเหมือนใช้งานได้ในเดโม แต่พอเข้า production จริงจะเจอปัญหาเป็นระลอก: หลายคำขอเขียน file พร้อมกันทำให้ log บางบรรทัดหาย file โตไม่มีที่สิ้นสุดเพราะไม่มี rotation ไม่มีทางกรอง log level เพื่อลด noise และไม่มีทาง query แบบ structured เมื่อเกิดเหตุการณ์ผิดปกติในเวลา 3 นาฬิกา — สิ่งเหล่านี้คือปัญหาที่ library logging ระดับ production แก้ไปเรียบร้อยแล้ว
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”ก่อนลงมือเขียนอะไรเอง ให้ตั้งคำถามก่อนเสมอว่า “นี่คือสิ่งที่สินค้าของเราต้อง โดดเด่น หรือเป็นแค่ ส่วนประกอบสนับสนุน” ตามหลักการของ Know Where You Are Going และ YAGNI:
flowchart TD
A[ต้องการความสามารถใหม่] --> B{เป็นแกนหลักที่ทำให้สินค้าแตกต่าง}
B -->|ใช่| C[ประเมินสร้างเองอย่างจริงจัง]
B -->|ไม่ใช่| D{มี library หรือบริการที่โตแล้ว}
D -->|มี| E[Adopt ของที่มีอยู่]
D -->|ไม่มี หรือเงื่อนไข license ไม่เข้ากัน| F[สร้างเท่าที่จำเป็นจริง ๆ]
C --> G[ลงทุนเวลาและทรัพยากรเต็มที่]
E --> H[ทุ่มเวลาที่เหลือให้แกนหลักของสินค้า]
F --> H
เมื่อคำตอบคือ “ไม่ใช่แกนหลัก” ให้แทนที่ code homegrown ด้วย library ที่โตแล้วของอีโคซิสเต็ม เช่นในตัวอย่าง logger ด้านบน ควรใช้ Microsoft.Extensions.Logging ร่วมกับ sink อย่าง Serilog ซึ่งมี log level, structured logging, rotation และ async I/O ให้พร้อมใช้:
// ทางแก้: ใช้ library logging ที่โตแล้วผ่าน dependency injectionpublic class OrderService{ private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) { _logger = logger; }
public void PlaceOrder(Order order) { _logger.LogInformation("เริ่มสั่งซื้อ OrderId {OrderId}", order.Id); // ... โลจิกการสั่งซื้อ ... _logger.LogInformation("สั่งซื้อสำเร็จ OrderId {OrderId}", order.Id); }}
// Program.cs — ตั้งค่า Serilog เป็น logging provider พร้อม rotation และ enrichmentvar builder = Host.CreateApplicationBuilder(args);builder.Services.AddSerilog(config => config .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day) .WriteTo.Console() .Enrich.FromLogContext());หลักปฏิบัติเมื่อต้องตัดสินใจ build-vs-buy:
- ทำ spike สั้น ๆ ก่อนตัดสินใจ — ลองใช้ library ตัวเลือกจริงกับเคสของทีมสัก 1-2 วัน ก่อนสรุปว่า “ของสำเร็จรูปไม่พอดี” ซึ่งมักเป็นสมมติฐานที่ผิด
- แยก interface ออกจาก implementation — หากจำเป็นต้องเขียนเองจริง ๆ (เช่นติดเงื่อนไข license หรือ compliance) ให้ครอบด้วย interface ของทีมเองไว้ก่อน เพื่อให้สลับไปใช้ library มาตรฐานภายหลังได้โดยไม่กระทบ code ที่เรียกใช้ ตามหลัก DIP
- ถ้าตัดสินใจสร้างเอง ให้ทำอย่างจริงจัง — เขียน test ครอบคลุม เขียนเอกสาร และปฏิบัติกับมันเหมือนเป็นผลิตภัณฑ์จริง ไม่ใช่ code ชั่วคราวที่ไม่มีใครดูแล เพราะถ้าตัดสินใจว่ามันคือแกนหลักของสินค้าแล้ว มันสมควรได้รับความใส่ใจระดับนั้น
- ระวัง Not Invented Here — ถ้าเหตุผลที่แท้จริงในการปฏิเสธ library คือ “ไม่ได้เขียนโดยทีมเรา” มากกว่าเหตุผลทางเทคนิค นั่นคือสัญญาณของแอนตี้ pattern อีกตัวหนึ่งที่เกี่ยวโยงกันโดยตรง
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Not Invented Here — แอนตี้ pattern พี่น้องที่ปฏิเสธของภายนอกด้วยเหตุผลทางจิตวิทยามากกว่าทางเทคนิค
- Shiny Toy — หลงเทคโนโลยีใหม่แทนที่จะใช้โซลูชันที่พิสูจน์แล้ว ซึ่งมักนำไปสู่การ reinvent โดยไม่ตั้งใจ
- Golden Hammer — อีกด้านของเหรียญ คือใช้เครื่องมือเดียวซ้ำ ๆ ทุกปัญหาแทนที่จะเลือกเครื่องมือให้เหมาะกับงาน
- YAGNI — อย่าสร้างสิ่งที่ยังไม่ต้องใช้จริง หลักการเดียวกับที่ควรใช้ก่อนตัดสินใจ build เอง
- KISS — เลือกทางออกที่เรียบง่ายที่สุดที่ใช้งานได้จริง ซึ่งมักหมายถึงการใช้ของที่มีอยู่แล้ว
- Know Where You Are Going — รู้เป้าหมายทางธุรกิจ เพื่อแยกแยะว่าอะไรคือแกนหลักที่ควรลงทุนสร้างเอง