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

Tell, Don't Ask

สั่ง​ให้ object ทำงาน อย่า​ถาม​สถานะ​แล้ว​ตัดสิน​ใจ​แทน​มัน

Tell, Don’t Ask (TDA) เป็น​หลักการ​ออกแบบ​เชิง​วัตถุ​ที่​บอกว่า แทนที่​ผู้​เรียก (caller) จะ​สอบถาม​สถานะ​ภายใน​ของ object แล้ว​นำ​ไป​ใช้​ตัดสิน​ใจ​และ​กระทำ​การ​แทน​มัน​เอง ผู้​เรียก​ควร​จะ “สั่ง” (tell) ให้ object นั้น​ทำงาน​หรือ​ดำเนิน​ตรรกะ​ด้วย​ตัวเอง Martin Fowler อธิบาย​ไว้​ใน bliki ของ​เขา​ว่า​หลักการ​นี้​ช่วย​เตือน​ใจ​ว่า​แก่น​แท้​ของ object-orientation คือ​การ​ผูก data เข้า​กับ function ที่​ทำงาน​บน data นั้น​ไว้​ด้วย​กัน แทนที่​จะ​ดึง​ข้อมูล​ออก​มา​แล้ว​เขียน​ตรรกะ​ไว้​ข้าง​นอก — TDA จึง​ผลักดัน​ให้ behavior ย้าย​เข้าไป​อยู่​กับ data ที่​มัน​เกี่ยวข้อง

รากศัพท์​ของ​หลักการ​นี้​มัก​ถูก​โยง​ถึง​กลุ่ม Pragmatic Programmers (Andy Hunt และ Dave Thomas) และ​แพร่หลาย​ใน​ชุมชน object-oriented design ผ่าน​การ​อภิปราย​บน comp.object และ c2 wiki ตั้งแต่​ปลาย​ทศวรรษ 1990 มัน​มี​ความหมาย​เดียวกัน​โดย​พื้นฐาน​กับ​หลักการ Information Expert ใน GRASP: ให้​มอบ​หมายความ​รับผิดชอบ​แก่ module ที่ “รู้” ข้อมูล​ที่​จำเป็น​มาก​ที่สุด​สำหรับ​งาน​นั้น เพราะ​ถ้า module อื่น​ต้อง​มา​ถาม​ข้อมูล​ออก​ไป​คำนวณ​เอง ก็​เท่ากับ​ละเมิด TDA ไป​พร้อม​กัน

คุณ​สังเกต​การ​ละเมิด TDA ได้​ง่าย​ใน code ที่​สอบถาม​หรือ​ใช้ property หลาย​ตัว​ของ object เพื่อ​ทำการ​คำนวณ ยิ่ง​เป็น​ปัญหา​เมื่อ​การ​คำนวณ​แบบ​เดียวกัน​ถูก​ทำ​ใน​หลาย​ที่ (ละเมิด Don’t Repeat Yourself) แต่​ก็​อาจ​สะท้อน​ข้อ​บกพร่อง​ของ​การ​ออกแบบ​แม้​จะ​เกิด​เพียง​ที่​เดียว​ใน codebase ปัจจุบัน หลักการ​นี้​ยัง​เกี่ยวข้อง​อย่าง​ใกล้​ชิด​กับ Law of Demeter ซึ่ง​จำกัด​ว่า object ควร “คุย​กับ​ใคร​ได้​บ้าง” ใน​ขณะ​ที่ TDA จำกัด​ว่า “คุย​กัน​แบบ​ไหน” — ทั้ง​สอง​หลักการ​ทำงาน​ร่วม​กัน​เพื่อ​รักษา loose coupling: Law of Demeter ป้องกัน message chain ที่​ไล่​เจาะ​เข้าไป​ใน​โครงสร้าง​ภายใน​ของ object อื่น ส่วน TDA ป้องกัน​ไม่​ให้​ผู้​เรียก​ดึงสถานะ​ออก​มา​ตัดสิน​ใจ​แทน​เจ้าของ​สถานะ​นั้น

แผนภาพ​ด้าน​ล่าง​เปรียบเทียบ​ทิศทาง​ของ​ตรรกะ​ระหว่าง​สอง​แบบ:

flowchart LR
    subgraph AskStyle
        Client1[Client] --> Getter[Call getter for state]
        Getter --> Decide[Client decides and computes]
        Decide --> Setter[Client calls setter]
    end
    subgraph TellStyle
        Client2[Client] --> Command[Tell object what to do]
        Command --> ObjectLogic[Object decides and updates itself]
    end

ทั้งนี้ Fowler เอง​ก็​ให้​ข้อสังเกต​ที่​สำคัญ: เขา​ไม่​ได้​ยึด TDA เป็น​แนว​ปฏิบัติ​หลัก​ใน​งาน​ของ​ตัวเอง เพราะ​เห็น​ว่า​บาง​ครั้ง​มัน​ผลักดัน​ให้​โปรแกรมเมอร์​กลาย​เป็น “Getter Eradicator” ที่​พยายาม​กำจัด query method ทั้งหมด​ออก​จาก class จน code ซับซ้อน​เกิน​จำเป็น สำหรับ​เขา TDA เป็น​เพียง “บันได​ขั้น​หนึ่ง” ที่​นำ​ไป​สู่​เป้าหมาย​ที่แท้​จริง​คือ​การ​รวม data กับ behavior ไว้​ด้วย​กัน ไม่ใช่​กฎ​เหล็ก​ที่​ต้อง​ทำ​ตาม​ทุก​กรณี — object ยัง​สามารถ​เปิดเผย query ได้​ตาม​ปกติ ตราบ​ใด​ที่​ผู้​เรียก​ไม่​นำ​ค่าที่​ได้​ไป​ตัดสิน​ใจ​เรื่อง​ที่​เป็น​ความ​รับผิดชอบ​ของ object นั้น​เอง

  • รักษา encapsulation — เมื่อ business rule และ magic number (เช่น threshold, limit) ถูก​ฝัง​อยู่​ใน​ตัว object แทนที่​จะ​กระจัดกระจาย​อยู่​ใน code ผู้​เรียก​หลาย​ที่ ตัว object จึง​เป็น​เจ้าของ​กฎ​ของ​ตัวเอง​อย่าง​แท้จริง ไม่มี​ใคร “ผิด​กฎ” ได้​จาก​ภายนอก
  • ลด coupling ระหว่าง object — ผู้​เรียก​ที่​แค่ “สั่ง” ไม่​จำเป็น​ต้อง​รู้จัก​โครงสร้าง​ภายใน​ของ object ที่​เรียก ทำให้​เปลี่ยน​วิธี​คำนวณ​ภายใน​ได้​โดย​ไม่​กระทบ​ผู้​เรียก สอดคล้อง​กับ​หลักการ Encapsulation
  • ป้องกัน Anemic Domain Model — Robert C. Martin ชี้​ว่าการ​แยก data ออก​จาก behavior (เก็บ data ไว้​ใน object แต่​ตรรกะ​ไป​อยู่​ใน service ชั้น​นอก) ขัด​กับ​แก่น​ของ object-orientation โดยตรง เพราะ class ควร​ทำให้ function มอง​เห็น​ได้​ใน​ขณะ​ที่ data ถูก​ซ่อน​ไว้ ตรง​ข้าม​กับ data structure ที่​เปิดเผย data แต่​ซ่อน function การ​ยึด TDA ช่วย​ผลักดัน​ให้ domain object เป็น “rich model” ที่​มี​ทั้ง​ข้อมูล​และ​พฤติกรรม​ของ​ตัวเอง
  • ลด​การ​ซ้ำ​ของ business rule — ถ้า​ตรรกะ​การ​ตัดสิน​ใจ​อยู่​ใน​ที่​เดียว​คือ​ตัว object เอง ก็​ไม่มี​ความ​เสี่ยง​ที่​จุด​ต่าง ๆ ใน​ระบบ​จะ​คำนวณ​กฎ​เดียวกัน​ไม่​ตรง​กัน ซึ่ง​สนับสนุน Don’t Repeat Yourself
  • ทดสอบ​ง่าย​ขึ้น — เมื่อ​พฤติกรรม​อยู่​ติด​กับ data ที่​มัน​ดำเนิน​การ​ด้วย การ​ทดสอบ​ก็​ทำได้​ใน​ระดับ unit เดียว​โดย​ไม่​ต้อง​ประกอบ​ตรรกะ​จาก​หลาย class เข้า​ด้วย​กัน

ตัวอย่าง​ด้าน​ล่าง​จำลอง​ระบบ monitor ค่า CPU ที่​ต้อง​แจ้ง​เตือน​เมื่อ​ค่า​เกิน threshold ที่​กำหนด ไล่​ตั้งแต่​ละเมิด TDA อย่าง​ชัดเจน ไป​จนถึง​ปรับปรุง​ที​ละ​ขั้น

ผู้​เรียก​ดึง​ค่า​จาก monitor แต่ละ​ตัว​มา​เทียบ​กับ threshold เอง (มี magic number คือ 90 ปน​อยู่​ใน code ผู้​เรียก) — นี่​คือ “ถาม” แล้ว​ตัดสิน​ใจ​แทน​ตัว object:

// ละเมิด TDA
public class CpuMonitor
{
public int Value { get; set; }
}
public class Client
{
public void AlertService(List<CpuMonitor> cpuMonitors)
{
foreach (var cpuMonitor in cpuMonitors)
{
if (cpuMonitor.Value > 90)
{
// แจ้งเตือน
}
}
}
}

ย้าย magic number ที่​แทน threshold เข้าไป​ใน​ตัว monitor เอง — ดี​ขึ้น​ตรง​ที่​กฎ​ถูก​ฝัง​ไว้​ใน​ที่​เดียว แต่​ผู้​เรียก​ยัง​คง “ถาม” ค่า ExceedsThreshold แล้ว​ตัดสิน​ใจ​ว่า​จะ​ทำ​อะไร​ต่อ​เอง​อยู่ดี ยัง​ไม่ใช่ “สั่ง”:

// ปรับปรุงแล้ว - threshold ย้ายเข้ามาแล้ว แต่ยังถามสถานะอยู่
public class CpuMonitor
{
private readonly int _alertThreshold;
public CpuMonitor(int alertThreshold)
{
_alertThreshold = alertThreshold;
}
public int Value { get; set; }
public bool ExceedsThreshold { get { return Value >= _alertThreshold; } }
}
public class Client
{
public void AlertService(List<CpuMonitor> cpuMonitors)
{
foreach (var cpuMonitor in cpuMonitors)
{
if (cpuMonitor.ExceedsThreshold)
{
// แจ้งเตือน
}
}
}
}

ย้าย​ความ​รับผิดชอบ​ใน​การ​ส่ง​การ​แจ้ง​เตือน​เข้าไป​ใน​ตัว monitor เอง โดย​ยัง​ไม่​ผูก​แน่น​กับ implementation ของ​การ​แจ้ง​เตือน​ใด ๆ (ส่ง​ผ่าน Action เข้า​มา​ตอน construct) ตอน​นี้​ผู้​เรียก​แค่ “สั่ง” ให้​ตัวอย่าง​ข้อมูล (Sample()) แล้ว monitor จัดการ​ตัดสิน​ใจ​และ​กระทำ​การ​เอง​ทั้งหมด:

// ปรับปรุงเพิ่มเติม - ผู้เรียกสั่ง ("tell") object ให้ทำงาน โดยไม่ต้องรู้เกณฑ์ภายใน
public class CpuMonitor
{
private readonly int _alertThreshold;
private readonly Action<CpuMonitor> _alertAction;
public CpuMonitor(int alertThreshold, Action<CpuMonitor> alertAction)
{
_alertThreshold = alertThreshold;
_alertAction = alertAction;
}
public int Value { get; set; }
public bool ExceedsThreshold { get { return Value >= _alertThreshold; } }
public void Sample()
{
if (ExceedsThreshold)
{
_alertAction(this);
}
}
}
public class Client
{
public void AlertService(List<CpuMonitor> cpuMonitors)
{
foreach (var cpuMonitor in cpuMonitors)
{
cpuMonitor.Sample();
}
}
}

การ refactor ครั้ง​สุดท้าย​ย้าย​ความ​รับผิดชอบ​ใน​การ​ส่ง​การ​แจ้ง​เตือน​เข้าไป​ใน​ตัว monitor พร้อม​กับ​หลีก​เลี่ยง​การ​ผูก​แน่น​กับ implementation ของ​การ​แจ้ง​เตือน​ใด ๆ monitor จึง​ยัง loosely coupled จาก​ระบบ​แจ้ง​เตือน​ที่​ใช้​อยู่ สังเกต​ว่า ExceedsThreshold ยัง​คง​เป็น query ที่​เปิดเผยอยู่ — TDA ไม่​ได้​ห้าม​การ​มี query เสมอ​ไป เพียง​แต่​ตรรกะ​ที่​ตัดสิน​ใจ “ต้อง​ทำ​อะไร​ต่อ” ถูก​ย้าย​เข้าไป​อยู่​กับ​ตัว object แล้ว​เท่านั้น

ไม่​จำเป็น​ต้อง​ยึด “อย่า​ถาม” สุด​โต่ง​ถึง​ขั้น​กำจัดการ​เข้าถึง​สถานะ​ทั้งหมด โดย​ทั่วไป​การ​สอบถาม​สถานะ​ของ object ทำได้ ตราบ​ใด​ที่​ข้อมูล​นั้น​ไม่​ได้​ถูก​ใช้​ตัดสิน​ใจ​เรื่อง​ที่​เกี่ยว​กับ​ตัว object เอง หาก​ใช่ การ​ตัดสิน​ใจ​นั้น​และ​พฤติกรรม​ที่​ตาม​มา​ควร​ถูก​ย้าย​เข้าไป​ใน​ตัว object อีก​ผล​หนึ่ง​ของ​การ​ละเมิด TDA คือ magic number หรือ​กฎ​ทาง​ธุรกิจ​มัก​กระจัดกระจาย​ใน code ที่​อ้าง​ถึง​สถานะ​ของ object แทนที่​จะ​ฝัง​อยู่​ใน​ตัว object หรือ​ถูก​ส่ง​เข้า​มา​ใน​รูป​ที่​มีชื่อ​ชัดเจน (เช่น CpuAlertThreshold)

  • เรียก getter หลาย​ตัว​ติด​กัน​เพื่อ​คำนวณ​หรือ​ตัดสิน​ใจ เช่น if (order.Status == OrderStatus.Paid && order.Items.Count > 0) แล้ว​ตาม​ด้วย​การ​เปลี่ยน​สถานะ object นั้น​เอง — ตรรกะ​นี้​ควร​อยู่​ใน method ของ Order เอง ไม่ใช่​กระจาย​อยู่​ใน code ผู้​เรียก
  • Feature Envy — method ใน class หนึ่ง​สนใจ (envy) ข้อมูล​ของ​อีก class มากกว่า​ข้อมูล​ของ​ตัวเอง เป็น​สัญญาณ​คลาสสิกว่า​ตรรกะ​ควร​ถูก​ย้าย​ไป​อยู่​ใกล้ data ที่​มัน​ใช้งาน
  • Flags Over Objects — ส่ง flag หรือ enum เข้าไป​ให้ object แล้ว​ให้​ผู้​เรียก branch ตาม​ค่า​นั้น​เอง​แทนที่​จะ​ให้ object ตัดสิน​ใจ ทำให้​ตรรกะ​แตก​กระจาย​และ​ซ้ำซ้อน
  • Anemic Domain Model — domain object กลาย​เป็น​แค่​กล่อง​เก็บ property (getter/setter ล้วน ๆ) ใน​ขณะ​ที่ business logic ทั้งหมด​ย้าย​ไป​อยู่​ใน “service” หรือ “manager” ชั้น​นอก
  • Magic number หรือ​กฎ​ทาง​ธุรกิจ​ซ้ำ​ใน​หลาย​จุด เพราะ​ทุก​ที่​ที่​ต้อง​ตัดสิน​ใจ​เรื่อง​เดียวกัน​ต้อง​ดึงสถานะ​ออก​มา​คำนวณ​เอง แทนที่​จะ​เรียก method เดียว​ที่​ห่อ​กฎ​นั้น​ไว้
  • Test ที่​ต้อง setup mock/stub จำนวน​มาก เพื่อ​ตรวจสอบ​ว่า​ผู้​เรียก “ถาม” ค่า​ถูกต้อง​แล้ว “ตัดสิน​ใจ” ถูกต้อง เป็น​สัญญาณ​ว่า​ตรรกะ​ที่​ควร​อยู่​ใน object เดียว​ถูก​กระจาย​ออก​ไป​อยู่​นอก class