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

Principle of Least Astonishment

ออกแบบ​ให้​คาด​เดา​ได้ อย่า​สร้าง​ความ​ประหลาด​ใจ

Principle of Least Astonishment (POLA) หรือ​ที่​รู้จัก​ใน​ชื่อ Principle of Least Surprise (POLS) เป็น​แนวทางการ​ออกแบบ​ซอฟต์แวร์​ที่​บอกว่า ส่วน​ประ​กอบใดๆ ของ​ระบบ — ไม่​ว่า​จะ​เป็น function, class, API, หรือ​หน้า​จอ UI — ควร​ทำงาน​ใน​แบบ​ที่​คน​ส่วน​ใหญ่​คาด​หวัง​ว่า​มัน​จะ​ทำงาน คำ​ว่า “คน” ใน​ที่​นี้​หมาย​ถึง​ทั้ง​ผู้​ใช้​ปลายทาง​และ​นัก​พัฒนา​ที่มา​อ่าน มา​เรียก หรือ​มา​ต่อยอด code นั้น การ​ยึด​หลักการ​นี้​ทำให้​ระบบ​ใช้งาน​ง่าย​ขึ้น คาด​เดา​ได้ และ​เรียนรู้​ได้​เร็ว​กว่า

รากศัพท์​ของ​หลักการ​นี้​ย้อน​ไป​ถึง “Law of Least Astonishment” ที่​ปรากฏ​ใน PL/I Bulletin ปี 1967 ใน​ฐานะ​แนวทาง​ออกแบบ​ภาษา​โปรแกรม และ​มี​การ​เขียน​ไว้​อย่าง​เป็น​ทางการ​อีก​ครั้ง​ใน​ปี 1972 ว่า “ทุก construct ใน​ระบบ​ควร​ทำงาน​ตรง​ตาม​ที่ syntax ของ​มัน​สื่อ​ความหมาย” ไอเดีย​นี้​จึง​เก่า​แก่พอๆ กับ​วงการ computer science เอง แต่​ยัง​คง​ใช้งาน​ได้​จริง​ใน​ทุก​วัน​นี้

หัวใจ​ของ POLA คือ มนุษย์​เป็น​ส่วน​หนึ่ง​ของ​ระบบ การ​ออกแบบ​ต้อง​สอดคล้อง​กับ​ประสบการณ์ ความ​คาด​หวัง และ mental model ที่​ผู้​ใช้​มี​อยู่​แล้ว แทนที่​จะ​บังคับ​ให้​ผู้​ใช้​ปรับ​ตัว​เข้าหา​การ​ทำงาน​ภายใน​ของ​ระบบ กล่าว​อีก​แบบ​คือ ผลลัพธ์​ของ operation หนึ่งๆ ควร “ชัดเจน สอดคล้อง และ​คาด​เดา​ได้ จาก​ชื่อ​ของ operation และ​เบาะแสอื่นๆ รอบ​ตัว​มัน” — ไม่ใช่​จาก​การ​อ่าน implementation ข้าง​ใน

หลักการ​นี้​มี corollary (ข้อ​สรุป​แทรก) ที่​สำคัญ: ถ้า feature ที่​จำเป็น​ต้อง​มี​นั้น​มี “astonishment factor” สูง ก็​อาจ​ถึง​เวลา​ต้อง​ออกแบบ feature นั้น​ใหม่ — พูด​ง่ายๆ คือ ถ้า feature หนึ่ง​สร้าง​ความ​ประหลาด​ใจ​ให้​ผู้​ใช้​บ่อยๆ ปัญหา​ไม่​ได้​อยู่​ที่​ผู้​ใช้ “ใช้​ผิด” แต่​อยู่​ที่​การ​ออกแบบ​เอง

ตัวอย่าง​คลาสสิก​ที่​มัก​ถูก​ยก​มา​เตือน​ใจ ได้แก่:

  • PL/I เอง ที่​ผิด​หลักการ​ที่​ตัวเอง​บัญญัติ​ไว้: นิพจน์ 25 + 1/3 กับ 1/3 + 25 ให้​ผลลัพธ์​ต่าง​กัน เพราะ​กฎ​การ​แปลง​ความ​ละเอียด (precision conversion) ขึ้น​กับ​ลำดับ​ของ operand
  • JavaScript ยุค​แรก ที่​ตีความ string ตัวเลข​ที่​ขึ้น​ต้น​ด้วย 0 เป็น​เลข​ฐาน 8 โดย​อัตโนมัติ (เช่น "017" กลาย​เป็น 15) สร้าง​ความ​สับสน​ให้​นัก​พัฒนา​จำนวน​มาก
  • key ลัด F1 ที่​คน​คาด​หวัง​ว่า​จะ​เปิด​หน้า Help บน Windows/Linux — ถ้า​โปรแกรม​ใด​ใช้ F1 ทำ​อย่าง​อื่น ก็​ถือว่า​ละเมิด POLA

เหตุผล​เบื้องหลัง POLA คือ​การ​ลด cognitive load (ภาระ​ทาง​ความคิด) ของ​ทั้ง​ผู้​ใช้​และ​นัก​พัฒนา เมื่อ​ซอฟต์แวร์​ทำงาน​สอดคล้อง​กับ​สิ่ง​ที่​คน​คาด​หวัง คน​ก็​ไม่​ต้อง​หยุด​คิด ไม่​ต้อง​เดา ไม่​ต้อง​ไป​อ่าน source code เพื่อ​ยืนยัน​ว่า method ชื่อ​นี้​จะ​ทำ​อะไร​กัน​แน่

ประโยชน์​ที่​ได้​จาก​การ​ยึด​หลักการ​นี้:

  • ใช้งาน​ง่าย​ขึ้น — ซอฟต์แวร์​ที่​สอดคล้อง​และ​คาด​เดา​ได้​ทำให้​ผู้​ใช้​เรียนรู้​และ​ใช้งาน​ได้​อย่าง​มี​ประสิทธิภาพ โดย​อาศัย mental model ที่​มี​อยู่​แล้ว​จาก​โปรแกรม​อื่น​ที่​คล้าย​กัน
  • ลด​เวลา​พัฒนา​และ debug — เมื่อ codebase คาด​เดา​ได้ นัก​พัฒนา​ไม่​ต้อง​เปิด​อ่าน implementation ทุก​ครั้ง​เพื่อ​ยืนยัน​พฤติกรรม ทำงาน​ได้​เร็ว​ขึ้น​และ​ทำ code review ได้​ง่าย​ขึ้น
  • ดูแล​รักษา​ได้​ง่าย​ขึ้น — codebase ที่​จัด​ระเบียบ​ดี​และ​สม่ำเสมอ ลด technical debt เพราะ​นัก​พัฒนา​ใหม่​เข้า​มา​ต่อยอด​ได้​โดย​ไม่​ต้อง​เดาสุ่ม
  • ลด bug จาก​ความ​เข้าใจ​ผิด — เมื่อ obvious behavior ไม่​ถูก implement ตาม​ที่​ชื่อ​สื่อ ผู้​ใช้ code จะ​สูญเสีย​ความ​ไว้​วางใจ​ใน​สัญชาตญาณ​ของ​ตัวเอง และ​ต้อง​กลับ​ไป​อ่าน​รายละเอียด​ทุก​ครั้ง — นี่​คือ​ต้นทุน​ที่​สะสม​ไป​เรื่อยๆ

หลักการ​นี้​ยัง​ทำงาน​ร่วม​กับ Keep It Simple และ Tell, Don’t Ask โดย​ธรรมชาติ เพราะ​การ​ออกแบบ​ที่​เรียบ​ง่าย​และ​สื่อ​ความหมาย​ตรง​ไป​ตรง​มา​มัก​จะ​คาด​เดา​ได้​อยู่​แล้ว ใน​ขณะ​ที่​การ​ออกแบบ​ที่​ซับซ้อน​หรือ​มี side effect ซ่อน​อยู่​มัก​นำ​ไป​สู่​ความ​ประหลาด​ใจ

POLA นำ​ไป​ใช้ได้​ใน​หลาย​ชั้น​ของ​การ​ออกแบบ​ซอฟต์แวร์:

  • User Interface — ออกแบบ​ให้​สอดคล้อง​กัน​ทั้ง​ภายใน app เอง​และ​กับ​ธรรมเนียม​ของ platform (เช่น ปุ่ม “ลบ” ควร​อยู่​ตำแหน่ง​ที่​คาด​เดา​ได้ ไม่​สลับ​ตำแหน่ง​กับ​ปุ่ม “บันทึก”)
  • API design — ชื่อ method, ค่า default, และ error behavior ควร​สอดคล้อง​กับ​สิ่ง​ที่​ชื่อ​ของ​มัน​บอกใบ้ — operation ชื่อ GetX ไม่​ควร​มี side effect ที่​แก้ไข​ข้อมูล
  • โครงสร้าง code — วาง code ใน​ตำแหน่ง​ที่​ผู้​อ่าน​คาด​ว่า​จะ​เจอ​มัน​ตาม​ธรรมชาติ และ​ตั้ง​ชื่อ variable/method อย่าง​สม่ำเสมอ เช่น ถ้า function หนึ่ง​ตั้ง​ชื่อ processVerificationRequest function ที่​ทำงาน​คล้าย​กัน​ก็​ควร​ตั้ง​ชื่อ​ใน​รูปแบบ​เดียวกัน เช่น processDeletionRequest ไม่ใช่ handleDelete หรือ deleteReq
  • ข้อความ error — ข้อความ​แจ้ง​ข้อ​ผิดพลาด​ควร​ชัดเจน​และ​มี​ข้อมูล​เพียงพอ ไม่​ใช้​ศัพท์​เทคนิค​ที่​ผู้​ใช้​ปลายทาง​ไม่​เข้าใจ

ตัวอย่าง C# ด้าน​ล่าง​แสดง method GetOrCreateCustomer ที่​ชื่อ​บอกว่า​เป็นการ “อ่าน” (get) แต่​กลับ​มี side effect ที่​ไม่​คาด​คิด แล้ว​ปรับปรุง​ให้​พฤติกรรม​ตรง​กับ​ชื่อ:

// ก่อนแก้: ชื่อ "GetOrCreateCustomer" บอกว่าแค่ดึงข้อมูล
// แต่จริงๆ แล้วมันเปลี่ยน collection ของ order ที่ผู้เรียกส่งเข้ามา
// และยังส่ง welcome email แบบไม่มีใครคาดคิด — สร้างความประหลาดใจ 2 ต่อ
public class CustomerService
{
public Customer GetOrCreateCustomer(string email, List<Order> orders)
{
var customer = _repository.FindByEmail(email);
if (customer is null)
{
customer = new Customer(email);
_repository.Save(customer);
_emailSender.SendWelcomeEmail(customer); // side effect ที่ซ่อนอยู่
}
orders.Add(new Order(customer.Id, DateTime.UtcNow)); // แก้ไข list ของผู้เรียก!
return customer;
}
}
// เรียกใช้แล้วงงว่าทำไม orders ของฉันถึงมีรายการเพิ่มขึ้นมาเอง
var orders = new List<Order>();
var customer = customerService.GetOrCreateCustomer("a@b.com", orders);
// หลังแก้: แยกความรับผิดชอบตามที่ชื่อสื่อ ไม่มี side effect แอบแฝง
// ผู้เรียกควบคุม flow ได้เอง และคาดเดาผลลัพธ์ได้จากชื่อ method
public class CustomerService
{
// ชื่อบอกตรงๆ ว่า "หาหรือสร้าง" ลูกค้า ไม่แตะ order ใดๆ
public Customer GetOrCreateCustomer(string email)
{
var customer = _repository.FindByEmail(email);
if (customer is null)
{
customer = new Customer(email);
_repository.Save(customer);
_customerCreatedNotifier.Notify(customer); // สื่อสารผ่าน event ชัดเจน ไม่ใช่ side effect ซ่อน
}
return customer;
}
// แยกเป็นอีก method ต่างหาก ชื่อบอกชัดว่าเปลี่ยน state ของ order
public Order PlaceOrder(Customer customer)
=> new Order(customer.Id, DateTime.UtcNow);
}
// เรียกใช้แล้วคาดเดาได้ทุกขั้นตอนจากชื่อ method
var customer = customerService.GetOrCreateCustomer("a@b.com");
var order = customerService.PlaceOrder(customer);

code ตัวอย่าง​นี้​แสดง​ให้​เห็น​ว่า naming, side effect, และ​การ​แบ่ง responsibility ล้วน​เป็น​แหล่ง​ของ “ความ​ประหลาด​ใจ” ที่​พบ​บ่อย​ที่สุด​ใน code C#

  • ชื่อ method บอกว่า​เป็นการ​อ่าน (Get..., Is..., Calculate...) แต่​กลับ​แก้ไข state หรือ​มี side effect ที่​ผู้​เรียก​มอง​ไม่​เห็น
  • API สอง​ตัว​ที่​ทำงาน​คล้าย​กัน​แต่​มีชื่อ, parameter, หรือ return type ไม่​สอดคล้อง​กัน (บาง method โยน exception เมื่อ​หาไม่​เจอ บาง method คืน null เงียบๆ)
  • ปุ่ม​หรือ​เมนู​ใน UI ที่​ทำงาน​ต่าง​จาก​ธรรมเนียม​ทั่วไป​ของ platform โดย​ไม่มี​เหตุผล​ชัดเจน
  • ค่า default ที่​ขัด​กับ​สามัญสำนึก เช่น field ที่​ดู optional แต่​ระบบ​กลับ​ปฏิเสธ​ถ้า​ไม่​กรอก โดย​ไม่มี​ข้อความ​อธิบาย
  • error message ที่​คลุมเครือ​หรือ​ใช้​ศัพท์​ภายใน​ระบบ ทำให้​ผู้​ใช้​ต้อง​เดา​ว่า​เกิด​อะไร​ขึ้น
  • code ที่​ทีม​ต้อง​คอย “จำ” ข้อ​ยกเว้น​พิเศษ เช่น “ห้าม​เรียก method A ก่อน method B” โดย​ไม่มี​อะไร​บังคับ​หรือ​สื่อสาร​ลำดับ​นั้น​ใน code — เป็น​สัญญาณ​ของ temporal coupling ที่​มอง​ไม่​เห็น