[Design Pattern#2] Tell don't ask หลักการเพื่อลดความยุ่งยากในการคุมสถานะอ็อบเจ็กต์

Nuttavut Thongjor

บทความก่อนหน้านี้เราได้รู้จักหลักการ Law of Demeter ที่บอกเป็นหลักการกว้างๆไว้ว่า อ็อบเจ็กต์ควรเข้าถึงได้เพียงหน่วย (unit) ที่สัมพันธ์โดยตรงกับมัน ไม่อนุญาตให้เข้าถึง unit ที่ไม่เกี่ยวข้องโดยตรง วันนี้เราจะมาคุยกันในหลักการที่เรียกว่า Tell don't ask ซึ่งเป็นหลักการที่ใกล้เคียงกับ Law of Demeter จากในบทความที่แล้วครับ

จัดการสถานะ (state) ของอ็อบเจ็กต์อื่นด้วยตนเองนั้นไม่ดี

สมมติเราอยู่ที่ร้านอาหารซีฟู๊ดแห่งหนึ่ง คุณหิวมากจนน้ำลายฟูมปาก คุณจึงเริ่มเรียกพ่อครัวมาเพื่อจะบอกสูตรเด็ดในการทำต้มยำกุ้งล๊อบสเตอร์เพื่อนำมาทำจานโปรดให้คุณ เริ่มจากคุณขอดูกุ้งของทางร้าน เลือกกุ้งที่สดใหม่เองกับมือ เลือกปริมาณพริกเพียงเล็กน้อยเพราะคุณไม่กินเผ็ด สุดท้ายจึงส่งวัตถุดิบที่เลือกกลับไปให้พ่อครัว นี่ถ้าไม่ติดว่าคุณปรุงอาหารเองไม่เป็นเราคงได้ลงมือทำกันไปแล้ว

ความเป็นจริงคือเราเพียงบอกพ่อครัวว่า ช่วยทำต้มยำกุ้งล๊อบสเตอร์ ขอแบบเผ็ดน้อยนะ แค่นี้ทุกอย่างก็เรียบร้อยแล้วใช่ไหมครับ? นั่นละฮะคือหลักการของ Tell don't ask

หลักการของ Tell don't ask คือเราอย่าถามอ็อบเจ็กต์ว่าสถานะของอ็อบเจ็กต์คืออะไร เราไม่นำสถานะของอ็อบเจ็กต์ที่ถามมาได้เพื่อไปคำนวณอะไรซักอย่าง แต่เราจะบอกอ็อบเจ็กต์เพื่อให้ทำสิ่งที่เราต้องการแทน ลองพิจารณาตัวอย่างต่อไปนี้ครับ

JavaScript
1function getAddress(user) {
2 // ถ้า user มี address ให้คืนค่ากลับเป็น address
3 if (user.address) {
4 return user.address
5 } else {
6 return 'No address'
7 }
8}

จากตัวอย่างข้างบนเพื่อนๆคงเห็นแล้วว่าฟังก์ชัน getAddress ของเราดึงสถานะหรือ state ของอ็อบเจ็กต์ user ออกมาเพื่อตรวจสอบว่ามี address หรือไม่ ถ้าไม่มีจะคืนค่ากลับเป็น No address วิธีการนี้นั้นขัดกับหลักการ Tell don't ask นั่นเป็นเพราะเราถาม (ask) อ็อบเจ็กต์ user ถึงสถานะที่มันเก็บเอาไว้ ฉะนั้นแล้วจึงควรเปลี่ยนการถามเป็นการบอกอ็อบเจ็กต์ user ถึงสิ่งที่เราต้องการให้ทำแทนดังนี้

JavaScript
1class User {
2 getAddress() {
3 return this.address || 'No address'
4 }
5}
6
7function getAddress(user) {
8 // เราไม่ ask เพื่อดึงสถานะออกมาจาก user
9 // แต่เราจะ tell คือบอก user ว่าเราต้องการ address
10 // ที่เหลือเป็นหน้าที่ของ user ที่จะจัดการกับสถานะของ address ด้วยตัวมันเอง
11 // โดยเราไม่ต้องรับรู้ถึงสถานะในตัวมันเลย
12 return user.getAddress()
13}

ลองดูอีกซักตัวอย่างครับ สมมติเรามีหน้า dashboard สำหรับทั้ง admin และผู้ใช้งานระบบทั่วไป เมื่อผู้ใช้งานระบบเข้ามาเราต้องทำการตรวจสอบก่อน หากเป็นแอดมินเราจะแสดงหน้า dashboard พิเศษสำหรับแอดมิน แต่หากไม่ใช่แล้วก็ให้แสดงหน้า dashboard สำหรับผู้ใช้งานระบบทั่วไปแทน

JavaScript
1if (user.isAdmin) {
2 render(user.getAdminDashboard())
3} else {
4 render(user.getUserDashboard())
5}

วิธีข้างต้นยังคงขัดกับหลัก Tell don't ask เพราะเราถามถึงสถานะความเป็นแอดมินของ user ก่อนจะตัดสินใจว่าจะเลือกเพจแบบไหนมาแสดงผลดี จึงควรเปลี่ยนจากการถามเป็นการออกคำสั่งแทนแบบนี้

JavaScript
1// บอก user ว่าต้องการ dashboard นะ
2// ที่เหลือเป็นหน้าที่ของ user เองที่จะจัดการกับสถานะของตนเองอย่างไร
3render(user.getDashboard())

คำถามชวนคิด: บริษัทของเราต้องออกรายงาน (Report) ทุกๆปีโดยรายงานดังกล่าวประกอบด้วยรายชื่อลูกค้าทั้งหมด (Customer) และรายชื่อผู้จัดการสาขาทั้งหมด (Manager) เราจึงออกแบบระบบของเราดังนี้

TypeScript
1class Customer {
2 private name : string;
3 // ถ้าเป็น true เราจะเปิดเผยข้อมูลส่วนตัวคือ tel ในรายงานไม่ได้
4 private isPrivate : boolean;
5 private tel : string;
6
7 getName() : string {
8 return this.name;
9 }
10
11 isPrivate() : boolean {
12 return this.isPrivate;
13 }
14
15 getTel() : string {
16 return this.tel;
17 }
18}
19
20class Manager {
21 private name : string;
22 // เป็นผู้จัดการสาขาอะไร?
23 private branch : string;
24
25 getName() : string {
26 return this.name;
27 }
28
29 getBranch() : string {
30 return this.branch;
31 }
32}
33
34class Report {
35 private customers : Customer[];
36 private managers : Manager[];
37
38 constructor(customers : Customer[], managers : Manager[]) {
39 this.customers = customers;
40 this.order = managers;
41 }
42
43 getHeaders() { ... }
44 getFooters() { ... }
45
46 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}
67
68const report = new Report(customers, managers)
69report.print()

จากตัวอย่างโค๊ดนี้จะพบว่าเมธอด print ของเราเข้าถึงข้อมูลของทั้ง customers และ managers แบบตรงๆเลย เราต้องรับรู้สถานะภายในของพวกมันเพื่อจะสามารถสร้างรายงานออกมาได้ถูกต้อง เพื่อนๆลองหาวิธีใช้หลักการ Tell don't ask เพื่อเปลี่ยนโค๊ดข้างบนให้ดีขึ้นกันครับ ลองแชร์โค๊ดของเพื่อนๆไว้ในคอมเม้นใต้บทความได้นะครับ

สารบัญ

สารบัญ

  • จัดการสถานะ (state) ของอ็อบเจ็กต์อื่นด้วยตนเองนั้นไม่ดี