ข้าม​ไป​ยัง​เนื้อหา

Not Invented Here

ปฏิเสธ​ทุก​สิ่ง​ที่​ไม่​ได้​สร้าง​เอง จน​เสีย​ทรัพยากร​ไป​กับ​ของ​ซ้ำ

Not Invented Here (NIH) คือ​แนวโน้ม​ของ​องค์กร​หรือ​ทีม​ที่ ปฏิเสธ​จะ​เชื่อถือ ใช้ หรือ​แม้แต่​พิจารณา library เครื่องมือ มาตรฐาน หรือ​แนวคิด​ใด ๆ ที่​ไม่​ได้​ถูก​สร้าง​ขึ้น​ภายใน​ทีม​ของ​ตนเอง แม้​จะ​มี​ทาง​เลือก​ที่​ดี​กว่า ถูก​กว่า หรือ​ผ่าน​การ​พิสูจน์​มา​แล้ว​ใน​ตลาด​อยู่​ก็ตาม อาการ​ทั่วไป​คือ​ทีม​พูด​ว่า “เดี๋ยว​เรา​เขียน​เอง” ทันที​ที่​มี​คน​เสนอ library หรือ​บริการ​ภายนอก โดย​ไม่ทัน​ได้​ประเมิน​ต้นทุน-ผล​ประโยชน์​อย่าง​จริงจัง

ผลลัพธ์​คือ​ทรัพยากร​วิศวกรรม​ที่​มี​ค่า — เวลา สมอง และ​งบประมาณ — ถูก​ทุ่ม​ไป​กับ​การ​สร้าง​ของ​ที่​ตลาด​มี​อยู่​แล้ว​ซ้ำ​แล้ว​ซ้ำ​เล่า (เช่น ORM ของ​ตัวเอง, message queue ของ​ตัวเอง, authentication framework ของ​ตัวเอง) แทนที่​จะ​เอา​ไป​ใช้​กับ feature หลัก​ที่​สร้าง​ความ​แตก​ต่าง​ให้​ผลิตภัณฑ์​จริง ๆ

NIH เป็น​อีก​ด้าน​หนึ่ง​ของ​เหรียญ​เดียวกัน​กับ Reinventing the Wheel — Reinventing the Wheel อธิบาย “การกระทำ” ของ​การ​สร้าง​ของ​ซ้ำ ส่วน NIH อธิบาย “ทัศนคติ” หรือ​วัฒนธรรม​ที่​นำ​ไป​สู่​การกระทำ​นั้น

  • ควบคุม​ได้​เต็ม​ที่ — code ที่​เขียน​เอง​แก้ไข​ได้​ตามใจ ไม่​ต้อง​รอ vendor หรือ maintainer ของ open-source แก้ bug ให้
  • เข้าใจ​ทุก​บรรทัด — ทีม​รู้สึก​มั่นใจ​ว่า​เข้าใจ​ระบบ​ทั้งหมด ไม่มี “กล่อง​ดำ” ที่​ไม่รู้​ว่า​ข้าง​ใน​ทำงาน​อย่างไร
  • ไม่มี​ความ​เสี่ยง​ด้าน​สัญญา​อนุญาต​หรือ​ความ​ปลอดภัย​จาก​ภายนอก — ไม่​ต้อง​กังวล​เรื่อง license ที่​ไม่​เข้า​กัน หรือ​ช่อง​โหว่​ใน code ของ​คน​อื่น
  • ความ​ภาค​ภูมิใจ​ทาง​วิศวกรรม — ทีม​ที่​มี​ทักษะ​สูง​มัก​เชื่อ​ว่า​ตนเอง​สร้าง​ของ​ที่ “ดี​กว่า” หรือ “เข้า​กับ use case ของ​เรา” ได้​มากกว่า​ของ​สำเร็จรูป​ทั่วไป ซึ่ง​บาง​ครั้ง​ก็​จริง โดย​เฉพาะ​กับ​ตรรกะ​ทาง​ธุรกิจ​ที่​เป็น​แก่น​ของ​ผลิตภัณฑ์
  • ประสบการณ์​แย่​ใน​อดีต — เคย​เจอ library ภายนอก​ที่ bug เยอะ ดูแล​ไม่​ดี หรือ​ถูก deprecate กลางคัน จึง​สรุป​เหมา​รวม​ว่า​ของ​ภายนอก​เชื่อถือ​ไม่​ได้​เลย

เหตุผล​เหล่า​นี้​ทำให้ NIH ฟัง​ดูเหมือน​วินัย​ทาง​วิศวกรรม​ที่​ดี ไม่ใช่​ปัญหา — นั่น​คือ​กับดัก​ของ​มัน

  • เสีย​เวลา​สร้าง​ของ​สามัญ (commodity) ที่​ไม่​สร้าง​ความ​แตก​ต่าง​ทาง​ธุรกิจ — เวลา​ที่​ทีม​ใช้​เขียน ORM หรือ logging framework ของ​ตัวเอง คือ​เวลา​ที่​ไม่​ได้​ใช้​กับ feature ที่​ลูกค้า​จ่าย​เงิน​ซื้อ
  • ภาระ​บำรุง​รักษา​ระยะ​ยาว​ที่​มอง​ไม่​เห็น​ตอน​เริ่ม — โซลูชัน​สำเร็จรูป​มี​ชุมชน​คอย​แก้ bug อัปเดต​ความ​ปลอดภัย และ​เขียน​เอกสาร​ให้​ฟรี ส่วน​ของ​ที่​เขียน​เอง​ต้อง​มี​ทีม​ภายใน​ดูแล​ตลอด​อายุ​ของ​ระบบ ต้นทุน​สะสม​ตลอด​วงจร​ชีวิต​ซอฟต์แวร์ (total cost of ownership) มัก​สูงกว่าที่​ประเมิน​ไว้​มาก
  • คุณภาพ​และ​ความ​ลึก​ของ feature สู้​ของ​สำเร็จรูป​ที่​โต​มา​หลาย​ปี​ไม่​ได้ — library open-source ที่​ใช้​กัน​แพร่หลาย​ผ่าน​การ​ทดสอบ​จาก edge case นับ​พัน​จาก​ผู้​ใช้​ทั่ว​โลก ขณะ​ที่​ของ​ที่​เขียน​เอง​มัก​ถูก​ทดสอบ​แค่​กับ use case ที่​ทีม​นึกออก​ตอน​นั้น
  • แยก​ตัว​จาก​องค์​ความ​รู้​ของ ecosystem — ทีม​ที่​ทำ​ทุก​อย่าง​เอง​พลาด​โอกาส​เรียนรู้​จาก​แนว​ปฏิบัติ​ที่​ดี​ที่สุด​ของ​อุตสาหกรรม และ​วิศวกร​ใหม่​ที่​เข้า​ทีม​ต้อง​เรียนรู้​ระบบ​ภายใน​ที่​ไม่มี​ใคร​นอก​ทีม​รู้จัก แทนที่​จะ​ใช้​ทักษะ​มาตรฐาน​ที่​ติดตัว​มา
  • เอกสาร​และ onboarding แย่ — ระบบ​ภายใน​มัก​ไม่มี​เอกสาร​ดี​เท่า project ที่​มี​ชุมชน​ขนาด​ใหญ่ ทำให้​ความ​รู้​กระจุก​อยู่​กับ​คน​ไม่​กี่​คน (ดู Lois Lane และ​ปัญหา bus factor)
  • ล่าช้า​กว่า​คู่แข่ง​เข้า​ตลาด — เวลา​ที่​หมด​ไป​กับ​การ​ประดิษฐ์​ล้อ​ใหม่ คือ​เวลา​ที่​คู่แข่ง​ใช้​สร้าง feature ที่​ทำให้​พวก​เขา​ชนะ​ตลาด

ทีม​ต้องการ retry policy พร้อม exponential backoff และ circuit breaker สำหรับ​เรียก API ภายนอก แทนที่​จะ​ใช้ library ที่​ผ่าน​การ​พิสูจน์​แล้ว​อย่าง Polly ทีม​ตัดสิน​ใจ​เขียน​กลไก retry ของ​ตัวเอง​ตั้งแต่​ศูนย์:

// ตัวอย่างที่เป็นปัญหา — เขียน retry/circuit-breaker เองทั้งหมด
// เพราะ "ไม่อยากพึ่งพา library ภายนอก"
public class HomeGrownRetryClient
{
private readonly HttpClient _http;
private int _consecutiveFailures;
private DateTime _circuitOpenUntil = DateTime.MinValue;
public HomeGrownRetryClient(HttpClient http) => _http = http;
public async Task<string> GetWithRetryAsync(string url)
{
if (DateTime.UtcNow < _circuitOpenUntil)
throw new InvalidOperationException("circuit ยังเปิดอยู่ ลองใหม่ทีหลัง");
var delayMs = 200;
for (var attempt = 1; attempt <= 5; attempt++)
{
try
{
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
_consecutiveFailures = 0;
return await response.Content.ReadAsStringAsync();
}
catch (Exception) when (attempt < 5)
{
// ยังไม่ครอบกรณี jitter, timeout ที่เหมาะสม,
// หรือ thread-safety เวลามีหลาย request พร้อมกัน
await Task.Delay(delayMs);
delayMs *= 2;
_consecutiveFailures++;
if (_consecutiveFailures >= 5)
_circuitOpenUntil = DateTime.UtcNow.AddSeconds(30);
}
}
throw new InvalidOperationException("เรียก API ไม่สำเร็จหลังพยายามครบจำนวนครั้ง");
}
}

code นี้​ดู​ใช้งาน​ได้​ใน​ตอน​แรก แต่​ขาด​สิ่ง​ที่ library ที่​โต​แล้ว​อย่าง Polly มี​ให้​ครบ เช่น jitter เพื่อ​ลด thundering herd, การ​รองรับ cancellation token อย่าง​ถูกต้อง, thread-safety ของ state ร่วม, และ​การ​แยก policy ออก​จาก business logic ทุก​ครั้ง​ที่​ทีม​เจอ edge case ใหม่ (เช่น timeout ต้อง​ต่าง​กัน​ตาม endpoint) ก็​ต้อง​แก้ code นี้​เอง แทนที่​จะ​แค่ configure policy ที่​มี​อยู่​แล้ว

หลักการ​แก้​คือ แยก​ให้​ชัด​ว่า​อะไร​คือ “แก่น​ที่​สร้าง​ความ​แตก​ต่าง” ของ​ธุรกิจ กับ​อะไร​คือ “ของ​สามัญ” (commodity) — ของ​สามัญ​ควร​ซื้อ​หรือ​ใช้​ของ​สำเร็จรูป​เสมอ ส่วน​แก่น​ของ​ธุรกิจ​เท่านั้น​ที่​ควร​สร้าง​เอง

// ทางแก้ — ใช้ library ที่ผ่านการพิสูจน์แล้ว (Polly)
// แล้วโฟกัสพลังงานทีมไปที่ policy ที่เหมาะกับ domain ของเราแทน
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 200))); // jitter
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
var resiliencePolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
public class ResilientApiClient
{
private readonly HttpClient _http;
private readonly IAsyncPolicy _policy;
public ResilientApiClient(HttpClient http, IAsyncPolicy policy)
{
_http = http;
_policy = policy;
}
public Task<string> GetAsync(string url) =>
_policy.ExecuteAsync(async () =>
{
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
});
}

ขั้นตอน​เชิง​ปฏิบัติ​สำหรับ​ทีม​และ​องค์กร:

  1. ทำ build-vs-buy อย่าง​เป็น​ระบบ ก่อน​เริ่ม​เขียน​ของ​ใหม่​ทุก​ครั้ง ให้​ถาม​ว่า “นี่​คือ​สิ่ง​ที่​สร้าง​ความ​ได้​เปรียบ​ทางการ​แข่งขัน​ให้​เรา​จริง​หรือ หรือ​เป็น​แค่​ของ​สามัญ​ที่​ตลาด​มี​คำ​ตอบ​อยู่​แล้ว” — หาก​เป็น​ของ​สามัญ ให้ default ไป​ทาง buy/use เสมอ
  2. ห่อ​ของ​ภายนอก​ด้วย abstraction ของ​ตัวเอง เมื่อ​จำเป็น​ต้อง​พึ่ง library ภายนอก ใช้ Adapter หรือ Facade เพื่อ​กัน​ไม่​ให้ API ของ third-party รั่วไหล​เข้าไป​ทั่ว codebase วัน​ที่​ต้อง​เปลี่ยน library จะ​เจ็บ​น้อย​ลง
  3. นับ​ต้นทุน​ตลอด​วงจร​ชีวิต ไม่ใช่​แค่​ต้นทุน​ตอน​เขียน​ครั้ง​แรก รวม​ค่า​บำรุง​รักษา อัปเดต​ความ​ปลอดภัย และ onboarding พนักงาน​ใหม่​เข้าไป​ใน​การ​ประเมิน​ด้วย
  4. ให้​รางวัล​กับ​ผลลัพธ์ ไม่ใช่​กับ​การ​ควบคุม​สถาปัตยกรรม วัฒนธรรม​ที่​ยกย่อง​วิศวกร​ที่ “ส่ง feature ได้​เร็ว​ด้วย​ของ​สำเร็จรูป” พอ ๆ กับ​วิศวกร​ที่​เขียน​ระบบ​ซับซ้อน​เอง จะ​ลด​แรง​จูงใจ​ของ NIH ลง​ตาม​ธรรมชาติ
  5. สำรวจ​ระบบ​ภายใน​ที่​มี​อยู่​แล้ว ถ้า​องค์กร​มี​วัฒนธรรม NIH ฝัง​ลึก ให้​ทำ inventory ของ​เครื่องมือ​ที่​เขียน​เอง​ทั้งหมด ประเมิน​ว่า​ตัว​ไหน low-value ควร​ย้าย​ไป​ใช้​มาตรฐาน​อุตสาหกรรม แล้ว​ยก​ตัวอย่าง​ความ​สำเร็จ​ของ​การ integrate ของ​ภายนอก​ให้​ทีม​เห็น​เป็น​รูปธรรม

ทั้งนี้ NIH ไม่​ได้​แปล​ว่า “ห้าม​เขียน​เอง​เด็ดขาด” — งาน​ที่​เป็น​แก่น​ของ domain ธุรกิจ (core domain) ที่​ไม่มี​ของ​สำเร็จรูป​ตรง​กับ use case จริง ๆ ยัง​คุ้ม​ค่าที่​จะ​สร้าง​เอง หลักการ “duplication is far cheaper than the wrong abstraction” ยัง​ใช้ได้​กับ​ตรรกะ​ทาง​ธุรกิจ​เฉพาะ​ทาง ประเด็น​คือ​ต้อง​เป็นการ​ตัดสิน​ใจ​ที่​ไตร่ตรอง​แล้ว ไม่ใช่​ปฏิกิริยา​อัตโนมัติ​จาก​อคติ​ต่อ​ของ​ภายนอก

flowchart TD
    Need[มีความต้องการ feature ใหม่]
    Ask{เป็นแก่นที่สร้าง<br/>ความแตกต่างทางธุรกิจหรือไม่}
    Commodity[เป็นของสามัญ เช่น logging<br/>retry queue orm auth]
    Core[เป็นแก่น domain ธุรกิจเฉพาะทาง]
    Use[ใช้ของสำเร็จรูปที่พิสูจน์แล้ว<br/>ห่อด้วย Adapter หรือ Facade]
    Build[พิจารณาสร้างเอง<br/>ด้วยความเข้าใจต้นทุนตลอดวงจรชีวิต]

    Need --> Ask
    Ask -->|ไม่ใช่| Commodity
    Ask -->|ใช่| Core
    Commodity --> Use
    Core --> Build
  • Reinventing the Wheel — การกระทำ​ที่​เป็น​ผล​จาก​ทัศนคติ​แบบ NIH
  • Golden Hammer — อคติ​ทาง​เครื่องมือ​อีก​แบบ ใช้​เครื่องมือ​เดียว​กับ​ทุก​ปัญหา​แทนที่​จะ​พิจารณา​ทาง​เลือก
  • Found on Internet — สุด​โต่ง​ฝั่ง​ตรง​ข้าม เชื่อ​ของ​ที่​หา​มา​จาก​อินเทอร์เน็ต​โดย​ไม่​ตรวจสอบ
  • Frozen Caveman — ยึด​ติด​กับ​ความ​เสี่ยง​ใน​อดีต​จน​ปฏิเสธ​แนวทาง​ใหม่​ที่​เหมาะสม​กว่า
  • Adapter — pattern ที่​ช่วย​ห่อ library ภายนอก​ให้​ปลอดภัย​เมื่อ​เลือก​ใช้​ของ​สำเร็จรูป
  • Facade — ลด​พื้นที่​ผิว​ของ API ภายนอก​ที่ codebase ต้อง​รู้จัก