SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 4 – ISP)
สำหรับหลัก ISP ของ SOLID Principles ตอนนี้อาจจะเน้นหนักไปทาง Interface นิดนะครับ ดังนั้นถ้าท่านใดยังไม่ค่อยคุ้นกับ Interface แนะนำให้ลองอ่านลิงค์ตามด้านล่างก่อนเริ่มนะครับ
สารบัญสำหรับตอนอื่นๆ
- ตอนที่ 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)
Interface Segregation Principle (ISP)
กฎข้อนี้กล่าวไว้ว่า
no client should be forced to depend on methods it does not use
หมายความว่า
Class ไม่ควรที่จะถูกบังคับให้มี Method ที่ Class นั้นไม่ได้ใช้
ลองมาดูตัวอย่างกันครับ เริ่มแรกเรามี CarInterface ซึ่งประกอบไปด้วย 3 Method สำหรับการ สตาร์ท, เคลื่อน, และ เติมน้ำมันให้รถ
1 2 3 4 5 6 7 8 9 10 11 |
interface CarInterface { // สตาร์ทรถ public function startEngine(); // เคลื่อนที่ public function move(); // เติมน้ำมัน public function fillUpFuel(); } |
และเรามีคลาส ToyotaAltis ซึ่ง Implement CarInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ToyotaAltis implements CarInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function fillUpFuel() { echo 'Filling up fuel.'; } } |
และเราก็มีคลาสคนขับรถ Driver ซึ่งมีหน้าที่ในการควบคุมรถยนต์
1 2 3 4 5 6 7 8 |
class Driver { public function control(CarInterface $car){ $car->startEngine(); $car->move(); $car->fillUpFuel(); } } |
หลังจากนั้นเราเพิ่มคลาสรถยนต์พลังงานไฟฟ้ารุ่นใหม่ล่าสุด ชื่อ ToyotaIQ ซึ่งไม่จำเป็นต้องเติมน้ำมันเนื่องจากใช้การชาร์จพลังงานไฟฟ้าอย่างเดียว ซึ่งเราก็จะเพิ่ม Method สำหรับการชาร์จไฟฟ้าเข้าไปใน CarInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
interface CarInterface { // สตาร์ทรถ public function startEngine(); // เคลื่อนที่ public function move(); // เติมน้ำมัน public function fillUpFuel(); // ชาร์จแบตเตอรี่ public function chargeBattery(); } |
ทำให้เราต้องเพิ่ม Method chargeBattery เข้าไปใน ToyotaAltis ด้วยแต่เนื่องจาก Toyota Altis เอง ใช้น้ำมันเป็นพลังงาน ทำให้เราต้องสร้าง Method ทิ้งไว้เพื่อให้ไม่เกิด Error เมื่อรันโปรแกรม
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ToyotaAltis implements CarInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function fillUpFuel() { echo 'Filling up gas.'; } public function chargeBattery() { return null; } } |
เช่นกันสำหรับคลาส ToyotaIQ จะมี Method fillUpFuel ที่คลาสไม่ได้ใช้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ToyotaIQ implements CarInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function fillUpFuel() { return null; } public function chargeBattery() { echo 'Charging battery.'; } } |
ทีนี้จะเห็นได้ว่าทั้งคลาส ToyotaAltis และ ToyotaIQ มี Method ที่ไม่ได้จำเป็นใช้ต้องใช้ แต่ถูกบังคับให้มีเพราะทั้งสองคลาสนั้น Implement CarInterface และจำเป็นต้องมี Method ทั้งหมดที่ Interface มี และนั่นก็คือการฝ่าฝืนกด ISP นั่นเอง
เริ่ม Refactor
สาเหตุที่ทั้ง 2 คลาสนั้นมี Method ที่ไม่ได้ใช้อยู่ในคลาสนั้นก็เพราะว่า CarInterface นั้นถูกออกแบบให้ทำงานกว้างเกินไป (Fat Interface) ซึ่งหลัก ISP นั้นก็แนะนำให้เราแบ่งการทำงานของ Interface นั่นให้เฉพาะเจาะจงที่สุด ซึ่งจะทำให้คลาสที่จะ Implement Interface นั้นมีเฉพาะ Method ที่ตัวคลาสนั้นต้องการใช้
มาถึงจุดนี้ประโยคที่ว่า
แบ่งการทำงานของ Interface นั่นให้เฉพาะเจาะจงที่สุด
ทำให้เรานึกถึงหลัก SOLID ข้อไหนบ้างมั้ยครับ? … ใช่แล้วครับหลักการนั้นก็คือ Single Responsibility Principle (SRP) นั่นเอง ถึงตอนนี้เราก็จะทราบแล้วว่าไม่ว่าจะเป็น Class ธรรมดาหรือ Interface ก็แล้วแต่ การออกแบบนั้นก็ต้องทำให้ตัวคลาสหรือ Interface นั้นมีหน้าที่และความรับผิดชอบเพียงอย่างเดียวเช่นกัน
ลองมา Refactor โค้ดก่อนหน้านี้กันครับ โดยผมจะเพิ่ม Interface อีก 2 ตัวด้วยกันนั่นก็คือ FuelFillableInterface และ BatteryChargeableInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface BatteryChargeableInterface { // ชาร์จแบตเตอรี่ public function chargeBattery(); } interface FuelFillableInterface { // เติมน้ำมัน public function fillUpFuel(); } |
และปรับ CarInterface เป็น
1 2 3 4 5 6 7 8 9 |
interface CarInterface { // สตาร์ทรถ public function startEngine(); // เคลื่อนที่ public function move(); } |
ทีนี้กลับมาที่ ToyotaAltis ซึ่งเป็นรถยนต์ใช้น้ำมันเชื้อเพลิง ดังนั้นเราสามารถให้คลาสนี้ Implement FuelFillableInterface ได้ หลังจากนั้นเราก็สามารถลบ chargeBattery ออกได้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ToyotaAltis implements CarInterface, FuelFillableInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function fillUpFuel() { echo 'Filling up fuel.'; } } |
และสำหรับ ToyotaIQ ก็ให้คลาส Implement BatteryChargeableInterface และลบ fillUpFuel ออก
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ToyotaIQ implements CarInterface, BatteryChargeableInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function chargeBattery() { echo 'Charging battery.'; } } |
จะเห็นได้ว่าตอนนี้ทั้ง 2 คลาส ToyotaAltis และ ToyotaIQ มีเพียง Method ที่คลาสนั้นๆ จำเป็นต้องมีแล้วและนั่นก็ทำให้คลาสทั้งสองของเราเป็นไปตามหลัก ISP เรียบร้อยแล้ว 🙂
ทีนี้ลองกลับมาดูคลาส Driver ของเรากันครับ เนื่องจากทั้ง ToyotaAltis และ ToyotaIQ นั้น Implement CarInterface ทั้งคู่แต่เพราะการเติมพลังงานของทั้งสองคลาสนั้นไม่เหมือนกัน ทำให้เราต้องเช็คประเภทของคลาสก่อนการเรียก Method ดังนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Driver { public function control(CarInterface $car) { $car->startEngine(); $car->move(); if ($car instanceof ToyotaAltis) { $car->fillUpFuel(); } elseif ($car instanceof ToyotaIQ) { $car->chargeBattery(); } else { throw new \Exception('Unsupported car.'); } } } |
แต่…การใช้วิธีการเช็คประเภทของคลาสเพื่อเรียก Method นั้นเป็นการฝ่าฝืนหลัก Open Closed Principle (OCP) เพราะถ้าในอนาคตเรามีคลาสรถยนต์ใหม่เช่น ToyotaHydrogen ซึ่งใช้การเติมพลังงานเป็นไฮโดรเจนทำให้เราต้องเพิ่มการเช็คเข้าไปเรื่อยๆ ดังนั้นสิ่งที่ผมจะทำก็คือการเพิ่ม ControllableInterface เข้าไป
1 2 3 4 |
interface ControllableInterface { public function control(); } |
และให้คลาส ToyotaAltis และ ToyotaIQ Implement และรวม Method ที่ใช้สำหรับในการควบคุมรถยนต์เข้าไปใน Method control()
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class ToyotaAltis implements CarInterface, FuelFillableInterface, ControllableInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function fillUpFuel() { echo 'Filling up fuel.'; } public function control() { $this->startEngine(); $this->move(); $this->fillUpFuel(); } } class ToyotaIQ implements CarInterface, BatteryChargeableInterface, ControllableInterface { public function startEngine() { echo 'Engine starting.'; } public function move() { echo 'Car moving.'; } public function chargeBattery() { echo 'Charging battery.'; } public function control() { $this->startEngine(); $this->move(); $this->chargeBattery(); } } |
และเราสามารถกลับมาแก้คลาส Driver ได้ดังนี้
1 2 3 4 5 6 7 |
class Driver { public function control(ControllableInterface $car) { $car->control(); } } |
ทำให้เมื่อเรามีรถยนต์ประเภทใหม่ สิ่งที่ต้องทำก็คือการให้คลาสใหม่นั้น Implement ControllableInterface นั่นเอง โดยที่เราไม่ต้องแก้ไขคลาส Driver เลย
บทสรุป
หลักการนี้ค่อนข้างที่มีความคล้ายกับหลัก SRP นั่นก็คือการออกแบบ Interface นั้นก็ต้องกำหนดหน้าที่และความรับผิดชอบให้มีเพียงอย่างเดียวเช่นเดียวกับคลาส นั่นก็จะทำให้เรากำจัด Method ที่คลาสไม่จำเป็นต้องใช้งานออกไปได้
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 1 - SRP)()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 2 - OCP) - IKQ.ME()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 3 - LSP) - IKQ.ME()
Pingback: latestvideo sirius800 abdu23na5283 abdu23na68()