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 อย่างชัดเจน ไปจนถึงปรับปรุงทีละขั้น
ขั้นที่ 0: ละเมิด TDA
หัวข้อที่มีชื่อว่า “ขั้นที่ 0: ละเมิด TDA”ผู้เรียกดึงค่าจาก monitor แต่ละตัวมาเทียบกับ threshold เอง (มี magic number คือ 90 ปนอยู่ใน code ผู้เรียก) — นี่คือ “ถาม” แล้วตัดสินใจแทนตัว object:
// ละเมิด TDApublic 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) { // แจ้งเตือน } } }}ขั้นที่ 1: ย้าย threshold เข้า object
หัวข้อที่มีชื่อว่า “ขั้นที่ 1: ย้าย threshold เข้า object”ย้าย 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) { // แจ้งเตือน } } }}ขั้นที่ 2: สั่งให้ object ทำงาน
หัวข้อที่มีชื่อว่า “ขั้นที่ 2: สั่งให้ object ทำงาน”ย้ายความรับผิดชอบในการส่งการแจ้งเตือนเข้าไปในตัว 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
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Encapsulation
- Don’t Repeat Yourself
- Law of Demeter
- Anemic Domain Model
- Feature Envy
- Flags Over Objects