[Design Pattern#1] ลดความยุ่งเหยิงของโค๊ดด้วยหลักการ Law of Demeter

Nuttavut Thongjor

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

TypeScript
1// ตัวอย่างโปรแกรมใน TypeScript
2
3// คลาสกระเป๋าตังค์
4class Wallet {
5 // กระเป๋าตังค์ก็ต้องมีตังค์ซิฮะ
6 private money: number
7
8 constructor(money: number) {
9 this.money = money
10 }
11
12 getMoney(): number {
13 return this.money
14 }
15
16 setMoney(money: number) {
17 this.money = money
18 }
19}
20
21class Customer {
22 // ลูกค้าที่เข้ามาในร้านต้องมีกระเป๋าตังค์
23 // ไม่มีตังค์ก็ไปล้างจานตรงนู้่น~~
24 private wallet: Wallet
25
26 constructor(wallet: Wallet) {
27 this.wallet = wallet
28 }
29
30 getWallet(): Wallet {
31 return this.wallet
32 }
33}
34
35class Waiter {
36 private name: string
37
38 constructor(name: string) {
39 this.name = name
40 }
41
42 // เมธอดเก็บตังค์จาก customer ด้วยจำนวนเงินเป็น money
43 collectMoney(customer: Customer, money: number) {
44 const wallet = customer.getWallet()
45 const customerMoney = wallet.getMoney()
46
47 // ถ้าตังค์ไม่พอก็ไปล้างจานเลยคร๊าบ
48 if (customerMoney < money) {
49 throw new Error('Not enough!')
50 }
51
52 // แต่ถ้าในกระเป๋าตังค์มีเงินพอก็หักเงินออกไป
53 wallet.setMoney(customerMoney - money)
54 }
55}
56
57// ทั้งเนื้อทั้งตัวมีอยู่ 1,000
58const wallet = new Wallet(1000)
59const customer = new Customer(wallet)
60const waiter = new Waiter('Adam Lorem')
61
62// พนักงงานเก็บค่าอาหารไป 750
63waiter.collectMoney(customer, 750)
64// ตัวเบาหวิวเหลือตังค์กลับบ้านแค่ 250
65console.log(wallet.getMoney())

โค๊ดนี้ทำงานได้อย่างถูกต้องครับ แต่ด้วยดีไซน์ที่ผิดพลาด เราเจอปัญหาเข้าแล้ว

การออกแบบอ็อบเจ็กต์ให้รู้มากไปนั้นไม่ดี!

จากการประมวลผลโค๊ดข้างบนทั้งหมด เราลองมาแปลเป็นภาษามนุษย์เล่นๆกันครับ เริ่มจากลูกค้าที่เข้ามาในร้านมีกระเป๋าตังค์กันทุกคน แน่นอนว่าในนั้นก็ต้องมีเงินด้วยคงไม่มาหลอกกินฟรีเป็นแน่ เมื่อลูกค้าเรียกพนักงงานเพื่อมาเก็บเงิน พนักกงานจะเก็บเงินลูกค้าผ่านเมธอด collectMoney โดยทำขั้นตอนต่อไปนี้

  • บรรทัดที่44: บริกรล้วงกระเป๋าลูกค้าออกมาเองเลย
  • บรรทัดที่45: บริกรเปิดกระเป๋าตังค์ของลูกค้าพร้อมนับเงิน
  • บรรทัดที่48: ถ้าในกระเป๋าตังค์มีเงินไม่พอ ไล่ไปล้างจานท้ายครัวนู่น~~
  • บรรทัดที่53: แต่ถ้ามีเงินพอ ก็หยิบออกไปตามราคาอาหาร

เริ่มเห็นปัญหาแล้วใช่ไหมครับ? การออกแบบอ็อบเจ็กต์ของเรานั้นมีปัญหา กล่าวคือคงไม่มีลูกค้าที่ไหนชอบใจหรอกที่จะให้พนักงานมากระซวกกระเป๋าตังค์ไปนับเงินเองจริงไหม?

ปัญหาข้างต้นเกิดจากการที่เราออกแบบให้ waiter ของเรารู้มากเกินไป นั่นคือรู้แม้กระทั่งว่าลูกค้ามีกระเป๋าตังค์และในนั้นมีตังค์ด้วย! ถ้าสมมติวันหนึ่ง getMoney ในบรรทัดที่13คืนค่ากลับมาเป็น null เราก็ต้องมาแก้เมธอด collectMoney ของเราในบรรทัดที่48และ53ด้วยเพิ่อไม่ให้เกิดข้อผิดพลาดของโปรแกรมในการคำนวณตัวเลขกับค่า null ทั้งๆที่การจัดการข้อผิดพลาดของจำนวนเงินพนักงานไม่ควรรับรู้ด้วยซ้ำ ก็มันกระเป๋าตังค์ลูกค้านี่นา...

Law of Demeter

เพื่อลดการผูกติดกันแน่นหนึบของอ็อบเจ็กต์ที่ไม่สัมพันธ์กัน จึงเกิดหลักการในการออกแบบที่เรียกว่า Law of Demeter โดยหลักการที่ว่านี้บอกไว้ว่า

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

เมื่อประยุกต์หลักการข้างต้นเข้ากับโลกของ OOP โดยพิจารณาจากอ็อบเจ็กต์ waiter ของเรากับเมธอด collectMoney จะได้ว่า

  • collectMoney จะเรียกใช้ this ที่อ้างถึง waiter ได้เพราะมันคือความสัมพันธ์แบบผูกติด
  • collectMoney สามารถเรียกพารามิเตอร์ที่ส่งเข้ามาได้คือ customer และ money เพราะมันสัมพันธ์กัน
  • collectMoney สามารถเรียกอะไรก็ตามที่เป็นชิ้นส่วนของ waiter เช่น name
  • แต่ collectMoney ไม่สามารถเรียก customer.getWallet().getMoney() ได้ นั่นเป็นเพราะเราจะสนทนาแต่กับเพื่อนเรา เราจะไม่คุยกับเพื่อนของเพื่อนเรา พูดง่ายๆคือยิ่งพูดกับคนแปลกหน้าเยอะยิ่งเป็นการเอาเหามาใส่หัว

waiter จะเรียก customer.getWallet().getMoney() ไม่ได้นั่นเป็นเพราะบริกรไม่จำเป็นต้องรู้เลยว่าลูกค้ามีเงินเท่าไหร่ ขอเพียงเขามีปัญญาจ่ายตังค์ก็พอครับ

ถ้าเราสังเกตดูดีๆจะเริ่มค้นพบว่า ใช้ Law of Demeter ไปๆมาๆการเรียกเมธอดของเราจะเหลือแค่ . ตัวเดียว เช่น customer.getWallet().getMoney() มีสองจุดจึงผิดหลักการเพราะกำลังคุยอยู่กับเพื่อนของเพื่อนเราอีกที

Delegation และ one dot ไม่ใช่ Law of Demeter เสมอไป

ในหัวข้อที่แล้วผมบอกว่าการออกแบบโดยอิงหลักการ Law of Demeter นั้นมีแนวโน้มที่จะทำให้โค๊ดของเราเรียกเมธอดแบบใช้จุดเดียวหรือ one dot เพื่อนๆที่อ่านตรงนี้เลยอาจคิดไปว่า เห้ยงั้นเราทำยังไงก็ได้ให้การเรียกเมธอดของเราทำได้ด้วยการใช้แค่จุดเดียวซะซิ แบบที่เราทำกันในโลกของ Ruby on Rails ผ่าน delegate ดังนี้

Ruby
1# โค๊ดไม่ได้ผ่านการรันนะครับ อ่านเอาคอนเซ็ปต์พอครับ
2class Wallet
3 # set/get money ได้
4 attr_accessor :money
5
6 def initialize(money)
7 @money = money
8 end
9end
10
11class Customer
12 attr_accessor :wallet_money
13
14 # ลูกค้าของเรามีกระเป๋าตังค์ใบเดียว
15 has_one :wallet
16
17 # หัวใจอยู่ตรงนี้
18 # ต่อไปนี้เราไม่ต้องเรียก customer.wallet.money เพื่อดูเงินในกระเป๋าแล้ว
19 # ด้วยความสามารถของ delegate เราสามารถยุบเหลือเพียง
20 # customer.wallet_money ได้
21 # เย้เหลือแค่จุดเดียวแล้ว
22 delegate :money, to: :wallet, prefix: true
23end
24
25class Waiter
26 def collect_money(customer, money)
27 # ตรงนี้ก็ใช้จุดเดียว (one dot)
28 customer_money = customer.wallet_money
29
30 if customer_money < money
31 raise 'ไปล้างจานซะ!'
32 end
33
34 # ตรงนี้ก็ใช้จุดเดียว (one dot)
35 customer.wallet_money = customer_money - money
36 end
37end

โอวตลอกทั้งโค๊ดเราเรียกเมธอดอื่นโดยใช้จุดเดียวแล้ว! คงมาถูกทางแล้ว นี่ซินะเดชานุภาพของ Delegation กับ Law of Demeter!

ช้าก่อนครับ แม้เราจะลดการเรียกเมธอดจนเหลือแค่จุดเดียวแล้ว แต่ความจริงยังไม่เปลี่ยน การเรียก customer.wallet_money แท้จริงแล้วพนักงงานก็ยังคงรับรู้รายละเอียดอยู่ดีว่า ลูกค้ามีกระเป๋าตังค์และในกระเป๋านั้นมีเงิน ซึ่งมันผิดหลักการ information hiding หรือการซ่อนข้อมูลที่อ็อบเจ็กต์อื่นไม่ควรทราบนั่นเอง

จึงสรุปได้ว่า delegation ไม่ได้ช่วยให้ปัญหาการผูกติดกันเช่นนี้หายไปเลย ทั้งยังไม่ช่วยซ่อนข้อมูลตังค์ในกระเป๋าของเราออกจากการรับรู้ของบริกรอีกด้วย ต้องระวังตรงจุดนี้ครับเพราะส่วนใหญ่ที่ใช้หลักการนี้กันชอบแปลงโค๊ดให้เหลือ one dot โดยความหมายไม่เปลี่ยนแปลงเลย

Law of Demeter คือการระงับการรับรู้ส่วนที่ไม่เกี่ยวข้อง

เราเปลี่ยนโค๊ดของเราเสียใหม่ให้บริกรไม่สามารถเข้าถึงกระเป๋าตังค์เราได้ โดยให้โค๊ดในการตรวจสอบเงินคงเหลือในกระเป๋าตังค์อยู่ที่ wallet เองเลย พร้อมทั้งให้ลูกค้าเป็นผู้หยิบเงินออกจากกระเป๋าตังค์ด้วยตนเองผ่านเมธอด pay ดังนี้

TypeScript
1class Wallet {
2 private money: number
3
4 constructor(money: number) {
5 this.money = money
6 }
7
8 // อนุญาตให้ลูกค้าเป็นผู้หยิบเงินออกจากกระเป๋าตังค์ได้ด้วยตนเอง
9 withdraw(amount: number) {
10 // ย้ายการตรวจสอบจำนวนเงินมาไว้ในกระเป๋าตังค์เอง
11 // เป็นการปกปิดข้อมูลจำนวนเงินไม่ให้ภายนอกรับรู้
12 if (this.money < amount) {
13 throw new Error('Not enough!')
14 }
15
16 this.money -= amount
17 }
18}
19
20class Customer {
21 private wallet: Wallet
22
23 constructor(wallet: Wallet) {
24 this.wallet = wallet
25 }
26
27 pay(amount: number) {
28 // เพื่อนๆอาจสงสัยไหนผมบอกว่า Law of Demeter มีแนวโน้มทำให้โค๊ดเราเรียกเมธอดด้วย dot เดียว
29 // แต่อันนี้มีตั้งสองจุด?
30 // จริงๆแล้วมันมีแค่ dot เดียวครับคือ wallet.withdraw
31 // แต่ในภาษา TypeScript เราจะอ้างถึง attribute ในอ็อบเจ็กต์ได้
32 // ต้องเรียกผ่าน this
33 // ถ้าเราเรียกในภาษาอื่นอาจเหลือ dot เดียว
34 // เช่นใน Ruby จะเป็น @wallet.withdraw แทน
35 return this.wallet.withdraw(amount)
36 }
37}
38
39class Waiter {
40 private name: string
41
42 constructor(name: string) {
43 this.name = name
44 }
45
46 collectMoney(customer: Customer, money: number) {
47 // บริกรไม่รู้เห็นเงินในกระเป๋าตังค์เราอีกต่อไป!
48 customer.pay(money)
49 }
50}
51
52const wallet = new Wallet(1000)
53const customer = new Customer(wallet)
54const waiter = new Waiter('Adam Lorem')
55
56waiter.collectMoney(customer, 750)
57console.log(wallet.getMoney())

ทบทวนกระบวนการของหลักการ Law of Demeter

ลองตรวจสอบโค๊ดใหม่ที่เราเขียนอีกรอบว่าเข้าหลักการของ Law of Demeter หรือไม่

  • บรรทัดที่9: ภายใน withdraw ของเราเข้าถึงแค่ money ที่เป็น attribute ของอ็อบเจ็กต์เอง และเข้าถึง amount ที่เป็นพารามิเตอร์ในระดับชั้นเดียว ไม่ได้เรียกซ้อนเข้าไปเรื่อยๆจึงถูกต้อง
  • บรรทัดที่27: pay ของเรามีความสัมพันธ์ใกล้ชิดกับ wallet ในระดับเดียวด้วยการรับรู้เพียงว่า wallet มี withdraw ให้เรียกใช้ โดยไม่รู้ถึงไส้ในว่า wallet เองเก็บ money เอาไว้ จึงเป็นการรับรู้เพียงของที่ควรรู้เข้ากับหลักการของ Law of Demeter
  • บรรทัดที่46: collectMoney ไม่ได้ตรวจสอบเงินในกระเป๋าเองอีกแล้ว เพียงแต่โบ้ยให้ลูกค้าเป็นผู้รับผิดชอบจ่ายเงินผ่านเมธอด pay จึงเป็นการแบ่งแยกหน้าที่ของใครของมันอย่างชัดเจน

Law of Demeter นั้นช่วยให้โค๊ดของเราซับซ้อนน้อยลงด้วยการทำ loose coupling หรือให้อ็อบเจ็กต์แต่ละตัวผูกติดกันน้อยลง ด้วยการให้อ็อบเจ็กต์รู้จักเพียง unit ที่เกี่ยวข้องพอ ไม่รับรู้ถึงการทำงานของผู้อื่นเช่นไม่ให้บริกรรู้ว่าเงินในกระเป๋าตังค์ของลูกค้ามีเท่าไหร่เป็นต้น Law of Demeter จึงเป็นหนึ่งในหลักการสำคัญที่จะช่วยให้โค๊ดของเพื่อนๆดีงามพระรามแปดครับ

เอกสารอ้างอิง

Dan Manges (2007). Misunderstanding the Law of Demeter. Retrieved June, 20, 2016, from http://www.dan-manges.com/blog/37

Law of Demeter. Retrieved June, 20, 2016, from https://en.wikipedia.org/wiki/Law_of_Demeter

สารบัญ

สารบัญ

  • การออกแบบอ็อบเจ็กต์ให้รู้มากไปนั้นไม่ดี!
  • Law of Demeter
  • Delegation และ one dot ไม่ใช่ Law of Demeter เสมอไป
  • Law of Demeter คือการระงับการรับรู้ส่วนที่ไม่เกี่ยวข้อง
  • ทบทวนกระบวนการของหลักการ Law of Demeter
  • เอกสารอ้างอิง