SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 5 – DIP)
และก็มาถึงหลักการข้อสุดท้ายของ SOLID Principles ที่ชื่อว่า Dependency Inversion Principle (DIP) กันแล้ว มีหลายคนอาจจะสับสนกับอีกหลักการที่ชื่อว่า Dependency Injection ซึ่งจริงๆ แล้วสองหลักการนี้ไม่เหมือนกันนะครับ
Dependency Inversion != Dependency Injection
แต่ก็ยังมีความเกี่ยวเนื่องกันเพราะว่าการที่เราจะทำตามหลักการ Dependency Inversion ได้นั้นเราต้องอาศัย Dependency Injection ดังนั้นแล้วเราควรจะมีความเข้าใจที่ดีต่อคำว่า Dependency Injection ก่อนซึ่งก็สามารถอ่านได้จากลิงค์ด้านล่างเลยครับ
บทความที่จำเป็นต้องอ่านก่อนเริ่ม
สารบัญสำหรับตอนอื่นๆ
- ตอนที่ 1 – Single Responsibility Principle (SRP)
- ตอนที่ 2 – Open Closed Principle (OCP)
- ตอนที่ 3 – Liskov Substitution Principle (LSP)
- ตอนที่ 4 – Interface Segregation Principle (ISP)
- ตอนที่ 5 – Dependency Inversion Principle (DIP)
Dependency Inversion Principle (DIP)
หลัก DIP ถูกกล่าวไว้ว่า
High-level code should not depend on low-level code. Both should depend on abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
ซึ่งก็แปลได้ว่า
High-level Code นั้นไม่ควรที่จะผูกติดอยู่กับ Low-level code แต่ทั้งสองนั้นควรจะผูกติดและขึ้นอยู่กับ Abstraction
และ Abstraction นั้นก็ไม่ควรผูกติดอยู่กับรายละเอียดการทำงาน แต่ในทางกลับกันรายละเอียดการทำงานควรจะผูกติดอยู่กับ Abstraction
ลองมาเจาะลึกถึงแต่ละคำกันครับ
Low-level code & High-level code
Low-level code ตามความหมายแล้วก็คือโค้ดที่เกี่ยวกับการทำงานพื้นฐานต่างๆ เช่น การอ่าน/เขียน ไฟล์ หรือ การอ่าน/เขียนข้อมูลจาก Database
High-level code นั้นก็คือโค้ดที่มีความซับซ้อนสูง(กว่า) Low-level code ซึ่งก็อาจจะเป็น โค้ดที่รวมการทำงานของ Low-level code หลายๆ ประเภทเข้าด้วยกันเพื่อให้สามารถทำงานอย่างใดอย่างหนึ่งได้
ลองมาดูตัวอย่างเกี่ยวกับร้านพิซซ่า ( PizzaShop ) กันครับ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class PizzaShop { protected $pizzaChef; protected $pizzaBoy; public function __construct(PizzaChef $pizzaChef, PizzaBoy $pizzaBoy) { $this->pizzaChef = $pizzaChef; $this->pizzaBoy = $pizzaBoy; } public function takeOrder() { $this->pizzaChef->makePizza(); $this->pizzaBoy->deliverPizza(); } } class PizzaChef { public function makePizza() { echo 'Making a pizza...'; } } class PizzaBoy { public function deliverPizza() { echo 'Delivering a pizza...'; } } |
จะเห็นว่าในที่นี้ร้านพิซซ่า ( PizzaShop ) เมื่อมี Order เข้ามาร้านพิซซ่านั้นต้องอาศัย พ่อครัว ( PizzaChef ) ในการทำพิซซ่า และ เด็กส่งพิซซ่า ( PizzaBoy ) เพื่อส่งพิซซ่าให้ลูกค้า ดังนั้นจะเห็นได้ว่า
Low-level code ในที่นี้ก็คือคลาสที่ทำงานทั่วๆ ก็คือ PizzaChef และ PizzaBoy
และสำหรับ High-level code ที่จะควบคุมการทำงานที่ซับซ้อนกว่าก็คือ PizzaShop
Abstraction
Abstraction ความหมายทั่วไปก็คือ “นามธรรม” หรือสิ่งที่เราจับต้องไม่ได้ ดังนั้นในทางโปรแกรมมิ่งก็คือ Abstract Class และ Interface นั่นเองเพราะทั้ง Abstract Class และ Interface นั้นไม่ได้เป็นที่สามารถใช้งานได้จริงเพียงแต่เป็นสิ่งที่ไว้สำหรับวางโครงสร้างพื้นฐานให้ Class นั่นเอง
ดังนั้นถ้าพูดง่ายๆ จากนิยามของ DIP นั้นก็คือ ทั้ง High-level และ Low-level code ควรจะต้องผูกติดอยู่กับ Abstract หรือ Interface นั่นเอง
กลับมาดูคลาส PizzaShop กันครับ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PizzaShop { protected $pizzaChef; protected $pizzaBoy; public function __construct(PizzaChef $pizzaChef, PizzaBoy $pizzaBoy) { $this->pizzaChef = $pizzaChef; $this->pizzaBoy = $pizzaBoy; } public function takeOrder() { $this->pizzaChef->makePizza(); $this->pizzaBoy->deliverPizza(); } } |
จะเห็นได้ว่าตอนนี้ PizzaShop ที่เป็น High-level code นั้นถูกผูกอยู่กับ PizzaChef และ PizzaBoy ที่เป็น Low-level code อยู่ ซึ่งไม่เป็นไปตามหลักการของ DIP แต่ก่อนที่เราจะ Refactor คลาสนี้เราลองมาดูกันก่อนว่าการที่ High-level code ผูกติดอยู่กับ Low-level code นั้นไม่ดียังไง
ลองคิดถึงสถานการณ์ที่ว่าร้านพิซซ่า ( PizzaShop ) นั้นมีพ่อครัวอิตาเลียนคนใหม่ ( ItalianChef ) มาแทนที่พ่อครัวพิซซ่าคนเดิม ( PizzaChef )
1 2 3 4 5 6 7 |
class ItalianChef { public function makePizza() { echo 'Making a pizza'; } } |
และเมื่อเราส่งคลาสใหม่เข้าไปก็จะทำให้เกิด Error เนื่องจาก PizzaShop นั้นถูกผูกติดอยู่กับเฉพาะ (Highly coupled) PizzaChef เท่านั้น
ทำให้เราต้องแก้ไขคลาส PizzaShop เป็น
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PizzaShop { protected $pizzaChef; protected $pizzaBoy; public function __construct(ItalianChef $pizzaChef, PizzaBoy $pizzaBoy) { $this->pizzaChef = $pizzaChef; $this->pizzaBoy = $pizzaBoy; } public function takeOrder() { $this->pizzaChef->makePizza(); $this->pizzaBoy->deliverPizza(); } } |
ถึงจุดนี้คลาส PizzaShop เราก็ผูกติดอยู่กับ ItalianChef แทน รวมถึงการที่เราแก้ไขคลาสนั้นก็คือการฝ่าฝืนหลัก Open Closed Principle (OCP) ด้วย ซึ่งหลัก OCP ได้บอกไว้ว่า
โค้ดนั้นต้องเปิดรับต่อส่วนขยาย แต่ปิดสำหรับการแก้ไข
และในภายหลังถ้าเรามีการเปลี่ยนแปลงคลาสอื่นๆ ไม่ว่าจะเป็นพ่อครัว ( PizzaChef ) หรือ เด็กส่งพิซซ่า ( PizzaBoy ) เราก็จำเป็นที่จะต้องแก้ไขคลาส PizzaShop อีก ซึ่งก็เป็นการเพิ่มความเสี่ยงให้ตัวโปรแกรมเรามี Error ในทุกๆ ครั้งที่แก้ไขอีกด้วย
เริ่ม Refactor
เราเริ่มด้วยการสร้าง Abstraction ครับ สำหรับผมแล้วหลักการคิดในการสร้าง Abstraction คือ การดูว่าคลาสที่เป็น High-lvel code นั้นสนใจที่จะใช้อะไรรวมถึงจำเป็นที่จะต้องรู้อะไรบ้าง ซึ่งในที่นี้คลาส PizzaShop สนใจเพียงคนที่จะทำพิซซ่าได้ ( CanMakePizza ) และคนที่จะส่งพิซซ่าได้ ( CanDeliverPizza )
1 2 3 4 5 6 7 8 9 |
interface CanMakePizza { public function makePizza(); } interface CanDeliverPizza { public function deliverPizza(); } |
ตามด้วยการทำให้ High-level code ผูกติดอยู่กับ Abstraction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PizzaShop { protected $pizzaChef; protected $pizzaBoy; public function __construct(CanMakePizza $pizzaChef, CanDeliverPizza $pizzaBoy) { $this->pizzaChef = $pizzaChef; $this->pizzaBoy = $pizzaBoy; } public function takeOrder() { $this->pizzaChef->makePizza(); $this->pizzaBoy->deliverPizza(); } } |
หลังจากนั้นก็ทำให้ Low-level code ผูกติดอยู่กับ Abstraction เช่นกัน
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class PizzaChef implements CanMakePizza { public function makePizza() { echo 'Making a pizza...'; } } class PizzaBoy implements CanDeliverPizza { public function deliverPizza() { echo 'Delivering a pizza...'; } } |
เพียงเท่านี้คลาสของเราก็ทำตามหลักการของ DIP เป็นที่เรียบร้อยแล้ว และถ้าหากเรามีพ่อครัวอิตาเลียนคนใหม่ ( ItalianChef ) สิ่งที่เราต้องทำก็คือการทำคลาสใหม่นี้ผูกติดกับ Abstraction เช่นเดียวกัน
1 2 3 4 5 6 7 |
class ItalianChef implements CanMakePizza { public function makePizza() { echo 'Making a pizza'; } } |
หรือจะเปลี่ยนคลาสเด็กส่งพิซซ่าเป็นไปรษณีย์ไทย ThailandPost
1 2 3 4 5 6 7 |
class ThailandPost implements CanDeliverPizza { public function deliverPizza() { echo 'Delivering a pizza...'; } } |
และเรียกใช้งานโดย
1 2 |
$pizzaShop = new PizzaShop(new ItalianChef(), new ThailandPost()); $pizzaShop->takeOrder(); |
บทสรุป
จะเห็นได้ว่าเมื่อเราทำให้คลาส PizzaShop ให้เป็นไปตามหลัก DIP แล้ว คลาส PizzaShop นั้นจะไม่ผูกติดอยู่กับเฉพาะ PizzaChef และ PizzaBoy อีกต่อไป ทำให้เมื่อเราต้องการเปลี่ยนพ่อครัวหรือคนส่งพิซซ่าหลังจากนี้ เราก็ไม่จำเป็นที่จะต้องแก้ไขคลาส PizzaShop อีกเลย
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 4 - ISP) - IKQ.ME()