[Design Pattern#2] Tell don't ask หลักการเพื่อลดความยุ่งยากในการคุมสถานะอ็อบเจ็กต์
บทความก่อนหน้านี้เราได้รู้จักหลักการ Law of Demeter ที่บอกเป็นหลักการกว้างๆไว้ว่า อ็อบเจ็กต์ควรเข้าถึงได้เพียงหน่วย (unit) ที่สัมพันธ์โดยตรงกับมัน ไม่อนุญาตให้เข้าถึง unit ที่ไม่เกี่ยวข้องโดยตรง วันนี้เราจะมาคุยกันในหลักการที่เรียกว่า Tell don't ask ซึ่งเป็นหลักการที่ใกล้เคียงกับ Law of Demeter จากในบทความที่แล้วครับ
จัดการสถานะ (state) ของอ็อบเจ็กต์อื่นด้วยตนเองนั้นไม่ดี
สมมติเราอยู่ที่ร้านอาหารซีฟู๊ดแห่งหนึ่ง คุณหิวมากจนน้ำลายฟูมปาก คุณจึงเริ่มเรียกพ่อครัวมาเพื่อจะบอกสูตรเด็ดในการทำต้มยำกุ้งล๊อบสเตอร์เพื่อนำมาทำจานโปรดให้คุณ เริ่มจากคุณขอดูกุ้งของทางร้าน เลือกกุ้งที่สดใหม่เองกับมือ เลือกปริมาณพริกเพียงเล็กน้อยเพราะคุณไม่กินเผ็ด สุดท้ายจึงส่งวัตถุดิบที่เลือกกลับไปให้พ่อครัว นี่ถ้าไม่ติดว่าคุณปรุงอาหารเองไม่เป็นเราคงได้ลงมือทำกันไปแล้ว
ความเป็นจริงคือเราเพียงบอกพ่อครัวว่า ช่วยทำต้มยำกุ้งล๊อบสเตอร์ ขอแบบเผ็ดน้อยนะ
แค่นี้ทุกอย่างก็เรียบร้อยแล้วใช่ไหมครับ? นั่นละฮะคือหลักการของ Tell don't ask
หลักการของ Tell don't ask คือเราอย่าถามอ็อบเจ็กต์ว่าสถานะของอ็อบเจ็กต์คืออะไร เราไม่นำสถานะของอ็อบเจ็กต์ที่ถามมาได้เพื่อไปคำนวณอะไรซักอย่าง แต่เราจะบอกอ็อบเจ็กต์เพื่อให้ทำสิ่งที่เราต้องการแทน ลองพิจารณาตัวอย่างต่อไปนี้ครับ
1function getAddress(user) {2 // ถ้า user มี address ให้คืนค่ากลับเป็น address3 if (user.address) {4 return user.address5 } else {6 return 'No address'7 }8}
จากตัวอย่างข้างบนเพื่อนๆคงเห็นแล้วว่าฟังก์ชัน getAddress ของเราดึงสถานะหรือ state ของอ็อบเจ็กต์ user ออกมาเพื่อตรวจสอบว่ามี address หรือไม่ ถ้าไม่มีจะคืนค่ากลับเป็น No address วิธีการนี้นั้นขัดกับหลักการ Tell don't ask นั่นเป็นเพราะเราถาม (ask) อ็อบเจ็กต์ user ถึงสถานะที่มันเก็บเอาไว้ ฉะนั้นแล้วจึงควรเปลี่ยนการถามเป็นการบอกอ็อบเจ็กต์ user ถึงสิ่งที่เราต้องการให้ทำแทนดังนี้
1class User {2 getAddress() {3 return this.address || 'No address'4 }5}67function getAddress(user) {8 // เราไม่ ask เพื่อดึงสถานะออกมาจาก user9 // แต่เราจะ tell คือบอก user ว่าเราต้องการ address10 // ที่เหลือเป็นหน้าที่ของ user ที่จะจัดการกับสถานะของ address ด้วยตัวมันเอง11 // โดยเราไม่ต้องรับรู้ถึงสถานะในตัวมันเลย12 return user.getAddress()13}
ลองดูอีกซักตัวอย่างครับ สมมติเรามีหน้า dashboard สำหรับทั้ง admin และผู้ใช้งานระบบทั่วไป เมื่อผู้ใช้งานระบบเข้ามาเราต้องทำการตรวจสอบก่อน หากเป็นแอดมินเราจะแสดงหน้า dashboard พิเศษสำหรับแอดมิน แต่หากไม่ใช่แล้วก็ให้แสดงหน้า dashboard สำหรับผู้ใช้งานระบบทั่วไปแทน
1if (user.isAdmin) {2 render(user.getAdminDashboard())3} else {4 render(user.getUserDashboard())5}
วิธีข้างต้นยังคงขัดกับหลัก Tell don't ask เพราะเราถามถึงสถานะความเป็นแอดมินของ user ก่อนจะตัดสินใจว่าจะเลือกเพจแบบไหนมาแสดงผลดี จึงควรเปลี่ยนจากการถามเป็นการออกคำสั่งแทนแบบนี้
1// บอก user ว่าต้องการ dashboard นะ2// ที่เหลือเป็นหน้าที่ของ user เองที่จะจัดการกับสถานะของตนเองอย่างไร3render(user.getDashboard())
คำถามชวนคิด: บริษัทของเราต้องออกรายงาน (Report) ทุกๆปีโดยรายงานดังกล่าวประกอบด้วยรายชื่อลูกค้าทั้งหมด (Customer) และรายชื่อผู้จัดการสาขาทั้งหมด (Manager) เราจึงออกแบบระบบของเราดังนี้
1class Customer {2 private name : string;3 // ถ้าเป็น true เราจะเปิดเผยข้อมูลส่วนตัวคือ tel ในรายงานไม่ได้4 private isPrivate : boolean;5 private tel : string;67 getName() : string {8 return this.name;9 }1011 isPrivate() : boolean {12 return this.isPrivate;13 }1415 getTel() : string {16 return this.tel;17 }18}1920class Manager {21 private name : string;22 // เป็นผู้จัดการสาขาอะไร?23 private branch : string;2425 getName() : string {26 return this.name;27 }2829 getBranch() : string {30 return this.branch;31 }32}3334class Report {35 private customers : Customer[];36 private managers : Manager[];3738 constructor(customers : Customer[], managers : Manager[]) {39 this.customers = customers;40 this.order = managers;41 }4243 getHeaders() { ... }44 getFooters() { ... }4546 print() {47 // พิมพ์ส่วนหัวของรายงาน48 render(this.getHeaders())49 // วนลูปรอบ customers ทั้งหมดเพื่อพิมพ์ชื่อออกมาในรายงาน50 this.customers.forEach(51 (customer) => {52 if(customer.isPrivate()) {53 render(customer.getName())54 } else {55 render(`${customer.getName()} ${customer.getTel()}`)56 }57 }58 )59 // วนลูปรอบ managers ทั้งหมดเพื่อพิมพ์ชื่อและสาขาออกมาในรายงาน60 this.managers.forEach(61 (manager) => render(`${manager.getName()} ${manager.getBranch()}`)62 )63 // พิมพ์ส่วนท้ายของรายงาน64 render(this.getFooters())65 }66}6768const report = new Report(customers, managers)69report.print()
จากตัวอย่างโค๊ดนี้จะพบว่าเมธอด print ของเราเข้าถึงข้อมูลของทั้ง customers และ managers แบบตรงๆเลย เราต้องรับรู้สถานะภายในของพวกมันเพื่อจะสามารถสร้างรายงานออกมาได้ถูกต้อง เพื่อนๆลองหาวิธีใช้หลักการ Tell don't ask เพื่อเปลี่ยนโค๊ดข้างบนให้ดีขึ้นกันครับ ลองแชร์โค๊ดของเพื่อนๆไว้ในคอมเม้นใต้บทความได้นะครับ
สารบัญ
- จัดการสถานะ (state) ของอ็อบเจ็กต์อื่นด้วยตนเองนั้นไม่ดี