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

Simple Design

สี่​กฎ​ของ Kent Beck สำหรับ design ที่​เรียบ​ง่าย

Simple Design คือ​แนว​ปฏิบัติ​ที่มา​จาก Extreme Programming (XP) ใน​ยุค 1990s ซึ่ง Kent Beck เสนอ​กฎ​สี่​ข้อ​เพื่อ​ใช้​ตัดสิน​ว่า “design นี้​เรียบ​ง่าย​พอ​หรือ​ยัง” กฎ​เหล่า​นี้​บาง​ครั้ง​ถูก​เรียก​ว่า The Four Commandments หรือ XP Simplicity Rules และ​เรียง​ตาม​ลำดับ​ความ​สำคัญ​จาก​มาก​ไป​น้อย code ควร:

  1. ผ่าน test ทั้งหมด (Passes all tests)
  2. สื่อ​เจตนา​ของ​ผู้​เขียน (Reveals intention)
  3. ไม่มี​ความ​ซ้ำซ้อน (No duplication)
  4. มี​จำนวน class method และ module น้อย​ที่สุด (Fewest elements)

จุด​ที่​สำคัญ​ที่สุด​ของ​แนวคิด​นี้​ไม่ใช่​ตัว​กฎ​แต่ละ​ข้อ แต่​คือ ลำดับ​ความ​สำคัญ ระหว่าง​กฎ Martin Fowler อธิบาย​ไว้​ใน​บทความ BeckDesignRules ว่า​ลำดับ​นี้​บอก​เรา​ว่า​เมื่อ​กฎ​สอง​ข้อ​ขัดแย้ง​กัน ควร​ยึด​กฎ​ที่มา​ก่อน​เป็น​หลัก และ​แม้​กฎ​ข้อ​ที่ 2 (สื่อ​เจตนา) กับ​ข้อ​ที่ 3 (ไม่​ซ้ำซ้อน) จะ​ดูเหมือน​แข่ง​กัน​ใน​ทาง​ปฏิบัติ แต่ Fowler ชี้​ว่า​ทั้ง​สอง​มัก “ป้อน​กลับ​ซึ่ง​กัน​และ​กัน” ใน​การ​ขัดเกลา code ให้​ดี​ขึ้น​เรื่อย ๆ มากกว่า​จะ​ขัดแย้ง​กัน​จริงจัง

Simple Design ไม่ใช่​การ​หลีก​เลี่ยง design หรือ​การ​เขียน code แบบ​ไม่​คิด​หน้า​คิด​หลัง แต่​คือ​วินัย​ที่​คอย​ถาม​ซ้ำ ๆ ระหว่าง​ทำ TDD และ Refactoring ว่า “ตอน​นี้ design เรียบ​ง่าย​ที่สุด​เท่า​ที่​จำเป็น​สำหรับ test ที่​ผ่าน​อยู่​แล้ว​หรือ​ยัง” มัน​จึง​เป็น​เครื่องมือ​คาน​กับ​แนวโน้ม​ตาม​ธรรมชาติ​ของ​นัก​พัฒนา​ที่​มัก​ออกแบบ​เผื่อ​อนาคต​เกิน​จำเป็น (over-engineering)

กฎ​ข้อ​ที่ 1 — ผ่าน test ทั้งหมด

หาก​คุณ​เป็น​นัก​พัฒนา​สาย XP วิธี​ที่​คุณ​รู้​ว่า code “ทำงาน​ได้” คือ​มัน​ผ่าน automated unit test ที่​พิสูจน์​ว่า​ถูกต้อง กฎ​นี้​อยู่​อันดับ​หนึ่ง​เสมอ เพราะ​ไม่​ว่า design จะ​สวยงาม​แค่​ไหน หาก​มัน​ไม่​ทำงาน​ตาม​ที่​ตั้งใจ ก็​ไร้​ค่า นี่​คือ​เหตุผล​ที่ Simple Design มัก​ถูก​ฝึกฝน​คู่​กับ​วงจร Red-Green-Refactor — เขียน test ให้​ผ่าน​ก่อน (Red → Green) แล้ว​ค่อย​ขัดเกลา design (Refactor) โดย​มี test คอย​เป็น​ตาข่าย​นิรภัย

กฎ​ข้อ​ที่ 2 — สื่อ​เจตนา​ของ​ผู้​เขียน

เมื่อ code ทำงาน​ได้​แล้ว สิ่ง​สำคัญ​รอง​ลง​มา​คือ​ทำให้​มัน ชัดเจน สอดคล้อง และ​สื่อ​ความหมาย code ลึกลับ​ที่​ทำงาน​ได้​นั้น​มี​คุณค่า​น้อย เพราะ​ยาก​ที่​จะ​แก้ไข​หรือ​ดูแล​ใน​อนาคต การ​ตั้ง​ชื่อ​ตัวแปร method และ class ให้​สื่อ​ความหมาย (ดู Naming) คือ​เครื่องมือ​หลัก​ของ​กฎ​ข้อ​นี้ เพื่อ​ให้​เจตนา​นั้น​ชัดเจน​ต่อ​นัก​พัฒนา​ใน​อนาคต รวม​ถึง​ตัวผู้​เขียน​เอง​เมื่อ​กลับ​มา​อ่าน code ใน​อีก​หก​เดือน​ข้าง​หน้า

กฎ​ข้อ​ที่ 3 — ไม่มี​ความ​ซ้ำซ้อน

กฎ​นี้​คือ​หลักการ Don’t Repeat Yourself (DRY) ใน​ทาง​ปฏิบัติ Fowler เรียก​กฎ​นี้​ว่า “ทรง​พลัง​อย่าง​แยบยล” (powerfully subtle) เพราะ​การ​ไล่​กำจัด​ความ​ซ้ำซ้อน​อย่าง​เป็น​ระบบ​มัก​จะ “ขับ​ดัน” (drive out) design ที่​ดี​ออก​มา​เอง​โดย​ธรรมชาติ — เมื่อ​เห็น​ตรรกะ​ซ้ำ​กัน​สอง​ที่ ให้​รวบ​เข้า​ไว้​ใน method หรือ class แยก แล้ว​อ้างอิง​จาก​ทุก​จุด​ที่​เคย​ซ้ำ นี่​เป็น​ขั้นตอน refactoring ที่​พบ​บ่อย​ที่สุด​ขั้นตอน​หนึ่ง

แต่​หาก​การ​ทำ​ตาม​กฎ​นี้​ทำให้ code หยุด​ทำงาน (ผิด​กฎ​ข้อ 1) หรือ​ทำให้​เข้าใจ​ยาก​ขึ้น (ผิด​กฎ​ข้อ 2) การ​ยอม​ให้​มี​ความ​ซ้ำซ้อน​บ้าง​ใน​กรณี​นั้น​สำคัญ​กว่า​การ​ยึด DRY อย่าง​เคร่งครัด — ความ​ซ้ำซ้อน​โดย​บังเอิญ (coincidental duplication) ที่มา​จาก​แนวคิด​คนละ​เรื่อง​กัน ไม่​ควร​ถูก​บังคับ​ให้​ใช้​โครงสร้าง​ร่วม​กัน​เพียง​เพราะ​หน้าตา​เหมือน​กัน

กฎ​ข้อ​ที่ 4 — มี​จำนวน​องค์​ประกอบ​น้อย​ที่สุด

กฎ​ข้อ​สุดท้าย​แทบ​ไม่​จำเป็น​ด้วย​ซ้ำ — จะ​สร้าง code มากกว่าที่​จำเป็น​ไป​ทำไม โดย​เฉพาะ​เมื่อ​กำจัด​ความ​ซ้ำซ้อน​ไป​แล้ว แต่​สิ่ง​สำคัญ​ที่สุด​ที่​ต้อง​จำ​คือ​มัน​มี ลำดับ​ความ​สำคัญ​ต่ำ​ที่สุด อย่า​กังวล​ว่า​กำลัง​สร้าง class และ method เล็ก ๆ จำนวน​มาก​ที่​ทำ​สิ่ง​เดียว​และ​ทำได้​ดี ตราบ​ใด​ที่​ไม่​ได้​ละเมิด​กฎ​สาม​ข้อ​แรก และ​หาก​ระหว่าง​ทำ​ตาม​กฎ​สาม​ข้อ​แรก​อยู่ คุณ​พบ​ว่า​สามารถ​ลด​จำนวน​โครงสร้าง​ใน design ลง​ได้ ก็​จง​ทำ​เพื่อ​ทำให้​ทุก​อย่าง​เรียบ​ง่าย​ขึ้น กฎ​นี้​คือ​คำ​เตือน​ไม่​ให้​เผื่อ​ความ​ยืดหยุ่น​ใน​อนาคต​ที่​ยัง​ไม่มี test รองรับ ซึ่ง​สอดคล้อง​กับ​หลักการ YAGNI

แผนภาพ​ด้าน​ล่าง​สรุป​ว่า​เวลา​มี design ให้​ทบทวน ควร​ไล่​ตรวจ​ตาม​ลำดับ​ความ​สำคัญ​อย่างไร:

flowchart TD
    Start[เริ่มทบทวน design] --> Q1{test ผ่านทั้งหมดไหม}
    Q1 -->|ไม่ผ่าน| Fix1[แก้จนผ่านก่อน ยังไม่ต้อง refactor]
    Q1 -->|ผ่าน| Q2{code สื่อเจตนาชัดเจนไหม}
    Q2 -->|ไม่ชัด| Fix2[ปรับชื่อและโครงสร้างให้สื่อความหมาย]
    Q2 -->|ชัดแล้ว| Q3{มีความซ้ำซ้อนไหม}
    Q3 -->|มี| Fix3[รวบตรรกะซ้ำเข้าจุดเดียว]
    Q3 -->|ไม่มี| Q4{ลดจำนวนองค์ประกอบได้อีกไหม}
    Q4 -->|ได้| Fix4[ลบ class method module ที่เกินจำเป็น]
    Q4 -->|ไม่ได้แล้ว| Done[design เรียบง่ายเพียงพอ]
    Fix2 --> Q3
    Fix3 --> Q4

JB Rainsberger เสนอ​มุมมอง​ของ​เขา​เอง​ใน​ชื่อ The Four Elements of Simple Design ซึ่ง​เรียนรู้​มา​จาก​งาน​ของ Kent Beck โดยตรง แต่​จัด​ลำดับ​ความ​สำคัญ​ต่าง​ออก​ไป​เล็กน้อย: ผ่าน test → ลด​ความ​ซ้ำซ้อน​ให้​น้อย​ที่สุด → เพิ่ม​ความ​ชัดเจน​ให้​มาก​ที่สุด → มี​องค์​ประกอบ​น้อย​ที่สุด ความ​ต่าง​สำคัญ​คือ Rainsberger ให้ “ลด​ความ​ซ้ำซ้อน” มา​ก่อน “เพิ่ม​ความ​ชัดเจน” เพราะ​ความ​ซ้ำซ้อน​วัดผล​ได้​ค่อน​ข้าง​เป็น​วัตถุวิสัย (มอง​เห็น​แล้ว​เห็น​ตรง​กัน) ใน​ขณะ​ที่​ความ​ชัดเจน​ของ​ชื่อ​ยัง​เป็น​เรื่อง​อัตวิสัย​กว่า เขา​สรุป​ว่า​ทักษะ​หลัก​ของ​การ​ออกแบบ​เชิง​วัตถุ​แทบ​ทั้งหมด​ย่อ​ลง​เหลือ​แค่​สอง​อย่าง: การ​กำจัด​ความ​ซ้ำซ้อน และ​การ​ปรับปรุง​ชื่อ​ให้​ดี​ขึ้น

ตัวอย่าง​ต่อ​ไป​นี้​แสดง​การ​ไล่ refactor code ตาม​ลำดับ​กฎ​ทั้ง​สี่​ข้อ เริ่ม​จาก code ที่​ผ่าน test แต่​ยัง​ไม่​เรียบ​ง่าย ไป​จนถึง design ที่​สื่อ​เจตนา ไม่​ซ้ำซ้อน และ​มี​องค์​ประกอบ​น้อย​ที่สุด

// ก่อน: ผ่าน test (กฎ 1) แต่ตรรกะซ้ำและตั้งชื่อไม่สื่อเจตนา
public class OrderProcessor
{
public decimal CalcA(decimal price, int qty)
{
decimal total = price * qty;
if (total > 1000) total = total - (total * 0.1m);
return total;
}
public decimal CalcB(decimal price, int qty, bool isVip)
{
decimal total = price * qty;
if (total > 1000) total = total - (total * 0.1m);
if (isVip) total = total - (total * 0.05m);
return total;
}
}
// หลัง: สื่อเจตนา (กฎ 2) ไม่ซ้ำซ้อน (กฎ 3)
// และรวม2 method เหลือหนึ่งจุดตัดสินใจ (กฎ 4)
public class OrderTotalCalculator
{
private const decimal BulkOrderThreshold = 1000m;
private const decimal BulkDiscountRate = 0.10m;
private const decimal VipDiscountRate = 0.05m;
public decimal CalculateTotal(decimal unitPrice, int quantity, bool isVipCustomer = false)
{
decimal subtotal = unitPrice * quantity;
decimal total = ApplyBulkDiscount(subtotal);
if (isVipCustomer)
{
total = ApplyDiscount(total, VipDiscountRate);
}
return total;
}
private decimal ApplyBulkDiscount(decimal amount)
=> amount > BulkOrderThreshold ? ApplyDiscount(amount, BulkDiscountRate) : amount;
private decimal ApplyDiscount(decimal amount, decimal rate)
=> amount - (amount * rate);
}

สังเกต​ว่า code “หลัง” ไม่​ได้​มี class หรือ interface เพิ่ม​เผื่อ​อนาคต (เช่น IDiscountStrategy ที่​ยัง​ไม่มี test เรียกร้อง) — นั่น​คือ​การ​เคารพ​กฎ​ข้อ​ที่ 4 อย่าง​เคร่งครัด: เพิ่ม​โครงสร้าง​ก็​ต่อ​เมื่อ test หรือ​ความ​ซ้ำซ้อน​บังคับ​ให้​ต้อง​เพิ่ม​จริง ๆ ไม่ใช่​เพราะ “คิด​ว่า​อาจ​ต้อง​ใช้​ใน​อนาคต”

ประโยชน์

  • ให้​เกณฑ์​ที่​เป็น​รูปธรรม​และ​เรียง​ลำดับ​ได้​ชัดเจน แทนที่​จะ​ถกเถียง​ว่า “design นี้​ดี​ไหม” อย่าง​เลื่อนลอย
  • ลด​ต้นทุน​การ​ดูแล​รักษา​ใน​ระยะ​ยาว เพราะ code ที่​ผ่าน test สื่อ​เจตนา และ​ไม่​ซ้ำซ้อน มัก​จะ​แก้ไข​และ​ต่อยอด​ได้​ง่าย​กว่า
  • ทำงาน​เข้า​คู่​กับ TDD ได้​เป็น​อย่าง​ดี — วงจร Red-Green-Refactor คือ​สนาม​ฝึก​กฎ​ทั้ง​สี่​ข้อ​ซ้ำ ๆ ใน​ทุกรอบ​เล็ก ๆ ของ​การ​พัฒนา
  • ป้องกัน​การ​ออกแบบ​เผื่อ​อนาคต​ที่​ยัง​ไม่​เกิด​ขึ้น​จริง (speculative generality) ซึ่ง​มัก​เพิ่ม​ความ​ซับซ้อน​โดย​ไม่​จำเป็น

ข้อ​ควร​ระวัง

  • ลำดับ​ความ​สำคัญ​มี​ไว้​ให้​ใช้​เวลา​กฎ​ขัดแย้ง​กัน​เท่านั้น ไม่ใช่​ข้อ​อ้าง​ให้​ละเลย​กฎ​ข้อ​หลัง ๆ ไป​เลย
  • การ​ไล่​ตาม “ไม่​ซ้ำซ้อน” อย่าง​สุด​โต่ง​อาจ​นำ​ไป​สู่​ความ​ซ้ำซ้อน​โดย​บังเอิญ​ถูกรวม​เข้า​ด้วย​กัน​ผิด​ที่​ผิด​ทาง กลาย​เป็นการ​ผูกมัด (coupling) กับ abstraction ที่​ผิด ซึ่ง​แก้ไข​ยาก​กว่า​ความ​ซ้ำซ้อน​เสีย​อีก
  • Simple Design ไม่​ได้​แปล​ว่า “ง่าย​ใน​สายตา​แรก” แต่​หมาย​ถึง​เรียบ​ง่าย​ที่สุดเท่า​ที่​จำเป็นสำหรับ​ความ​ต้องการ​ที่​มี test รองรับ​อยู่​จริง — design ที่​ง่าย​เกิน​ไป​จน​ขาด​โครงสร้าง​ที่​จำเป็น​ก็​ผิด​กฎ​ข้อ 2 เช่น​กัน
  • ต้อง​มี​ชุด test ที่​ครอบคลุม​และ​เชื่อถือ​ได้​ก่อน มิ​ฉะนั้น​กฎ​ข้อ 1 จะ​กลาย​เป็น​ภาพลวงตา และ​การ refactor ตาม​กฎ​ข้อ​อื่น​จะ​เสี่ยง​ทำ code พัง