[Design Pattern#1] ลดความยุ่งเหยิงของโค๊ดด้วยหลักการ Law of Demeter
ณ ร้านอาหารแห่งหนึ่ง ลูกค้าหน้าเดิมเรียกบริกรมาเก็บเงินหลังจากสวาปามอาหารมื้อใหญ่ร่วมสองชั่วโมง ด้วยโค๊ดต่อไปนี้พนักงานแฮปปี้ที่ได้เก็บตังค์ ส่วนลูกค้าก็ยิ้มแฉ่งจากความสุขที่ตัวเบาเพราะจ่ายตังค์จนหมดตัว
1// ตัวอย่างโปรแกรมใน TypeScript23// คลาสกระเป๋าตังค์4class Wallet {5 // กระเป๋าตังค์ก็ต้องมีตังค์ซิฮะ6 private money: number78 constructor(money: number) {9 this.money = money10 }1112 getMoney(): number {13 return this.money14 }1516 setMoney(money: number) {17 this.money = money18 }19}2021class Customer {22 // ลูกค้าที่เข้ามาในร้านต้องมีกระเป๋าตังค์23 // ไม่มีตังค์ก็ไปล้างจานตรงนู้่น~~24 private wallet: Wallet2526 constructor(wallet: Wallet) {27 this.wallet = wallet28 }2930 getWallet(): Wallet {31 return this.wallet32 }33}3435class Waiter {36 private name: string3738 constructor(name: string) {39 this.name = name40 }4142 // เมธอดเก็บตังค์จาก customer ด้วยจำนวนเงินเป็น money43 collectMoney(customer: Customer, money: number) {44 const wallet = customer.getWallet()45 const customerMoney = wallet.getMoney()4647 // ถ้าตังค์ไม่พอก็ไปล้างจานเลยคร๊าบ48 if (customerMoney < money) {49 throw new Error('Not enough!')50 }5152 // แต่ถ้าในกระเป๋าตังค์มีเงินพอก็หักเงินออกไป53 wallet.setMoney(customerMoney - money)54 }55}5657// ทั้งเนื้อทั้งตัวมีอยู่ 1,00058const wallet = new Wallet(1000)59const customer = new Customer(wallet)60const waiter = new Waiter('Adam Lorem')6162// พนักงงานเก็บค่าอาหารไป 75063waiter.collectMoney(customer, 750)64// ตัวเบาหวิวเหลือตังค์กลับบ้านแค่ 25065console.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
ดังนี้
1# โค๊ดไม่ได้ผ่านการรันนะครับ อ่านเอาคอนเซ็ปต์พอครับ2class Wallet3 # set/get money ได้4 attr_accessor :money56 def initialize(money)7 @money = money8 end9end1011class Customer12 attr_accessor :wallet_money1314 # ลูกค้าของเรามีกระเป๋าตังค์ใบเดียว15 has_one :wallet1617 # หัวใจอยู่ตรงนี้18 # ต่อไปนี้เราไม่ต้องเรียก customer.wallet.money เพื่อดูเงินในกระเป๋าแล้ว19 # ด้วยความสามารถของ delegate เราสามารถยุบเหลือเพียง20 # customer.wallet_money ได้21 # เย้เหลือแค่จุดเดียวแล้ว22 delegate :money, to: :wallet, prefix: true23end2425class Waiter26 def collect_money(customer, money)27 # ตรงนี้ก็ใช้จุดเดียว (one dot)28 customer_money = customer.wallet_money2930 if customer_money < money31 raise 'ไปล้างจานซะ!'32 end3334 # ตรงนี้ก็ใช้จุดเดียว (one dot)35 customer.wallet_money = customer_money - money36 end37end
โอวตลอกทั้งโค๊ดเราเรียกเมธอดอื่นโดยใช้จุดเดียวแล้ว! คงมาถูกทางแล้ว นี่ซินะเดชานุภาพของ Delegation กับ Law of Demeter!
ช้าก่อนครับ แม้เราจะลดการเรียกเมธอดจนเหลือแค่จุดเดียวแล้ว แต่ความจริงยังไม่เปลี่ยน การเรียก customer.wallet_money
แท้จริงแล้วพนักงงานก็ยังคงรับรู้รายละเอียดอยู่ดีว่า ลูกค้ามีกระเป๋าตังค์และในกระเป๋านั้นมีเงิน ซึ่งมันผิดหลักการ information hiding หรือการซ่อนข้อมูลที่อ็อบเจ็กต์อื่นไม่ควรทราบนั่นเอง
จึงสรุปได้ว่า delegation ไม่ได้ช่วยให้ปัญหาการผูกติดกันเช่นนี้หายไปเลย ทั้งยังไม่ช่วยซ่อนข้อมูลตังค์ในกระเป๋าของเราออกจากการรับรู้ของบริกรอีกด้วย ต้องระวังตรงจุดนี้ครับเพราะส่วนใหญ่ที่ใช้หลักการนี้กันชอบแปลงโค๊ดให้เหลือ one dot โดยความหมายไม่เปลี่ยนแปลงเลย
Law of Demeter คือการระงับการรับรู้ส่วนที่ไม่เกี่ยวข้อง
เราเปลี่ยนโค๊ดของเราเสียใหม่ให้บริกรไม่สามารถเข้าถึงกระเป๋าตังค์เราได้ โดยให้โค๊ดในการตรวจสอบเงินคงเหลือในกระเป๋าตังค์อยู่ที่ wallet เองเลย พร้อมทั้งให้ลูกค้าเป็นผู้หยิบเงินออกจากกระเป๋าตังค์ด้วยตนเองผ่านเมธอด pay ดังนี้
1class Wallet {2 private money: number34 constructor(money: number) {5 this.money = money6 }78 // อนุญาตให้ลูกค้าเป็นผู้หยิบเงินออกจากกระเป๋าตังค์ได้ด้วยตนเอง9 withdraw(amount: number) {10 // ย้ายการตรวจสอบจำนวนเงินมาไว้ในกระเป๋าตังค์เอง11 // เป็นการปกปิดข้อมูลจำนวนเงินไม่ให้ภายนอกรับรู้12 if (this.money < amount) {13 throw new Error('Not enough!')14 }1516 this.money -= amount17 }18}1920class Customer {21 private wallet: Wallet2223 constructor(wallet: Wallet) {24 this.wallet = wallet25 }2627 pay(amount: number) {28 // เพื่อนๆอาจสงสัยไหนผมบอกว่า Law of Demeter มีแนวโน้มทำให้โค๊ดเราเรียกเมธอดด้วย dot เดียว29 // แต่อันนี้มีตั้งสองจุด?30 // จริงๆแล้วมันมีแค่ dot เดียวครับคือ wallet.withdraw31 // แต่ในภาษา TypeScript เราจะอ้างถึง attribute ในอ็อบเจ็กต์ได้32 // ต้องเรียกผ่าน this33 // ถ้าเราเรียกในภาษาอื่นอาจเหลือ dot เดียว34 // เช่นใน Ruby จะเป็น @wallet.withdraw แทน35 return this.wallet.withdraw(amount)36 }37}3839class Waiter {40 private name: string4142 constructor(name: string) {43 this.name = name44 }4546 collectMoney(customer: Customer, money: number) {47 // บริกรไม่รู้เห็นเงินในกระเป๋าตังค์เราอีกต่อไป!48 customer.pay(money)49 }50}5152const wallet = new Wallet(1000)53const customer = new Customer(wallet)54const waiter = new Waiter('Adam Lorem')5556waiter.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
- เอกสารอ้างอิง