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

Anderson’s Law

ทุก​ปัญหา เมื่อ​มอง​ให้​ลึก มัก​ซับซ้อน​ยิ่ง​กว่า​เดิม

Anderson’s Law (โดย​นัก​เขียน​ไซ​ไฟ Poul Anderson) กล่าว​ว่า:

“ผม​ยัง​ไม่​เคย​เห็น​ปัญหา​ใด ไม่​ว่า​จะ​ซับซ้อน​แค่​ไหน ที่​เมื่อ​มอง​ใน​มุม​ที่​ถูกต้อง​แล้ว จะ​ไม่​กลับ​ยิ่ง​ซับซ้อน​ขึ้น​ไป​อีก”

— Poul Anderson

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

ประโยค​นี้​มี​ที่มา​ชัดเจน​กว่า​ที่​หลาย​คน​คิด: Poul Anderson เขียน​มัน​ไว้​ใน​ปาก​ตัว​ละคร Arne Viken จาก​เรื่องสั้น​แนว​ไซ​ไฟ “Call Me Joe” ซึ่ง​ตี​พิมพ์​ใน​นิตยสาร Astounding Science Fiction ฉบับ​เดือน​เมษายน ค.ศ. 1957 ถ้อยคำ​ต้นฉบับ​คือ “I’ve yet to see any problem, however complicated, which when you looked at it the right way didn’t become still more complicated”

ต่อ​มา​ใน​ปี 1967 Arthur Koestler นำ​ประโยค​นี้​ไป​อ้าง​เป็น​บทนำ​ของ​บท​ที่ 4 ใน​หนังสือ The Ghost in the Machine โดย​ให้​เครดิต Anderson อย่าง​ถูกต้อง แต่​เพราะ Koestler เป็น​คน​ดัง คำ​พูด​จึง​มัก​ถูก​เข้าใจ​ผิด​ว่า​เป็น​ของ​เขา​เอง (บาง​ครั้ง​เรียก​กัน​ว่า “Koestler’s motto” ทั้ง​ที่ Koestler เอง​ไม่​เคย​อ้าง​เครดิต​ให้​ตัวเอง) จาก​นั้น​ใน​ปี 1969 บทความ “Reductionism v. Organicism” ของ William Thorpe ใน​นิตยสาร New Scientist ได้​ปรับ​ถ้อยคำ​ให้​เป็น​รูปแบบ​ที่​แพร่หลาย​ที่สุด​ใน​ทุก​วัน​นี้ ก่อน​ที่ Paul Dickson จะ​รวบรวม​มัน​ไว้​ใน​หนังสือ The Official Rules ฉบับ​ขยาย (2013) พร้อม​ตั้ง​ชื่อ​ให้​เป็น​ทางการ​ว่า “Anderson’s Law”

ข้อสังเกต​ที่​น่า​สนใจ​คือ​บริบท​ดั้งเดิม​ของ​ประโยค​นี้​ไม่ใช่​วิศวกรรม​ซอฟต์แวร์​เลย แต่​เป็น​เรื่องสั้น​ไซ​ไฟ และ​ถูก​ยืม​มา​ใช้​ใน​บทความ​ปรัชญา​วิทยาศาสตร์​เรื่อง reductionism กับ organicism — ว่าการ​แยก​ส่วน​ระบบ​เพื่อ​ทำความ​เข้าใจ (reduction) จะ​ช่วย​ให้​เห็น​ความ​จริง​ง่าย​ขึ้น​หรือ​กลับ​ซ่อน​ความ​ซับซ้อน​ไว้​มาก​ขึ้น DevIQ หยิบยืม​นัย​ยะ​นี้​มา​ใช้​กับ​การ​ออกแบบ​ซอฟต์แวร์: สิ่ง​ที่​ดู​ง่าย​บน happy path จะ​ซับซ้อน​ขึ้น​ทันที​ที่​เริ่ม​ถาม​คำถาม “แล้ว​ถ้า…ล่ะ” — ถ้า​เครือข่าย​ขาด ถ้า​ผู้​ใช้​กด​ซ้ำ ถ้า​ข้อมูล​ไม่​สมบูรณ์ ถ้า​สอง​คน​แก้ไข​พร้อม​กัน

Anderson’s Law ไม่​ได้​บอกว่า “การ​วิเคราะห์​ให้​ลึก​เป็น​เรื่อง​ไร้​ประโยชน์” แต่​บอกว่า การ​วิเคราะห์​ที่​ดี​มัก​เผย ไม่ใช่​ซ่อน ความ​ซับซ้อน​ที่​มี​อยู่​แล้ว ความ​เรียบ​ง่าย​ที่​เรา​เห็น​ตอน​แรก​มัก​เป็น​ภาพลวงตา​ที่​เกิด​จาก​การ​มอง​ใน​ระดับ​สูง​เกิน​ไป (abstraction ที่​สูง​เกิน​จำเป็น) หรือ​มอง​แค่​กรณี​ทั่วไป​โดย​ละเลย​กรณี​ชาย​ขอบ

ใน​งาน​ซอฟต์แวร์ นัย​ยะ​ที่​สำคัญ​มี​อย่าง​น้อย​สาม​ด้าน:

  1. การ​ประมาณ​งาน (estimation) — ทีม​มัก​ประเมิน “เวลา​ที่​ต้อง​ใช้” จาก​ภาพ​ความ​เข้าใจ​ตอน​ต้น ซึ่ง​ยัง​ไม่​ผ่าน​การ​ซัก​ถาม​เชิง​ลึก พอ​เริ่ม​ลงมือ​จริง​จึง​เจอ edge case, ข้อ​จำกัด​ของ​ระบบ​เดิม, requirement ที่​ไม่มี​ใคร​พูด​ถึง ทำให้​งาน​ที่ “น่า​จะ​เสร็จ​ใน 2 วัน” กลาย​เป็น 2 สัปดาห์ ความ​สัมพันธ์​นี้​ใกล้​เคียง​กับ Hofstadter’s Law ที่​บอกว่า​งาน​ใช้​เวลา​นานกว่าที่​คาด​เสมอ แม้​จะ​เผื่อ Hofstadter’s Law ไว้​แล้ว​ก็ตาม — Anderson’s Law อธิบาย​ว่า ทำไม: เพราะ​ทุก​ครั้ง​ที่​มอง​ลึก​ขึ้น​อีก​ชั้น ความ​ซับซ้อน​ใหม่​จะ​โผล่​ออก​มา​เสมอ ไม่ใช่​น้อย​ลง
  2. สถาปัตยกรรม​ซอฟต์แวร์ — module ที่​ดูเหมือน​แยก​ส่วน​กัน​ชัดเจน​ตอน​ออกแบบ (เช่น “แค่​ผูก service A กับ service B”) มัก​ซ่อน​ความ​ซับซ้อน​ของ consistency, การ​จัดการ failure บาง​ส่วน (partial failure), และ contract ที่​เปลี่ยนแปลง​ได้ เมื่อ​มอง​ลึก​ลง​ไป​ใน​รายละเอียด​จริง
  3. domain ธุรกิจ — subdomain ที่​ผู้เชี่ยวชาญ​บอกว่า “ง่าย​นะ กฎ​แค่​ข้อ​เดียว” มัก​มี​ข้อ​ยกเว้น​ซ้อน​ข้อ​ยกเว้น​เมื่อ​คุย​กัน​ลึก​พอ นี่​คือ​เหตุผล​ที่​การ​ทำ Event Storming หรือ Domain Storytelling ถึง​มัก​เผย​ความ​ซับซ้อน​ที่​ไม่มี​ใคร​เห็น​มา​ก่อน แม้​จะ​ทำงาน​กับ​ระบบ​นี้​มา​หลาย​ปี​แล้ว

ผัง​ด้าน​ล่าง​แสดง​วงจร​ของ​การ “ลอก​ชั้น” ความ​ซับซ้อน​ออก​ที​ละ​ชั้น — แต่ละ​ชั้น​ที่​ลอก​ออก​ไม่​ได้​ทำให้​เหลือ​แค่​ความ​เรียบ​ง่าย มัน​มัก​เผย​ชั้น​ใหม่​ที่​ซับซ้อน​กว่า​เดิม​แทน

flowchart TD
    A[ออกแบบตาม happy path] --> B[ดูเหมือนง่าย]
    B --> C[เพิ่มสภาวะล้มเหลว]
    C --> D[เพิ่ม concurrency และ edge case]
    D --> E[เพิ่มเงื่อนไขข้อมูลจริง]
    E --> F[ความซับซ้อนที่แท้จริงเผยออกมา]
    F --> G[ต้องออกแบบใหม่ให้ลึกขึ้นอีกชั้น]
    G --> B

วง​วน​นี้​ไม่มี​จุดจบ​ตายตัว — สิ่ง​ที่​ทีม​ทำได้​คือ​ตัดสิน​ใจ​ว่า​จะ “ลง​ลึก​อีก​กี่​ชั้น” ให้​พอดี​กับ​ความ​เสี่ยง​และ​มูลค่า​ของ​งาน ไม่ใช่​พยายาม​ไล่​ตาม​ความ​สมบูรณ์​แบบ​ไป​เรื่อย ๆ (ดู Tolerance for Imperfection)

feature “แค่​ระบบ login” เป็น​ตัวอย่าง​คลาสสิก​ที่​ทุก​ทีม​เคย​เจอ ตอน​แรก​ดูเหมือน​งาน​เล็ก ๆ: รับ username/password แล้ว​เช็ค​กับ​ฐาน​ข้อมูล แต่​พอ​มอง​ลึก​ขึ้น​จะ​พบ​ว่า​ต้อง​มี: การ​รีเซ็ต​รหัส​ผ่าน, การ​ล็อก​บัญชี​ชั่วคราว​เมื่อ​พยายาม​ผิด​หลาย​ครั้ง (rate limiting), ข้อความ error ที่​ไม่​รั่ว​ข้อมูล​ผู้​ใช้ (ดู Fail Fast ใน​มุม​ความ​ปลอดภัย), การ​รองรับ​หลาย​ภาษา, การ invalidate session เมื่อ​เปลี่ยน​รหัส​ผ่าน, การ​เชื่อม​กับ OAuth ของ​ผู้​ให้​บริการ​ภายนอก​ที่​มี​เงื่อนไข​ต่าง​กัน และ​การ​ลบ​ข้อมูล​ผู้​ใช้​ตาม​กฎหมาย​คุ้มครอง​ข้อมูล​ส่วน​บุคคล — แต่ละ​อย่าง​เป็น​ชั้น​ความ​ซับซ้อน​ที่​ไม่มี​ใคร​พูด​ถึง​ตอน​ประเมิน​งาน​ครั้ง​แรก

code ตัวอย่าง​ด้าน​ล่าง​แสดง​พัฒนาการ​เดียวกัน​ใน​ระดับ function: version แรก​ดู​สมบูรณ์​ใน​ตัวเอง

// ดูเหมือนง่าย: แค่บันทึกคำสั่งซื้อแล้วเรียกเก็บเงิน
public void ProcessOrder(Order order)
{
_repository.Save(order);
_paymentGateway.Charge(order.Total);
}

แต่​เมื่อ​ถาม​คำถาม “แล้ว​ถ้า…ล่ะ” ไป​ที​ละ​ข้อ (สต็อก​หมด​พอดี​ตอน​สั่ง​พร้อม​กัน​สอง​คน, การ​ชาร์จ​เงิน timeout แล้ว​ถูก retry ซ้ำ, การ​ชำระ​เงิน​ถูก​ปฏิเสธ​หลัง​จอง​สต็อก​ไป​แล้ว) function เดิม​จะ​ขยาย​ตัว​ออก​เป็น​แบบ​นี้:

public async Task ProcessOrderAsync(Order order, CancellationToken ct)
{
// กัน race condition เมื่อสต็อกเหลือน้อยและมีคำสั่งซื้อพร้อมกัน
using var stockLock = await _stockLocks.AcquireAsync(order.ProductId, ct);
if (!await _inventory.TryReserveAsync(order.ProductId, order.Quantity, ct))
throw new OutOfStockException(order.ProductId);
try
{
// ต้องทำให้ idempotent เพราะ payment gateway อาจ timeout แล้วถูกเรียกซ้ำ
var charge = await _paymentGateway.ChargeAsync(
order.Total, idempotencyKey: order.Id, ct);
if (charge.Status == ChargeStatus.Declined)
{
await _inventory.ReleaseAsync(order.ProductId, order.Quantity, ct);
throw new PaymentDeclinedException(order.Id);
}
await _repository.SaveAsync(order, ct);
await _events.PublishAsync(new OrderPlaced(order.Id), ct);
}
catch
{
// ต้องคืนสต็อกที่จองไว้เสมอ ไม่ว่าจะพังตรงขั้นตอนไหน
await _inventory.ReleaseAsync(order.ProductId, order.Quantity, ct);
throw;
}
}

งาน​ที่ “น่า​จะ​เสร็จ​ใน 1 ชั่วโมง” กลาย​เป็น​งาน​ที่​ต้อง​คิด​เรื่อง locking, idempotency, compensating action และ event publishing — ทั้งหมด​นี้​มี​อยู่​จริง​ใน​ระบบ​ตั้งแต่​แรก เพียง​แต่​ยัง​ไม่​ถูก​มอง​เห็น

ใน​โลก​ของ​การ​ประมาณ​งาน บทความ “Hidden Complexities Of Software Estimation” ของ Mike McQuaid ชี้​ให้​เห็น​ปรากฏการณ์​เดียวกัน​โดย​ไม่​ได้​อ้าง​ถึง Anderson’s Law โดยตรง: การ​ประมาณ​เวลา​ต้อง​คิดถึง code review, การ deploy, ขวัญ​กำลังใจ​ของ​ทีม, ต้นทุน​การ​สลับ​งาน (context switching), และ​การ​ปรับ​โครงสร้าง​องค์กร​ระหว่าง​ทาง — ปัจจัย​เหล่า​นี้​ไม่​ปรากฏ​ใน​แผนงาน​ตอน​ต้น แต่​ปรากฏ​ขึ้น​เสมอ​เมื่อ​มอง​งาน​ให้​ลึก​พอ นอกจาก​วงการ​ซอฟต์แวร์​แล้ว ประโยค​นี้​ยัง​ถูก​อ้าง​ถึง​ใน​หนังสือ​การ​บริหาร​โครงการ​อย่าง Project Management: A Systems Approach ของ Harold Kerzner และ​หนังสือ Thinking in Systems: A Primer ของ Donella Meadows ซึ่ง​สะท้อน​ว่า​ปรากฏการณ์​นี้​ไม่​ได้​จำกัด​อยู่​แค่​วงการ​ไอที แต่​เป็น​ธรรมชาติ​ของ​ระบบ​ซับซ้อน​โดย​ทั่วไป

  • ประมาณ​งาน​เป็น​ช่วง (range) ไม่ใช่​ตัวเลข​เดียว เพราะ​ความ​ซับซ้อน​ที่​ยัง​ไม่​ถูก​ค้น​พบ​มี​อยู่​จริง​เสมอ การ​ให้​ตัวเลข​เดียว​เป็นการ​ซ่อน​ความ​ไม่​แน่นอน​ไว้ ไม่ใช่​การ​ขจัด​มัน​ออก​ไป
  • ทำ spike หรือ timebox สำรวจ​ความ​ไม่รู้​ก่อน​ตัดสิน​ใจ commit วัน​ส่ง​งาน การ​ขุด​ลึก​เพียง​เล็กน้อย​ก่อน​ประมาณ​มัก​คุ้ม​ค่า​กว่า​การ​เผื่อ buffer แบบ​เดา
  • ทำ pre-mortem หรือ threat modeling แบบ​สั้น ๆ ก่อน​เริ่ม​งาน โดย​ถาม​ว่า “อะไร​จะ​พัง​ได้​บ้าง” แทนที่​จะ​รอ​ให้​มัน​พัง​จริง​ระหว่าง​ทำงาน
  • อย่า​ใช้​กฎ​นี้​เป็น​ข้อ​อ้าง​ให้​วิเคราะห์​ไม่​จบ​สิ้น การ​ไล่​ตาม​ความ​ซับซ้อน​ทุก​ชั้น​จนกว่า​จะ “สมบูรณ์​แบบ” คือ​การ​ตกหลุมพราง Analysis Paralysis — เป้าหมาย​คือ​รู้​ว่า​มี​ความ​ไม่​แน่นอน​อยู่​ตรง​ไหน ไม่ใช่​ต้อง​แก้​มัน​ให้​หมด​ก่อน​เริ่ม​งาน
  • ออกแบบ​ให้​ระบบ​เรียบ​ง่าย​ที่สุด​ที่​ยัง​ทำงาน​ได้​ก่อน แล้ว​ค่อย​ให้​ความ​ซับซ้อน​ที่​จำเป็น​ค่อย ๆ ปรากฏ​ผ่าน​การ​ทำงาน​จริง แนวคิด​นี้​สอดคล้อง​กับ Gall’s Law ที่​ว่า​ระบบ​ซับซ้อน​ที่​ใช้งาน​ได้ มัก​วิวัฒนาการ​มา​จาก​ระบบ​ง่าย ๆ ที่​ใช้งาน​ได้​ก่อน ไม่ใช่​ถูก​ออกแบบ​ให้​ซับซ้อน​ตั้งแต่​วัน​แรก
  • บันทึก​สมมติฐาน​และ​สิ่ง​ที่​ยัง​ไม่รู้ (known unknowns) ไว้​อย่าง​ชัดเจน เพื่อ​ให้​ทีม​และ​ผู้​มี​ส่วน​ได้​ส่วน​เสีย​เห็น​ภาพ​เดียวกัน​ว่า​ตัวเลข​ประมาณ​การ​มี​ความ​ไม่​แน่นอน​แฝง​อยู่​มาก​แค่​ไหน