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 หนึ่งจุดแล้วส่งผลกระทบกระเพื่อมไปยังส่วนอื่นที่ดูไม่เกี่ยวข้องกันเลย (สัญญาณของการพึ่งพากันสูงเกินจำเป็น)