Liskov Substitution Principle (LSP): Class ลูกต้องแทนที่ Class แม่ได้
จากบทที่แล้วที่เราได้เรียนรู้เกี่ยวกับ Open/Closed Principle (OCP) ซึ่งเน้นการออกแบบให้สามารถเพิ่มฟังก์ชันใหม่ได้โดยไม่ต้องแก้ไขโค้ดเดิม คราวนี้เราจะมาทำความรู้จักกับ Liskov Substitution Principle (LSP) ซึ่งเป็นหลักการสำคัญอีกข้อหนึ่งในการออกแบบเชิงวัตถุที่ดี และมีความเกี่ยวพันกับ OCP อย่างใกล้ชิด
ความหมายของ LSP
Liskov Substitution Principle (LSP) กล่าวอย่างง่ายๆ ว่า "ถ้าคุณมี Class แม่ (Parent Class) คุณควรจะสามารถใช้ Class ลูก (Child Class) แทน Class แม่ได้ โดยที่โปรแกรมยังทำงานได้อย่างถูกต้อง"
พูดให้เจาะจงขึ้นคือ Class ลูกต้องไม่เปลี่ยนแปลงพฤติกรรมของ Class แม่ในลักษณะที่ทำให้โค้ดที่ใช้ Class แม่ทำงานผิดพลาด หากมีการใช้ Class ลูกแทนที่
หลักการนี้มีความสำคัญอย่างยิ่ง เพราะถ้าเราไม่สามารถใช้ Class ลูกแทน Class แม่ได้ นั่นหมายความว่าเราได้ละเมิดหลักการสืบทอด (Inheritance) และ Polymorphism ไปแล้ว ซึ่งจะนำไปสู่ปัญหาในการบำรุงรักษาโค้ดในระยะยาว
ตัวอย่างการละเมิด LSP
ลองพิจารณาตัวอย่างง่ายๆ ของ Class Rectangle (สี่เหลี่ยมผืนผ้า) และ Square (สี่เหลี่ยมจัตุรัส) ซึ่ง Square สืบทอดมาจาก Rectangle
<?php
class Rectangle {
protected $width;
protected $height;
public function setWidth(int $width): void {
$this->width = $width;
}
public function setHeight(int $height): void {
$this->height = $height;
}
public function getWidth(): int {
return $this->width;
}
public function getHeight(): int {
return $this->height;
}
public function getArea(): int {
return $this->width * $this->height;
}
}
class Square extends Rectangle {
public function setWidth(int $width): void {
$this->width = $width;
$this->height = $width;
}
public function setHeight(int $height): void {
$this->width = $height;
$this->height = $height;
}
}
// Example usage
function resizeRectangle(Rectangle $rectangle, int $newWidth, int $newHeight): void {
$rectangle->setWidth($newWidth);
$rectangle->setHeight($newHeight);
echo "Area after resize: " . $rectangle->getArea() . "<br>";
}
$rectangle = new Rectangle();
$rectangle->setWidth(5);
$rectangle->setHeight(10);
resizeRectangle($rectangle, 20, 5); // Expected: 100
$square = new Square();
$square->setWidth(5);
$square->setHeight(10); // This will actually set both width and height to 10
resizeRectangle($square, 20, 5); // Expected: 100, Actual: 25
ในตัวอย่างนี้ ฟังก์ชัน resizeRectangle รับ Rectangle เป็น Parameter และทำการปรับขนาดความกว้างและความสูง จากนั้นคำนวณพื้นที่
เมื่อเราส่ง Rectangle เข้าไป ฟังก์ชันทำงานได้อย่างถูกต้อง แต่เมื่อเราส่ง Square เข้าไป (ซึ่งเป็น Class ลูกของ Rectangle) พฤติกรรมกลับเปลี่ยนไป เนื่องจาก Square override setWidth และ setHeight ให้ปรับทั้งความกว้างและความสูงให้เท่ากัน
ผลลัพธ์คือ เมื่อเราตั้งค่าความกว้างเป็น 20 และความสูงเป็น 5 ใน Square, ทั้งความกว้างและความสูงจะถูกตั้งเป็น 5 ทำให้พื้นที่ที่คำนวณได้คือ 25 ซึ่งไม่ตรงกับที่คาดหวัง (100) นี่คือการละเมิด LSP เพราะ Square ไม่สามารถใช้แทน Rectangle ได้อย่างถูกต้องในบริบทนี้
การแก้ไข LSP
ปัญหาในตัวอย่างก่อนหน้าเกิดจากการที่เราพยายามบังคับให้ Square "เป็น" Rectangle ในขณะที่จริงๆ แล้ว Square มีข้อจำกัดที่แตกต่างออกไป
ทางออกที่ดีกว่าคือการใช้ Abstraction ที่เหมาะสมกว่า เช่น Interface หรือ Abstract Class ที่กำหนดพฤติกรรมที่เกี่ยวข้องกับรูปทรงเรขาคณิตโดยทั่วไป จากนั้นให้ทั้ง Rectangle และ Square Implement Interface หรือสืบทอดจาก Abstract Class นั้น
<?php
interface Shape {
public function getArea(): int;
}
class Rectangle implements Shape {
protected $width;
protected $height;
public function __construct(int $width, int $height) {
$this->width = $width;
$this->height = $height;
}
public function setWidth(int $width): void {
$this->width = $width;
}
public function setHeight(int $height): void {
$this->height = $height;
}
public function getWidth(): int {
return $this->width;
}
public function getHeight(): int {
return $this->height;
}
public function getArea(): int {
return $this->width * $this->height;
}
}
class Square implements Shape {
protected $side;
public function __construct(int $side) {
$this->side = $side;
}
public function setSide(int $side): void {
$this->side = $side;
}
public function getSide(): int {
return $this->side;
}
public function getArea(): int {
return $this->side * $this->side;
}
}
// Example usage
function printArea(Shape $shape): void {
echo "Area: " . $shape->getArea() . "<br>";
}
$rectangle = new Rectangle(5, 10);
printArea($rectangle); // Area: 50
$square = new Square(5);
printArea($square); // Area: 25
ในตัวอย่างนี้ เราได้สร้าง Interface Shape ที่มี Method getArea จากนั้นให้ทั้ง Rectangle และ Square Implement Interface นี้
เรายังได้ลบ setWidth และ setHeight ออกจาก Square และแทนที่ด้วย setSide เนื่องจาก Square มีด้านเดียวเท่านั้น
ฟังก์ชัน printArea รับ Shape เป็น Parameter ซึ่งสามารถเป็นได้ทั้ง Rectangle หรือ Square โดยที่ไม่ต้องกังวลว่าพฤติกรรมจะเปลี่ยนแปลงไป นี่คือการปฏิบัติตาม LSP
เหตุผลเชิงออกแบบ (Design Rationale): การใช้ Interface Shape ช่วยให้เราสามารถกำหนดสัญญา (Contract) ที่ชัดเจนว่า Object ใดๆ ที่ Implement Interface นี้จะต้องมี Method getArea ซึ่งจะคืนค่าพื้นที่ของรูปทรงนั้นๆ ทำให้เรามั่นใจได้ว่าเมื่อเราใช้ Object ที่ Implement Shape เราจะสามารถเรียก getArea ได้อย่างปลอดภัย และได้ผลลัพธ์ที่คาดหวัง
สรุป
Liskov Substitution Principle (LSP) เป็นหลักการสำคัญในการออกแบบเชิงวัตถุที่เน้นว่า Class ลูกควรจะสามารถใช้แทน Class แม่ได้ โดยที่โปรแกรมยังทำงานได้อย่างถูกต้อง การละเมิด LSP อาจนำไปสู่ปัญหาในการบำรุงรักษาโค้ดในระยะยาว
การใช้ Abstraction ที่เหมาะสม เช่น Interface หรือ Abstract Class เป็นวิธีที่ดีในการแก้ไขปัญหาการละเมิด LSP และทำให้โค้ดของเรามีความยืดหยุ่นและง่ายต่อการบำรุงรักษามากขึ้น
ในบทถัดไป เราจะมาเรียนรู้เกี่ยวกับ Interface Segregation Principle (ISP) ซึ่งเป็นหลักการที่เกี่ยวข้องกับ Interface โดยตรง และจะช่วยให้เราออกแบบ Interface ได้ดียิ่งขึ้น