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

Keep It Simple

ยิ่ง​เรียบ​ง่าย ยิ่ง​พัง​ยาก และ​ยิ่ง​เข้าใจ​ง่าย

หลักการ KISS หรือ Keep It Simple, Stupid ครอบคลุม​หลาย​วิชาชีพ​และ​อุตสาหกรรม แนวคิด​คือ ยิ่ง​บาง​สิ่ง​ซับซ้อน​มาก​เท่าใด ก็​ยิ่ง​มี​วิธี​ที่​มัน​จะ​ล้มเหลว​มาก​ขึ้น​เท่านั้น และ​ยิ่ง​อธิบาย​ให้​คน​อื่น​ที่​ต้อง​เข้าใจ​มัน​ได้​ยาก​ขึ้น

เมื่อ​สร้าง​ซอฟต์แวร์ การ​ใช้​แนวทาง​แบบ​ค่อย​เป็น​ค่อย​ไป (incremental) ที่​คง​ความ​เรียบ​ง่าย​ไว้​ให้​นาน​ที่สุด​เท่า​ที่​จะ​ทำได้ มัก​ให้​ซอฟต์แวร์​ที่​ทำงาน​ได้​โดย​มี​ข้อ​บกพร่อง​น้อย​กว่า​และ​เร็ว​กว่า วิธี​หนึ่ง​ใน​การ​ลด​จำนวน bug ใน application คือ​การ​เพิ่ม​จำนวน​บรรทัด​ของ code ที่ ไม่​ต้อง​เขียน ให้​มาก​ที่สุด และ​การ​หลีก​เลี่ยง​ความ​ซับซ้อน​ที่​ไม่​จำเป็น​ก็​เป็น​วิธี​ที่​แน่นอน​ใน​การ​บรรลุ​เป้าหมาย​นี้

สิ่ง​สำคัญ​ที่​ต้อง​จำ​ไว้​เมื่อ​ถกเถียง​ว่า​ความ​ซับซ้อน​บาง​อย่าง​อาจ​คุ้ม​ค่า​หรือ​ไม่ คือ​หลาย​ครั้ง You Aren’t Gonna Need It หาก​เรา​เขียน​ซอฟต์แวร์​ให้​ยืดหยุ่น เรา​สามารถ​เพิ่ม function ใหม่​ภายหลัง​ได้​เมื่อ​จำเป็น ด้วย​เหตุ​นี้ ซอฟต์แวร์​ที่​เรียบ​ง่าย​จึง​มัก​ดัดแปลง​ได้​ง่าย​กว่า​ซอฟต์แวร์​ที่​ซับซ้อน

Everything should be made as simple as possible, but no simpler.

— Albert Einstein

Simplicity is the ultimate sophistication.

— Leonardo da Vinci

The Lazy Programmer does not use “clever” code because everyone who ever reads it or tries to change it will have to be just as clever, every time.

— RichardCHaven

ที่มา​ของ​คำ​ว่า KISS มัก​อ้าง​ถึง​วิศวกร​การ​บิน Kelly Johnson แห่ง Lockheed Skunk Works ผู้​ตั้ง​โจทย์​ให้​ทีม​ออกแบบ​เครื่องบิน​ไอพ่น​ว่า ต้อง​ซ่อม​ได้​ใน​สนามรบ​โดย​ช่าง​ซ่อม​ทั่วไป​ด้วย​เครื่องมือ​พื้นฐาน​เพียง​ไม่​กี่​ชิ้น​เท่านั้น ส่วน​คำ​ว่า “Keep it Short and Simple” ปรากฏ​ใน​บทความ​หนังสือพิมพ์​ตั้งแต่​ปี 1938 และ​กองทัพ​เรือสหรัฐฯ บันทึก​หลักการ​นี้​อย่าง​เป็น​ทางการ​ใน​ปี 1960 ก่อน​ที่​คำ​ว่า “KISS principle” จะ​แพร่หลาย​ใน​ปี 1970

ใน​ทาง​วิศวกรรม​ซอฟต์แวร์ สิ่ง​สำคัญ​คือ​การ​แยก​ให้​ออก​ระหว่าง ความ​ซับซ้อน​โดย​ธรรมชาติ (essential complexity) ที่มา​จาก​ตัว​ปัญหา​จริง ๆ กับ ความ​ซับซ้อน​โดย​บังเอิญ (accidental complexity) ที่​เรา​เผลอ​สร้าง​ขึ้น​เอง เช่น layer นามธรรม​ที่​ไม่มี​ใคร​ใช้ interface ที่​มี implementation เดียว หรือ​การ​ออกแบบ​ให้ “ยืดหยุ่น” สำหรับ​กรณี​ที่​ยัง​ไม่​เคย​เกิด​ขึ้น​จริง ระบบ​ที่​เรียบ​ง่าย​ไม่​ได้​หมาย​ถึง​ระบบ​ที่​ทำ​อะไร​ได้​น้อย แต่​หมาย​ถึง​ระบบ​ที่​มีส่วนประกอบ​น้อย​ที่สุด​เท่า​ที่​จำเป็นและ​มีการ​พึ่งพา​กัน​ระหว่าง​ส่วนประกอบ​น้อย​ที่สุด สอง​มิติ​นี้​ต่างหาก​ที่​ทำให้​ระบบ​ยาก — จำนวน​ชิ้น​ส่วน​กับ​ความ​เชื่อม​โยง​ระหว่าง​ชิ้น​ส่วน ไม่ใช่​ตัว​ปัญหา​เอง​เสมอ​ไป

Kent Beck เสนอ “Four Rules of Simple Design” ไว้​เป็นกรอบ​ตัดสิน​ใจ​ที่​นำ​มา​ปรับ​ใช้​กับ KISS ได้​ดี โดย​เรียง​ลำดับ​ความ​สำคัญ​คือ (1) ผ่าน​การ​ทดสอบ​ทั้งหมด (2) สื่อ​เจตนา​ของ code ได้​ชัดเจน (3) ไม่มี​ความ​ซ้ำซ้อน (เกี่ยวโยง​กับ DRY) และ (4) มี​จำนวน​องค์​ประกอบ​น้อย​ที่สุด​เท่า​ที่​ยัง​ตอบ​สาม​ข้อ​แรก​ได้ กฎ​ข้อ​สุดท้าย​นี้​คือ​หัวใจ​ของ KISS โดยตรง — ถ้า​องค์​ประกอบ​ใด​ไม่​ได้​ช่วย​ให้ code ถูกต้อง อ่าน​ง่าย หรือ​ไม่​ซ้ำซ้อน ให้​ตัด​ทิ้ง

  • ลด​จำนวน​ที่​จะ​พัง ยิ่ง​มี​เงื่อนไข เส้น​ทางการ​ทำงาน (branch) และ​การ​พึ่งพา​กัน​มาก​เท่าใด ยิ่ง​มี​วิธี​ที่​ระบบ​จะ​ล้มเหลว​มาก​ขึ้น​เท่านั้น Microsoft Learn ใช้ cyclomatic complexity (จำนวน​เส้น​ทางการ​ตัดสิน​ใจ​อิสระ​ใน function) เป็น​ตัว​ชี้​วัด​ความ​เสี่ยง​ต่อ bug และ​ยิ่ง​เมทริกซ์​นี้​สูง ยิ่ง​ยาก​ต่อ​การ​ทดสอบ​ให้​ครบ​ทุก​เส้นทาง
  • จำกัด​ของ​สมอง​มนุษย์​มี​จริง Brian Kernighan เคย​กล่าว​ไว้​ว่าการ​ควบคุม​ความ​ซับซ้อน​คือ​แก่น​แท้​ของ​การ​เขียน​โปรแกรม เพราะ​เรา​ถูก​จำกัด​ด้วย​จำนวน​รายละเอียด​ที่​สมอง​เก็บ​ไว้​พร้อม​กัน​ได้​เสมอ ยิ่ง code เรียบ​ง่าย ยิ่ง​รักษา mental model ที่​ถูกต้อง​ของ​ระบบ​ไว้​ใน​หัว​ได้​นาน​ขึ้น
  • ดัดแปลง​และ​ต่อยอด​ง่าย​กว่า ระบบ​ที่​ไม่​แบกรับ​ความ​ยืดหยุ่น​แบบ​เผื่อ ๆ ไว้ (speculative flexibility) มัก​ปรับ​เปลี่ยน​ได้​ง่าย​กว่า​ระบบ​ที่ “ออกแบบ​ไว้​ล่วงหน้า​ให้​รองรับ​ทุก​กรณี” เพราะ code ที่​ซับซ้อน​เกิน​จำเป็น​สร้าง​แรง​เสียด​ทาน​ทุก​ครั้ง​ที่​ต้อง​แก้ไข แม้แต่​การ​แก้​เล็ก ๆ ก็​อาจ​กระเพื่อม​ไป​ทั่ว​ระบบ​อย่าง​คาด​ไม่​ถึง
  • ทีม​ทำงาน​เร็ว​ขึ้น สมาชิก​ใหม่​เข้าใจ code ง่าย​ขึ้น รีวิว code เร็ว​ขึ้น และ debug ได้​ตรง​จุด​ขึ้น เพราะ​ไม่​ต้อง​ไล่​ตาม​ชั้น​ของ​นามธรรม​ที่​ไม่​จำเป็น

ตัวอย่าง​คลาสสิก​ของ​ความ​ซับซ้อน​โดย​บังเอิญ​คือ method ที่​รวม​หลาย​ความ​รับผิดชอบ​เข้า​ไว้​ด้วย​กัน​โดย​ไม่​จำเป็น แล้ว​ห่อ​หุ้ม​ด้วย layer นามธรรม​ที่​ไม่มี​ใคร​ใช้​ประโยชน์​จริง

// ก่อน — ซับซ้อนเกินความจำเป็น: รวมสามความรับผิดชอบไว้ใน method เดียว
// และมี interface/factory ที่มี implementation เดียวโดยไม่มีเหตุผลรองรับ
public interface IOrderWorkflowStrategy
{
void Execute(Order order);
}
public class DefaultOrderWorkflowStrategy : IOrderWorkflowStrategy
{
private readonly IOrderWorkflowStrategyFactory _factory;
public DefaultOrderWorkflowStrategy(IOrderWorkflowStrategyFactory factory)
{
_factory = factory;
}
public void Execute(Order order)
{
// สร้างคำสั่งซื้อ ตัดสต็อก และจัดส่ง รวมอยู่ใน method เดียว
CreateOrderAndMoveStockAndDeliverToUser(order);
}
private void CreateOrderAndMoveStockAndDeliverToUser(Order order)
{
order.Status = OrderStatus.Created;
foreach (var line in order.Lines)
{
// ตัดสต็อกแทรกอยู่กลาง logic สร้างคำสั่งซื้อ
line.Product.StockQuantity -= line.Quantity;
}
order.Status = OrderStatus.Shipped;
}
}
// หลัง — แยกความรับผิดชอบให้ชัดเจน ตัดชั้นนามธรรมที่ไม่จำเป็นออก
// ทำให้แต่ละ method สื่อเจตนาตรงตัวและทดสอบแยกกันได้
public class OrderService
{
public void CreateOrder(Order order) => order.Status = OrderStatus.Created;
public void ReserveStock(Order order)
{
foreach (var line in order.Lines)
line.Product.StockQuantity -= line.Quantity;
}
public void ShipOrder(Order order) => order.Status = OrderStatus.Shipped;
}
// ผู้เรียกประกอบขั้นตอนเองตามที่ต้องการจริง — ไม่ต้องพึ่ง strategy/factory ที่มีทางเลือกเดียว
var service = new OrderService();
service.CreateOrder(order);
service.ReserveStock(order);
service.ShipOrder(order);

code “หลัง” ไม่​ได้​ทำ​อะไร​น้อย​ลง แต่​มี​จำนวน​ส่วนประกอบ​และ​ความ​เชื่อม​โยง​ระหว่าง​ส่วนประกอบ​น้อย​ลง — ไม่มี IOrderWorkflowStrategy, ไม่มี IOrderWorkflowStrategyFactory ที่​ไม่​เคย​มี implementation ตัว​ที่​สอง แต่ละ method ทำ​สิ่ง​เดียว​และ​ตั้ง​ชื่อ​สื่อ​เจตนา จึง​เปลี่ยนแปลง​และ​ทดสอบ​ได้​ง่าย​กว่า​เดิม​มาก

แผนภาพ​ต่อ​ไป​นี้​สรุป​เส้น​ทางการ​ตัดสิน​ใจ​อย่าง​ง่าย ๆ เมื่อ​พิจารณา​จะ​เพิ่ม​นามธรรม (abstraction) ใหม่​เข้าไป​ใน​ระบบ​หรือ​ไม่ — เป็น​วิธี​คิด​ที่​ช่วย​หลีก​เลี่ยง​การ​เพิ่ม​ความ​ซับซ้อน​แบบ​เผื่อ​อนาคต:

flowchart TD
    Need[มีความต้องการใช้งานจริงตอนนี้ไหม]
    Simple[เขียนแบบตรงไปตรงมาที่สุดที่ยังผ่าน test]
    Vary[จุดนี้แปรผันจริงกี่แบบในปัจจุบัน]
    OneImpl[คงไว้แบบเดียว ไม่ต้องมี interface เผื่อ]
    Abstract[แยกนามธรรมเท่าที่จำเป็นจริง]

    Need -->|ไม่ ยังไม่ต้องการ| Simple
    Need -->|ใช่ ต้องการแล้ว| Vary
    Vary -->|แบบเดียว| OneImpl
    Vary -->|มากกว่าหนึ่งแบบจริง| Abstract
  • มี interface หรือ abstract class ที่​มี implementation เพียง​ตัว​เดียว​มา​นาน​โดย​ไม่มี​ทีท่า​ว่า​จะ​มี​ตัว​ที่​สอง (มัก​เป็น​สัญญาณ​ของ YAGNI ที่​ถูก​ละเมิด​ร่วม​ด้วย)
  • method หรือ class มี cyclomatic complexity สูง — มี​เงื่อนไข​และ​เส้น​ทางการ​ทำงาน​จำนวน​มาก​ซ้อน​กัน​จน​ต้อง​ไล่ code หลาย​รอบ​ถึง​จะ​เข้าใจ
  • ต้อง​เปิด file มากกว่า 5-6 file เพื่อ​ทำความ​เข้าใจ flow เดียว ทั้ง​ที่​ปัญหา​ที่​แก้​จริง ๆ ไม่​ได้​ซับซ้อน​ขนาด​นั้น
  • ใช้ design pattern เพราะ “อาจ​มี​ประโยชน์​ใน​อนาคต” ไม่ใช่​เพราะ​ปัญหา​ปัจจุบัน​ต้องการ
  • ตัวแปร method หรือ class ที่​ไม่มี​ใคร​เรียก​ใช้​แล้ว (dead code) ยัง​ค้าง​อยู่​ใน​ระบบ เพิ่ม​ภาระ​การ​อ่าน​โดย​ไม่มี​ประโยชน์
  • แก้ bug หนึ่ง​จุด​แล้ว​ส่ง​ผลกระทบ​กระเพื่อม​ไป​ยัง​ส่วน​อื่น​ที่​ดู​ไม่​เกี่ยวข้อง​กัน​เลย (สัญญาณ​ของ​การ​พึ่งพา​กัน​สูง​เกิน​จำเป็น)